完善教程&帮助中心页面

This commit is contained in:
Eamon
2026-08-24 15:23:23 +08:00
parent 85f48004e1
commit 9bf97cab89
7 changed files with 210 additions and 289 deletions

31
src/api/article.ts Normal file
View 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) }
}

View File

@@ -1,47 +1,17 @@
import type { ApiResponse } from "@/lib/api"
import type {
CreateOrderResult,
GameCity,
GameItem,
GameOrderData,
HttpOrderInfo,
} from "@/lib/models/http"
export const PHP_API_BASE_URL =
import.meta.env.VITE_PHP_API_BASE_URL ?? "https://php-api.juip.com"
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>
async function postJson<T = RawHttpResponse>(

View File

@@ -4,44 +4,69 @@ import {
ChevronRightIcon,
SearchIcon,
} 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 { ARTICLES, type Article, CATEGORIES } from "@/lib/models/article"
import {
type Article,
CATEGORIES,
transformArticles,
} from "@/lib/models/article"
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() {
const [search, setSearch] = useState("")
const [keyword, setKeyword] = useState("")
const [activeCategoryId, setActiveCategoryId] = useState<number | null>(null)
const [selectedArticle, setSelectedArticle] = useState<Article | null>(null)
const [articles, setArticles] = useState<Article[]>([])
const [loading, setLoading] = useState(true)
const filtered = useMemo(() => {
let list = ARTICLES
if (activeCategoryId !== null) {
list = list.filter(a => a.catalogId === activeCategoryId)
useEffect(() => {
let cancelled = false
setLoading(true)
const load = async () => {
const res = await fetchArticles({
catalog: activeCategoryId ?? undefined,
keyword,
})
if (cancelled) return
setLoading(false)
if (!res.success) {
setArticles([])
toast.error(res.message)
return
}
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),
)
setArticles(transformArticles(res.data.List ?? []))
}
void load()
return () => {
cancelled = true
}
}, [activeCategoryId, keyword])
return list
}, [search, activeCategoryId])
const handleSearch = () => {
setKeyword(search.trim())
setSelectedArticle(null)
}
const currentIndex = selectedArticle
? filtered.findIndex(a => a.id === selectedArticle.id)
? articles.findIndex(a => a.id === selectedArticle.id)
: -1
const prevArticle = currentIndex > 0 ? filtered[currentIndex - 1] : null
const prevArticle = currentIndex > 0 ? articles[currentIndex - 1] : null
const nextArticle =
currentIndex >= 0 && currentIndex < filtered.length - 1
? filtered[currentIndex + 1]
currentIndex >= 0 && currentIndex < articles.length - 1
? articles[currentIndex + 1]
: null
const handleArticleClick = (article: Article) => {
@@ -56,6 +81,7 @@ export default function HelpPage() {
return (
<div className="min-h-[calc(100vh-5rem)] bg-gray-50">
<Toaster position="top-center" richColors />
<Wrap className="pt-28 pb-16">
<div className="max-w-2xl mx-auto mb-10">
<div className="relative">
@@ -69,8 +95,14 @@ export default function HelpPage() {
setSearch(e.target.value)
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>
</div>
@@ -79,27 +111,7 @@ export default function HelpPage() {
<div className="flex gap-8">
<aside className="w-52 shrink-0">
<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>
<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 => (
<li key={cat.id}>
<button
@@ -110,8 +122,8 @@ export default function HelpPage() {
className={cn(
"w-full text-left px-5 py-3 text-sm transition-colors flex items-center justify-between",
activeCategoryId === cat.id
? "bg-blue-50 text-blue-600 font-medium"
: "text-gray-600 hover:bg-gray-50",
? "bg-blue-50 text-blue-600 font-medium cursor-pointer"
: "text-gray-600 hover:bg-gray-50 cursor-pointer",
)}
>
{cat.name}
@@ -124,7 +136,26 @@ export default function HelpPage() {
</aside>
<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"
>
&lt;&lt;&lt;&gt;&gt;&gt;
</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
article={selectedArticle}
prev={prevArticle}
@@ -144,9 +175,7 @@ export default function HelpPage() {
/>
) : (
<ArticleList
articles={filtered}
search={search}
categoryName={activeCategory?.name}
articles={articles}
onArticleClick={handleArticleClick}
/>
)}
@@ -159,28 +188,13 @@ export default function HelpPage() {
function ArticleList({
articles,
search,
categoryName,
onArticleClick,
}: {
articles: Article[]
search: string
categoryName?: string
onArticleClick: (article: Article) => void
}) {
return (
<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 ? (
<div className="text-center py-16 text-gray-400">
<SearchIcon className="size-12 mx-auto mb-4 opacity-30" />
@@ -192,9 +206,9 @@ function ArticleList({
<li key={article.id}>
<button
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}
</span>
<span className="text-xs text-gray-400 whitespace-nowrap shrink-0">
@@ -227,6 +241,7 @@ function ArticleDetail({
categoryName?: string
}) {
const contentRef = useRef<HTMLDivElement>(null)
const videoUrl = VIDEO_TUTORIALS[article.id]
useEffect(() => {
if (contentRef.current) {
@@ -237,23 +252,28 @@ function ArticleDetail({
return (
<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">
<button
onClick={onBack}
className="hover:text-blue-600 transition-colors"
>
</button>
{categoryName && (
<>
<button onClick={onBack} className="text-gray-400 cursor-pointer">
{categoryName}
</button>
<ChevronRightIcon className="size-3" />
<span>{categoryName}</span>
</>
)}
<ChevronRightIcon className="size-3" />
<span className="text-gray-600 truncate">{article.title}</span>
</nav>
<h1 className="text-xl font-bold text-gray-900 mb-3">{article.title}</h1>
<p className="text-xs text-gray-400 mb-8">{article.createTime}</p>
{videoUrl && (
<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
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"
@@ -263,7 +283,7 @@ function ArticleDetail({
{prev ? (
<button
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" />
<span className="truncate">{prev.title}</span>
@@ -276,7 +296,7 @@ function ArticleDetail({
{next ? (
<button
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>
<ArrowRightIcon className="size-3.5 shrink-0" />
@@ -286,15 +306,6 @@ function ArticleDetail({
)}
</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>
)
}

View File

@@ -7,9 +7,6 @@ import {
fetchGameLineCount,
fetchGameList,
fetchHttpBalance,
type GameCity,
type GameItem,
type GameOrderData,
} from "@/api/http"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
@@ -25,6 +22,7 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import Wrap from "@/components/wrap"
import type { GameCity, GameItem, GameOrderData } from "@/lib/models/http"
import { cn } from "@/lib/utils"
import alipay from "../_assets/alipay.svg"
import wechat from "../_assets/wechat.svg"

View File

@@ -13,6 +13,29 @@ export type Article = {
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[] = [
{ id: 5, name: "公告中心" },
{ id: 4, name: "直连教程" },
@@ -22,165 +45,16 @@ export const CATEGORIES: ArticleCategory[] = [
{ id: 6, name: "代理合作" },
]
export const ARTICLES: Article[] = [
{
id: 1,
title: "聚IP代理使用协议",
subTitle: "",
content:
"<h3>聚IP代理使用协议</h3><p>感谢您选择聚IP代理服务。在使用本服务前请您仔细阅读以下条款...</p><p>1. 服务说明聚IP提供动态和静态代理IP服务支持HTTP/HTTPS/SOCKS5协议。</p><p>2. 使用规范:禁止用于任何违法违规行为,包括但不限于网络攻击、欺诈、侵权等。</p><p>3. 账号管理:请妥善保管账号密码,因账号泄露造成的损失由用户自行承担。</p>",
catalogId: 5,
createTime: "2025-06-01",
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 / iPadiOS 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/>客服QQ123456789</p><p><strong>邮件支持:</strong><br/>support@juip.com</p><p><strong>常见问题:</strong><br/>建议先查看帮助中心,大部分问题已有解答。</p>",
catalogId: 1,
createTime: "2025-04-01",
keyword: "客服,联系,帮助",
},
]
export function transformArticles(items: ArticleItem[]): Article[] {
return items
.filter(item => item.Publish === 1 && item.DeleteTag === 0)
.map(item => ({
id: item.Id,
title: item.Title,
subTitle: item.SubTitle,
content: item.Content,
catalogId: item.CatalogId,
createTime: item.CreateTime.slice(0, 10),
keyword: item.Keyword ?? "",
}))
}

36
src/lib/models/http.tsx Normal file
View 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
}

View File

@@ -14,6 +14,7 @@ export default defineConfig({
server: {
proxy: {
"/api": "http://192.168.3.6:5000",
"/article": "http://192.168.3.6:5000",
"/user": "http://192.168.3.6:5000",
"/product": {
target: "http://192.168.3.6:5000",