完善教程&帮助中心页面
This commit is contained in:
31
src/api/article.ts
Normal file
31
src/api/article.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { ApiResponse } from "@/lib/api"
|
||||||
|
import type { ArticleListResult } from "@/lib/models/article"
|
||||||
|
import { callPublic } from "./base"
|
||||||
|
|
||||||
|
type RawArticleResponse = ArticleListResult | ArticleListResult[]
|
||||||
|
|
||||||
|
function normalizeListResult(
|
||||||
|
result: RawArticleResponse | undefined,
|
||||||
|
): ArticleListResult {
|
||||||
|
if (!result) return { RowCount: 0, List: [] }
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
return result[0] ?? { RowCount: 0, List: [] }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchArticles(
|
||||||
|
props: { catalog?: number; keyword?: string } = {},
|
||||||
|
): Promise<ApiResponse<ArticleListResult>> {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (props.catalog !== undefined) params.set("Catalog", String(props.catalog))
|
||||||
|
if (props.keyword?.trim()) params.set("KeyWord", props.keyword.trim())
|
||||||
|
const query = params.toString()
|
||||||
|
const res = await callPublic<RawArticleResponse>(
|
||||||
|
`/article/apiindex${query ? `?${query}` : ""}`,
|
||||||
|
undefined,
|
||||||
|
"GET",
|
||||||
|
)
|
||||||
|
if (!res.success) return res
|
||||||
|
return { success: true, data: normalizeListResult(res.data) }
|
||||||
|
}
|
||||||
@@ -1,47 +1,17 @@
|
|||||||
import type { ApiResponse } from "@/lib/api"
|
import type { ApiResponse } from "@/lib/api"
|
||||||
|
import type {
|
||||||
|
CreateOrderResult,
|
||||||
|
GameCity,
|
||||||
|
GameItem,
|
||||||
|
GameOrderData,
|
||||||
|
HttpOrderInfo,
|
||||||
|
} from "@/lib/models/http"
|
||||||
|
|
||||||
export const PHP_API_BASE_URL =
|
export const PHP_API_BASE_URL =
|
||||||
import.meta.env.VITE_PHP_API_BASE_URL ?? "https://php-api.juip.com"
|
import.meta.env.VITE_PHP_API_BASE_URL ?? "https://php-api.juip.com"
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT = 30_000
|
const DEFAULT_TIMEOUT = 30_000
|
||||||
|
|
||||||
export type GameOrderData = {
|
|
||||||
isAbroad: number
|
|
||||||
isRelayed: number
|
|
||||||
shareType: number
|
|
||||||
gameId: number
|
|
||||||
lineType: number
|
|
||||||
bandwidth: number
|
|
||||||
cityCode: number
|
|
||||||
isp: number
|
|
||||||
ipAmount: number
|
|
||||||
periodType: number
|
|
||||||
periodAmount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GameCity = {
|
|
||||||
code: number
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GameItem = {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HttpOrderInfo = {
|
|
||||||
order_type: number
|
|
||||||
money: number
|
|
||||||
pay_type: number
|
|
||||||
data: GameOrderData
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateOrderResult = {
|
|
||||||
code: number
|
|
||||||
msg?: string
|
|
||||||
data?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type RawHttpResponse = Record<string, unknown>
|
type RawHttpResponse = Record<string, unknown>
|
||||||
|
|
||||||
async function postJson<T = RawHttpResponse>(
|
async function postJson<T = RawHttpResponse>(
|
||||||
|
|||||||
@@ -4,44 +4,69 @@ import {
|
|||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useEffect, useMemo, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { Toaster, toast } from "sonner"
|
||||||
|
import { fetchArticles } from "@/api/article"
|
||||||
import Wrap from "@/components/wrap"
|
import Wrap from "@/components/wrap"
|
||||||
import { ARTICLES, type Article, CATEGORIES } from "@/lib/models/article"
|
import {
|
||||||
|
type Article,
|
||||||
|
CATEGORIES,
|
||||||
|
transformArticles,
|
||||||
|
} from "@/lib/models/article"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const VIDEO_TUTORIALS: Record<number, string> = {
|
||||||
|
3: "http://mp4.juip.com/%E8%8B%B9%E6%9E%9C%E6%89%8B%E6%9C%BA%E6%95%99%E7%A8%8B.mp4",
|
||||||
|
4: "http://mp4.juip.com/%E5%AE%89%E5%8D%93%E6%89%8B%E6%9C%BA%E6%95%99%E7%A8%8B.mp4",
|
||||||
|
5: "http://mp4.juip.com/%E6%A8%A1%E6%8B%9F%E5%99%A8%E6%95%99%E7%A8%8B.mp4",
|
||||||
|
56: "http://mp4.juip.com/wjjc.mp4",
|
||||||
|
}
|
||||||
|
|
||||||
export default function HelpPage() {
|
export default function HelpPage() {
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("")
|
||||||
|
const [keyword, setKeyword] = useState("")
|
||||||
const [activeCategoryId, setActiveCategoryId] = useState<number | null>(null)
|
const [activeCategoryId, setActiveCategoryId] = useState<number | null>(null)
|
||||||
const [selectedArticle, setSelectedArticle] = useState<Article | null>(null)
|
const [selectedArticle, setSelectedArticle] = useState<Article | null>(null)
|
||||||
|
const [articles, setArticles] = useState<Article[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
useEffect(() => {
|
||||||
let list = ARTICLES
|
let cancelled = false
|
||||||
|
setLoading(true)
|
||||||
if (activeCategoryId !== null) {
|
const load = async () => {
|
||||||
list = list.filter(a => a.catalogId === activeCategoryId)
|
const res = await fetchArticles({
|
||||||
|
catalog: activeCategoryId ?? undefined,
|
||||||
|
keyword,
|
||||||
|
})
|
||||||
|
if (cancelled) return
|
||||||
|
setLoading(false)
|
||||||
|
if (!res.success) {
|
||||||
|
setArticles([])
|
||||||
|
toast.error(res.message)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
setArticles(transformArticles(res.data.List ?? []))
|
||||||
if (search.trim()) {
|
|
||||||
const kw = search.trim().toLowerCase()
|
|
||||||
list = list.filter(
|
|
||||||
a =>
|
|
||||||
a.title.toLowerCase().includes(kw) ||
|
|
||||||
a.keyword.toLowerCase().includes(kw) ||
|
|
||||||
a.content.toLowerCase().includes(kw),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
void load()
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [activeCategoryId, keyword])
|
||||||
|
|
||||||
return list
|
const handleSearch = () => {
|
||||||
}, [search, activeCategoryId])
|
setKeyword(search.trim())
|
||||||
|
setSelectedArticle(null)
|
||||||
|
}
|
||||||
|
|
||||||
const currentIndex = selectedArticle
|
const currentIndex = selectedArticle
|
||||||
? filtered.findIndex(a => a.id === selectedArticle.id)
|
? articles.findIndex(a => a.id === selectedArticle.id)
|
||||||
: -1
|
: -1
|
||||||
|
|
||||||
const prevArticle = currentIndex > 0 ? filtered[currentIndex - 1] : null
|
const prevArticle = currentIndex > 0 ? articles[currentIndex - 1] : null
|
||||||
const nextArticle =
|
const nextArticle =
|
||||||
currentIndex >= 0 && currentIndex < filtered.length - 1
|
currentIndex >= 0 && currentIndex < articles.length - 1
|
||||||
? filtered[currentIndex + 1]
|
? articles[currentIndex + 1]
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const handleArticleClick = (article: Article) => {
|
const handleArticleClick = (article: Article) => {
|
||||||
@@ -56,6 +81,7 @@ export default function HelpPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-5rem)] bg-gray-50">
|
<div className="min-h-[calc(100vh-5rem)] bg-gray-50">
|
||||||
|
<Toaster position="top-center" richColors />
|
||||||
<Wrap className="pt-28 pb-16">
|
<Wrap className="pt-28 pb-16">
|
||||||
<div className="max-w-2xl mx-auto mb-10">
|
<div className="max-w-2xl mx-auto mb-10">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -69,8 +95,14 @@ export default function HelpPage() {
|
|||||||
setSearch(e.target.value)
|
setSearch(e.target.value)
|
||||||
setSelectedArticle(null)
|
setSelectedArticle(null)
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === "Enter") handleSearch()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<button className="absolute right-2 top-1/2 -translate-y-1/2 px-6 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition">
|
<button
|
||||||
|
onClick={handleSearch}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 px-6 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition cursor-pointer"
|
||||||
|
>
|
||||||
搜索
|
搜索
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,27 +111,7 @@ export default function HelpPage() {
|
|||||||
<div className="flex gap-8">
|
<div className="flex gap-8">
|
||||||
<aside className="w-52 shrink-0">
|
<aside className="w-52 shrink-0">
|
||||||
<nav className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden sticky top-28">
|
<nav className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden sticky top-28">
|
||||||
<div className="px-5 py-4 border-b border-gray-100">
|
|
||||||
<h3 className="font-semibold text-gray-800">帮助分类</h3>
|
|
||||||
</div>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setActiveCategoryId(null)
|
|
||||||
setSelectedArticle(null)
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"w-full text-left px-5 py-3 text-sm transition-colors flex items-center justify-between",
|
|
||||||
activeCategoryId === null
|
|
||||||
? "bg-blue-50 text-blue-600 font-medium"
|
|
||||||
: "text-gray-600 hover:bg-gray-50",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
全部文章
|
|
||||||
<ChevronRightIcon className="size-4" />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{CATEGORIES.map(cat => (
|
{CATEGORIES.map(cat => (
|
||||||
<li key={cat.id}>
|
<li key={cat.id}>
|
||||||
<button
|
<button
|
||||||
@@ -110,8 +122,8 @@ export default function HelpPage() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"w-full text-left px-5 py-3 text-sm transition-colors flex items-center justify-between",
|
"w-full text-left px-5 py-3 text-sm transition-colors flex items-center justify-between",
|
||||||
activeCategoryId === cat.id
|
activeCategoryId === cat.id
|
||||||
? "bg-blue-50 text-blue-600 font-medium"
|
? "bg-blue-50 text-blue-600 font-medium cursor-pointer"
|
||||||
: "text-gray-600 hover:bg-gray-50",
|
: "text-gray-600 hover:bg-gray-50 cursor-pointer",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{cat.name}
|
{cat.name}
|
||||||
@@ -124,7 +136,26 @@ export default function HelpPage() {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main className="flex-1 min-w-0">
|
<main className="flex-1 min-w-0">
|
||||||
{selectedArticle ? (
|
{activeCategoryId === 4 && !selectedArticle && (
|
||||||
|
<div className="bg-blue-50/70 border border-blue-100 rounded-xl p-3 mb-4 text-sm text-gray-600 leading-relaxed">
|
||||||
|
<p>
|
||||||
|
*请优先选择客户端连接
|
||||||
|
<Link
|
||||||
|
to="/softdownload"
|
||||||
|
className="text-blue-600 hover:underline font-medium"
|
||||||
|
>
|
||||||
|
<<<下载客户端>>>
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
<p>*无对应客户端时,可通过线路表直连支持所有设备</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-16 text-center text-gray-400">
|
||||||
|
加载中...
|
||||||
|
</div>
|
||||||
|
) : selectedArticle ? (
|
||||||
<ArticleDetail
|
<ArticleDetail
|
||||||
article={selectedArticle}
|
article={selectedArticle}
|
||||||
prev={prevArticle}
|
prev={prevArticle}
|
||||||
@@ -144,9 +175,7 @@ export default function HelpPage() {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ArticleList
|
<ArticleList
|
||||||
articles={filtered}
|
articles={articles}
|
||||||
search={search}
|
|
||||||
categoryName={activeCategory?.name}
|
|
||||||
onArticleClick={handleArticleClick}
|
onArticleClick={handleArticleClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -159,28 +188,13 @@ export default function HelpPage() {
|
|||||||
|
|
||||||
function ArticleList({
|
function ArticleList({
|
||||||
articles,
|
articles,
|
||||||
search,
|
|
||||||
categoryName,
|
|
||||||
onArticleClick,
|
onArticleClick,
|
||||||
}: {
|
}: {
|
||||||
articles: Article[]
|
articles: Article[]
|
||||||
search: string
|
|
||||||
categoryName?: string
|
|
||||||
onArticleClick: (article: Article) => void
|
onArticleClick: (article: Article) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8">
|
||||||
<h2 className="text-lg font-semibold text-gray-800 mb-6">
|
|
||||||
{search
|
|
||||||
? `搜索"${search}"的结果`
|
|
||||||
: categoryName
|
|
||||||
? categoryName
|
|
||||||
: "全部文章"}
|
|
||||||
<span className="text-sm font-normal text-gray-400 ml-2">
|
|
||||||
共 {articles.length} 篇
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{articles.length === 0 ? (
|
{articles.length === 0 ? (
|
||||||
<div className="text-center py-16 text-gray-400">
|
<div className="text-center py-16 text-gray-400">
|
||||||
<SearchIcon className="size-12 mx-auto mb-4 opacity-30" />
|
<SearchIcon className="size-12 mx-auto mb-4 opacity-30" />
|
||||||
@@ -192,9 +206,9 @@ function ArticleList({
|
|||||||
<li key={article.id}>
|
<li key={article.id}>
|
||||||
<button
|
<button
|
||||||
onClick={() => onArticleClick(article)}
|
onClick={() => onArticleClick(article)}
|
||||||
className="w-full text-left py-4 px-2 -mx-2 rounded-lg hover:bg-gray-50 transition-colors group flex items-center justify-between"
|
className="w-full text-left py-4 px-2 -mx-2 rounded-lg hover:bg-gray-50 transition-colors group flex items-center cursor-pointer justify-between "
|
||||||
>
|
>
|
||||||
<span className="text-sm text-gray-700 group-hover:text-blue-600 transition-colors pr-4">
|
<span className="text-sm text-gray-700 pr-4">
|
||||||
{article.title}
|
{article.title}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-400 whitespace-nowrap shrink-0">
|
<span className="text-xs text-gray-400 whitespace-nowrap shrink-0">
|
||||||
@@ -227,6 +241,7 @@ function ArticleDetail({
|
|||||||
categoryName?: string
|
categoryName?: string
|
||||||
}) {
|
}) {
|
||||||
const contentRef = useRef<HTMLDivElement>(null)
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
|
const videoUrl = VIDEO_TUTORIALS[article.id]
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contentRef.current) {
|
if (contentRef.current) {
|
||||||
@@ -237,23 +252,28 @@ function ArticleDetail({
|
|||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8">
|
||||||
<nav className="flex items-center gap-2 text-sm text-gray-400 mb-6">
|
<nav className="flex items-center gap-2 text-sm text-gray-400 mb-6">
|
||||||
<button
|
|
||||||
onClick={onBack}
|
|
||||||
className="hover:text-blue-600 transition-colors"
|
|
||||||
>
|
|
||||||
帮助中心
|
|
||||||
</button>
|
|
||||||
{categoryName && (
|
{categoryName && (
|
||||||
<>
|
<>
|
||||||
|
<button onClick={onBack} className="text-gray-400 cursor-pointer">
|
||||||
|
{categoryName}
|
||||||
|
</button>
|
||||||
<ChevronRightIcon className="size-3" />
|
<ChevronRightIcon className="size-3" />
|
||||||
<span>{categoryName}</span>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ChevronRightIcon className="size-3" />
|
|
||||||
<span className="text-gray-600 truncate">{article.title}</span>
|
<span className="text-gray-600 truncate">{article.title}</span>
|
||||||
</nav>
|
</nav>
|
||||||
<h1 className="text-xl font-bold text-gray-900 mb-3">{article.title}</h1>
|
{videoUrl && (
|
||||||
<p className="text-xs text-gray-400 mb-8">{article.createTime}</p>
|
<div className="text-center mb-6">
|
||||||
|
<h1 className="text-lg font-bold text-gray-800 mb-3">视频教程</h1>
|
||||||
|
{/* biome-ignore lint/a11y/useMediaCaption: 教程视频无字幕文件 */}
|
||||||
|
<video
|
||||||
|
controls
|
||||||
|
height={500}
|
||||||
|
src={videoUrl}
|
||||||
|
className="max-w-full mx-auto rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
className="prose prose-sm max-w-none text-gray-700 leading-relaxed [&_h3]:text-lg [&_h3]:font-semibold [&_h3]:text-gray-800 [&_h3]:mt-6 [&_h3]:mb-3 [&_h4]:text-base [&_h4]:font-medium [&_h4]:text-gray-800 [&_h4]:mt-4 [&_h4]:mb-2 [&_p]:mb-3 [&_pre]:bg-gray-50 [&_pre]:p-4 [&_pre]:rounded-lg [&_pre]:text-sm [&_code]:bg-gray-100 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-sm [&_strong]:text-gray-800"
|
className="prose prose-sm max-w-none text-gray-700 leading-relaxed [&_h3]:text-lg [&_h3]:font-semibold [&_h3]:text-gray-800 [&_h3]:mt-6 [&_h3]:mb-3 [&_h4]:text-base [&_h4]:font-medium [&_h4]:text-gray-800 [&_h4]:mt-4 [&_h4]:mb-2 [&_p]:mb-3 [&_pre]:bg-gray-50 [&_pre]:p-4 [&_pre]:rounded-lg [&_pre]:text-sm [&_code]:bg-gray-100 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-sm [&_strong]:text-gray-800"
|
||||||
@@ -263,7 +283,7 @@ function ArticleDetail({
|
|||||||
{prev ? (
|
{prev ? (
|
||||||
<button
|
<button
|
||||||
onClick={onPrev}
|
onClick={onPrev}
|
||||||
className="text-sm text-gray-500 hover:text-blue-600 transition-colors flex items-center gap-1 max-w-full"
|
className="text-sm text-gray-500 hover:text-blue-600 transition-colors cursor-pointer flex items-center gap-1 max-w-full"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="size-3.5 shrink-0" />
|
<ArrowLeftIcon className="size-3.5 shrink-0" />
|
||||||
<span className="truncate">上一条:{prev.title}</span>
|
<span className="truncate">上一条:{prev.title}</span>
|
||||||
@@ -276,7 +296,7 @@ function ArticleDetail({
|
|||||||
{next ? (
|
{next ? (
|
||||||
<button
|
<button
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
className="text-sm text-gray-500 hover:text-blue-600 transition-colors flex items-center gap-1 justify-end max-w-full"
|
className="text-sm text-gray-500 hover:text-blue-600 transition-colors cursor-pointer flex items-center gap-1 justify-end max-w-full"
|
||||||
>
|
>
|
||||||
<span className="truncate">下一条:{next.title}</span>
|
<span className="truncate">下一条:{next.title}</span>
|
||||||
<ArrowRightIcon className="size-3.5 shrink-0" />
|
<ArrowRightIcon className="size-3.5 shrink-0" />
|
||||||
@@ -286,15 +306,6 @@ function ArticleDetail({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<button
|
|
||||||
onClick={onBack}
|
|
||||||
className="inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon className="size-3.5" />
|
|
||||||
返回列表
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ import {
|
|||||||
fetchGameLineCount,
|
fetchGameLineCount,
|
||||||
fetchGameList,
|
fetchGameList,
|
||||||
fetchHttpBalance,
|
fetchHttpBalance,
|
||||||
type GameCity,
|
|
||||||
type GameItem,
|
|
||||||
type GameOrderData,
|
|
||||||
} from "@/api/http"
|
} from "@/api/http"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -25,6 +22,7 @@ import { Input } from "@/components/ui/input"
|
|||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||||
import Wrap from "@/components/wrap"
|
import Wrap from "@/components/wrap"
|
||||||
|
import type { GameCity, GameItem, GameOrderData } from "@/lib/models/http"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import alipay from "../_assets/alipay.svg"
|
import alipay from "../_assets/alipay.svg"
|
||||||
import wechat from "../_assets/wechat.svg"
|
import wechat from "../_assets/wechat.svg"
|
||||||
|
|||||||
@@ -13,6 +13,29 @@ export type Article = {
|
|||||||
keyword: string
|
keyword: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ArticleItem = {
|
||||||
|
TenantId: number
|
||||||
|
Title: string
|
||||||
|
CatalogId: number
|
||||||
|
SubTitle: string
|
||||||
|
Thumb: string
|
||||||
|
Keyword?: string
|
||||||
|
Content: string
|
||||||
|
AccessCount: number
|
||||||
|
Publish: number
|
||||||
|
Tag?: string
|
||||||
|
CreateTime: string
|
||||||
|
LinkType: number
|
||||||
|
LinkId: number
|
||||||
|
DeleteTag: number
|
||||||
|
Id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ArticleListResult = {
|
||||||
|
RowCount: number
|
||||||
|
List: ArticleItem[]
|
||||||
|
}
|
||||||
|
|
||||||
export const CATEGORIES: ArticleCategory[] = [
|
export const CATEGORIES: ArticleCategory[] = [
|
||||||
{ id: 5, name: "公告中心" },
|
{ id: 5, name: "公告中心" },
|
||||||
{ id: 4, name: "直连教程" },
|
{ id: 4, name: "直连教程" },
|
||||||
@@ -22,165 +45,16 @@ export const CATEGORIES: ArticleCategory[] = [
|
|||||||
{ id: 6, name: "代理合作" },
|
{ id: 6, name: "代理合作" },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const ARTICLES: Article[] = [
|
export function transformArticles(items: ArticleItem[]): Article[] {
|
||||||
{
|
return items
|
||||||
id: 1,
|
.filter(item => item.Publish === 1 && item.DeleteTag === 0)
|
||||||
title: "聚IP代理使用协议",
|
.map(item => ({
|
||||||
subTitle: "",
|
id: item.Id,
|
||||||
content:
|
title: item.Title,
|
||||||
"<h3>聚IP代理使用协议</h3><p>感谢您选择聚IP代理服务。在使用本服务前,请您仔细阅读以下条款...</p><p>1. 服务说明:聚IP提供动态和静态代理IP服务,支持HTTP/HTTPS/SOCKS5协议。</p><p>2. 使用规范:禁止用于任何违法违规行为,包括但不限于网络攻击、欺诈、侵权等。</p><p>3. 账号管理:请妥善保管账号密码,因账号泄露造成的损失由用户自行承担。</p>",
|
subTitle: item.SubTitle,
|
||||||
catalogId: 5,
|
content: item.Content,
|
||||||
createTime: "2025-06-01",
|
catalogId: item.CatalogId,
|
||||||
keyword: "使用协议,服务条款",
|
createTime: item.CreateTime.slice(0, 10),
|
||||||
},
|
keyword: item.Keyword ?? "",
|
||||||
{
|
}))
|
||||||
id: 2,
|
}
|
||||||
title: "系统升级维护通知",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
"<h3>系统升级维护通知</h3><p>尊敬的用户:</p><p>为提升服务质量,聚IP将于2025年7月15日凌晨2:00-6:00进行系统升级维护,届时部分服务可能短暂中断,请您提前做好安排。给您带来的不便敬请谅解。</p>",
|
|
||||||
catalogId: 5,
|
|
||||||
createTime: "2025-07-10",
|
|
||||||
keyword: "维护通知,系统升级",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "苹果手机直连IP设置教程",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
'<h3>苹果手机直连IP设置教程</h3><p>适用设备:iPhone / iPad(iOS 12.0及以上)</p><h4>步骤一:获取代理信息</h4><p>登录聚IP官网,购买并获取代理IP地址、端口号、账号和密码。</p><h4>步骤二:配置WiFi代理</h4><p>1. 打开"设置" → "无线局域网"<br/>2. 点击当前连接的WiFi名称旁的 (i) 图标<br/>3. 滚动到底部,点击"配置代理"<br/>4. 选择"手动"<br/>5. 输入服务器地址和端口<br/>6. 如果需要认证,开启"认证"并输入用户名和密码</p><h4>步骤三:验证连接</h4><p>打开Safari浏览器访问 ipinfo.io 查看当前IP是否已切换。</p>',
|
|
||||||
catalogId: 4,
|
|
||||||
createTime: "2025-05-20",
|
|
||||||
keyword: "苹果,iPhone,iOS,代理设置",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "安卓手机代理设置教程",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
'<h3>安卓手机代理设置教程</h3><p>适用设备:Android 8.0及以上</p><h4>步骤一:获取代理信息</h4><p>登录聚IP官网,购买并获取代理IP地址和端口号。</p><h4>步骤二:配置WiFi代理</h4><p>1. 打开"设置" → "WLAN"<br/>2. 长按当前连接的WiFi名称<br/>3. 选择"修改网络" → "高级选项"<br/>4. 代理选择"手动"<br/>5. 输入代理服务器主机名和端口<br/>6. 保存设置</p><h4>步骤三:验证</h4><p>打开浏览器访问 ipinfo.io 验证IP是否已变更。</p>',
|
|
||||||
catalogId: 4,
|
|
||||||
createTime: "2025-05-20",
|
|
||||||
keyword: "安卓,Android,代理设置",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: "Windows电脑代理设置教程",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
'<h3>Windows电脑代理设置教程</h3><p>适用系统:Windows 10 / Windows 11</p><h4>方法一:系统代理设置</h4><p>1. 打开"设置" → "网络和Internet" → "代理"<br/>2. 在"手动设置代理"中,开启"使用代理服务器"<br/>3. 输入代理IP地址和端口<br/>4. 点击"保存"</p><h4>方法二:使用客户端</h4><p>推荐下载聚IP客户端,一键切换IP,支持定时切换、API调用等高级功能。</p>',
|
|
||||||
catalogId: 4,
|
|
||||||
createTime: "2025-05-18",
|
|
||||||
keyword: "Windows,电脑,代理设置",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
title: "浏览器代理插件使用教程",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
'<h3>浏览器代理插件使用教程</h3><p>推荐使用 SwitchyOmega 插件管理代理。</p><h4>Chrome/Edge 安装步骤</h4><p>1. 打开Chrome应用商店,搜索"SwitchyOmega"<br/>2. 点击"添加至Chrome"安装<br/>3. 点击插件图标 → "选项"<br/>4. 新建情景模式,选择"代理服务器"<br/>5. 输入代理协议、IP地址和端口<br/>6. 保存并应用</p>',
|
|
||||||
catalogId: 4,
|
|
||||||
createTime: "2025-05-15",
|
|
||||||
keyword: "浏览器,插件,SwitchyOmega",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
title: "代理连接失败怎么办?",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
"<h3>代理连接失败排查指南</h3><p>如果遇到代理连接失败,请按以下步骤排查:</p><p><strong>1. 检查账号是否过期</strong><br/>登录官网查看IP账号到期时间,过期请续费。</p><p><strong>2. 检查IP白名单</strong><br/>确保当前设备的公网IP已添加到白名单中。</p><p><strong>3. 检查网络连通性</strong><br/>使用 ping 或 telnet 测试代理服务器是否可达。</p><p><strong>4. 切换节点</strong><br/>尝试更换其他线路或地区的IP节点。</p><p>如仍无法解决,请联系在线客服。</p>",
|
|
||||||
catalogId: 3,
|
|
||||||
createTime: "2025-04-25",
|
|
||||||
keyword: "连接失败,排查,故障",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
title: "IP白名单添加与修改",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
'<h3>IP白名单添加与修改</h3><p>白名单用于授权特定IP地址访问代理服务。</p><p><strong>添加白名单:</strong><br/>1. 登录聚IP官网 → 进入用户中心<br/>2. 找到对应IP账号 → 点击"白名单管理"<br/>3. 输入需要授权的IP地址 → 保存</p><p><strong>注意事项:</strong><br/>- 每次最多可添加5个白名单IP<br/>- 修改后约1-3分钟生效<br/>- 请确保填写正确的公网IP地址</p>',
|
|
||||||
catalogId: 3,
|
|
||||||
createTime: "2025-04-20",
|
|
||||||
keyword: "白名单,IP授权",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
title: "如何查询剩余流量和到期时间",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
'<h3>如何查询剩余流量和到期时间</h3><p>1. 登录聚IP官网,进入"用户中心"</p><p>2. 在"我的IP账号"列表中可以看到:<br/>- 到期时间<br/>- 剩余流量<br/>- 连接数上限<br/>- 当前在线状态</p><p>3. 即将到期时,系统会通过短信提醒,请及时续费避免服务中断。</p>',
|
|
||||||
catalogId: 3,
|
|
||||||
createTime: "2025-04-15",
|
|
||||||
keyword: "流量,到期时间,查询",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
title: "如何通过API获取代理IP",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
'<h3>API获取代理IP教程</h3><p>聚IP支持通过API接口获取代理IP,方便程序化调用。</p><h4>API地址</h4><p><code>GET https://api.juip.com/v1/proxy/ip</code></p><h4>请求参数</h4><p>- apikey: 用户API密钥(在用户中心获取)<br/>- num: 获取IP数量<br/>- type: IP类型(1=HTTP, 2=SOCKS5)<br/>- area: 地区编码</p><h4>返回示例</h4><pre>{"code":10000,"data":[{"ip":"1.2.3.4","port":8080}]}</pre>',
|
|
||||||
catalogId: 4,
|
|
||||||
createTime: "2025-05-01",
|
|
||||||
keyword: "API,接口,程序调用",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
title: "618优惠活动说明",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
"<h3>618优惠活动</h3><p>聚IP年中大促,全场代理套餐低至5折!</p><p>活动时间:6月1日 - 6月20日</p><p><strong>活动内容:</strong><br/>1. 所有动态IP套餐享受8折优惠<br/>2. 静态IP包年套餐直降200元<br/>3. 新用户注册即送3次免费测试机会<br/>4. 老用户续费额外赠送7天时长</p><p>数量有限,先到先得!</p>",
|
|
||||||
catalogId: 2,
|
|
||||||
createTime: "2025-06-01",
|
|
||||||
keyword: "618,优惠,促销",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
title: "代理合作推广计划",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
"<h3>代理合作推广计划</h3><p>聚IP诚邀各类合作伙伴加入推广计划,共享收益!</p><p><strong>合作模式:</strong><br/>1. CPS分成:每成功推荐一位付费用户,获得30%佣金<br/>2. API代理:将聚IP代理能力集成到您的产品中<br/>3. 流量合作:大批量采购享受专属折扣</p><p><strong>合作要求:</strong><br/>- 拥有网站、公众号或社群资源<br/>- 遵守相关法律法规<br/>- 不接受涉及违法违规业务的合作</p><p>有意合作请联系在线客服或发送邮件至 business@juip.com</p>",
|
|
||||||
catalogId: 6,
|
|
||||||
createTime: "2025-05-10",
|
|
||||||
keyword: "合作,代理加盟,推广",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 13,
|
|
||||||
title: "国庆特惠活动",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
"<h3>国庆特惠活动</h3><p>庆祝国庆,聚IP推出限时特惠!</p><p>活动时间:10月1日 - 10月7日</p><p><strong>活动详情:</strong><br/>1. 全场套餐9折优惠<br/>2. 充值满100减10,满200减30<br/>3. 购买年套餐额外赠送1个月</p>",
|
|
||||||
catalogId: 2,
|
|
||||||
createTime: "2025-09-28",
|
|
||||||
keyword: "国庆,特惠,活动",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 14,
|
|
||||||
title: "模拟器代理配置教程",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
'<h3>模拟器代理配置教程</h3><p>适用:雷电模拟器、夜神模拟器、MuMu模拟器等</p><h4>方法一:模拟器内WiFi代理</h4><p>1. 打开模拟器 → 进入"设置" → "WLAN"<br/>2. 长按当前WiFi → "修改网络"<br/>3. 设置代理为手动,填写IP和端口</p><h4>方法二:使用代理APP</h4><p>下载Postern或ProxyDroid等代理APP,在模拟器中安装并配置代理。</p>',
|
|
||||||
catalogId: 4,
|
|
||||||
createTime: "2025-05-22",
|
|
||||||
keyword: "模拟器,代理,雷电,夜神",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 15,
|
|
||||||
title: "软路由代理配置教程",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
"<h3>软路由代理配置教程</h3><p>适用:OpenWrt / LEDE / iKuai 等软路由系统</p><h4>OpenWrt配置步骤</h4><p>1. SSH登录路由器<br/>2. 安装代理客户端软件<br/>3. 配置代理服务器地址和认证信息<br/>4. 设置流量转发规则<br/>5. 重启网络服务生效</p><p>详细配置脚本请联系技术支持获取。</p>",
|
|
||||||
catalogId: 4,
|
|
||||||
createTime: "2025-05-25",
|
|
||||||
keyword: "软路由,OpenWrt,代理配置",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 16,
|
|
||||||
title: "如何联系客服",
|
|
||||||
subTitle: "",
|
|
||||||
content:
|
|
||||||
"<h3>如何联系客服</h3><p>我们提供多种客服渠道:</p><p><strong>在线客服:</strong><br/>工作时间:8:00 - 23:30<br/>点击官网右下角客服图标即可在线咨询。</p><p><strong>QQ客服:</strong><br/>客服QQ:123456789</p><p><strong>邮件支持:</strong><br/>support@juip.com</p><p><strong>常见问题:</strong><br/>建议先查看帮助中心,大部分问题已有解答。</p>",
|
|
||||||
catalogId: 1,
|
|
||||||
createTime: "2025-04-01",
|
|
||||||
keyword: "客服,联系,帮助",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|||||||
36
src/lib/models/http.tsx
Normal file
36
src/lib/models/http.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export type GameOrderData = {
|
||||||
|
isAbroad: number
|
||||||
|
isRelayed: number
|
||||||
|
shareType: number
|
||||||
|
gameId: number
|
||||||
|
lineType: number
|
||||||
|
bandwidth: number
|
||||||
|
cityCode: number
|
||||||
|
isp: number
|
||||||
|
ipAmount: number
|
||||||
|
periodType: number
|
||||||
|
periodAmount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GameCity = {
|
||||||
|
code: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GameItem = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HttpOrderInfo = {
|
||||||
|
order_type: number
|
||||||
|
money: number
|
||||||
|
pay_type: number
|
||||||
|
data: GameOrderData
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateOrderResult = {
|
||||||
|
code: number
|
||||||
|
msg?: string
|
||||||
|
data?: string
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": "http://192.168.3.6:5000",
|
"/api": "http://192.168.3.6:5000",
|
||||||
|
"/article": "http://192.168.3.6:5000",
|
||||||
"/user": "http://192.168.3.6:5000",
|
"/user": "http://192.168.3.6:5000",
|
||||||
"/product": {
|
"/product": {
|
||||||
target: "http://192.168.3.6:5000",
|
target: "http://192.168.3.6:5000",
|
||||||
|
|||||||
Reference in New Issue
Block a user