diff --git a/src/api/article.ts b/src/api/article.ts new file mode 100644 index 0000000..f585429 --- /dev/null +++ b/src/api/article.ts @@ -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> { + 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( + `/article/apiindex${query ? `?${query}` : ""}`, + undefined, + "GET", + ) + if (!res.success) return res + return { success: true, data: normalizeListResult(res.data) } +} diff --git a/src/api/http.ts b/src/api/http.ts index f45279f..52c1542 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -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 async function postJson( diff --git a/src/home/help/index.tsx b/src/home/help/index.tsx index 21bc5ba..4e5ab51 100644 --- a/src/home/help/index.tsx +++ b/src/home/help/index.tsx @@ -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 = { + 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(null) const [selectedArticle, setSelectedArticle] = useState
(null) + const [articles, setArticles] = useState([]) + 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 + } + 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 - }, [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 (
+
@@ -69,8 +95,14 @@ export default function HelpPage() { setSearch(e.target.value) setSelectedArticle(null) }} + onKeyDown={e => { + if (e.key === "Enter") handleSearch() + }} /> -
@@ -79,27 +111,7 @@ export default function HelpPage() {
- {selectedArticle ? ( + {activeCategoryId === 4 && !selectedArticle && ( +
+

+ *请优先选择客户端连接 + + <<<下载客户端>>> + +

+

*无对应客户端时,可通过线路表直连支持所有设备

+
+ )} + + {loading ? ( +
+ 加载中... +
+ ) : selectedArticle ? ( ) : ( )} @@ -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 (
-

- {search - ? `搜索"${search}"的结果` - : categoryName - ? categoryName - : "全部文章"} - - 共 {articles.length} 篇 - -

- {articles.length === 0 ? (
@@ -192,9 +206,9 @@ function ArticleList({
  • {categoryName && ( <> + - {categoryName} )} - {article.title} -

    {article.title}

    -

    {article.createTime}

    + {videoUrl && ( +
    +

    视频教程

    + {/* biome-ignore lint/a11y/useMediaCaption: 教程视频无字幕文件 */} +
    + )}
    上一条:{prev.title} @@ -276,7 +296,7 @@ function ArticleDetail({ {next ? (
  • -
    - -
    ) } diff --git a/src/home/product/http.tsx b/src/home/product/http.tsx index 095007c..c5084aa 100644 --- a/src/home/product/http.tsx +++ b/src/home/product/http.tsx @@ -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" diff --git a/src/lib/models/article.ts b/src/lib/models/article.ts index 985d266..39fe078 100644 --- a/src/lib/models/article.ts +++ b/src/lib/models/article.ts @@ -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: - "

    聚IP代理使用协议

    感谢您选择聚IP代理服务。在使用本服务前,请您仔细阅读以下条款...

    1. 服务说明:聚IP提供动态和静态代理IP服务,支持HTTP/HTTPS/SOCKS5协议。

    2. 使用规范:禁止用于任何违法违规行为,包括但不限于网络攻击、欺诈、侵权等。

    3. 账号管理:请妥善保管账号密码,因账号泄露造成的损失由用户自行承担。

    ", - catalogId: 5, - createTime: "2025-06-01", - keyword: "使用协议,服务条款", - }, - { - id: 2, - title: "系统升级维护通知", - subTitle: "", - content: - "

    系统升级维护通知

    尊敬的用户:

    为提升服务质量,聚IP将于2025年7月15日凌晨2:00-6:00进行系统升级维护,届时部分服务可能短暂中断,请您提前做好安排。给您带来的不便敬请谅解。

    ", - catalogId: 5, - createTime: "2025-07-10", - keyword: "维护通知,系统升级", - }, - { - id: 3, - title: "苹果手机直连IP设置教程", - subTitle: "", - content: - '

    苹果手机直连IP设置教程

    适用设备:iPhone / iPad(iOS 12.0及以上)

    步骤一:获取代理信息

    登录聚IP官网,购买并获取代理IP地址、端口号、账号和密码。

    步骤二:配置WiFi代理

    1. 打开"设置" → "无线局域网"
    2. 点击当前连接的WiFi名称旁的 (i) 图标
    3. 滚动到底部,点击"配置代理"
    4. 选择"手动"
    5. 输入服务器地址和端口
    6. 如果需要认证,开启"认证"并输入用户名和密码

    步骤三:验证连接

    打开Safari浏览器访问 ipinfo.io 查看当前IP是否已切换。

    ', - catalogId: 4, - createTime: "2025-05-20", - keyword: "苹果,iPhone,iOS,代理设置", - }, - { - id: 4, - title: "安卓手机代理设置教程", - subTitle: "", - content: - '

    安卓手机代理设置教程

    适用设备:Android 8.0及以上

    步骤一:获取代理信息

    登录聚IP官网,购买并获取代理IP地址和端口号。

    步骤二:配置WiFi代理

    1. 打开"设置" → "WLAN"
    2. 长按当前连接的WiFi名称
    3. 选择"修改网络" → "高级选项"
    4. 代理选择"手动"
    5. 输入代理服务器主机名和端口
    6. 保存设置

    步骤三:验证

    打开浏览器访问 ipinfo.io 验证IP是否已变更。

    ', - catalogId: 4, - createTime: "2025-05-20", - keyword: "安卓,Android,代理设置", - }, - { - id: 5, - title: "Windows电脑代理设置教程", - subTitle: "", - content: - '

    Windows电脑代理设置教程

    适用系统:Windows 10 / Windows 11

    方法一:系统代理设置

    1. 打开"设置" → "网络和Internet" → "代理"
    2. 在"手动设置代理"中,开启"使用代理服务器"
    3. 输入代理IP地址和端口
    4. 点击"保存"

    方法二:使用客户端

    推荐下载聚IP客户端,一键切换IP,支持定时切换、API调用等高级功能。

    ', - catalogId: 4, - createTime: "2025-05-18", - keyword: "Windows,电脑,代理设置", - }, - { - id: 6, - title: "浏览器代理插件使用教程", - subTitle: "", - content: - '

    浏览器代理插件使用教程

    推荐使用 SwitchyOmega 插件管理代理。

    Chrome/Edge 安装步骤

    1. 打开Chrome应用商店,搜索"SwitchyOmega"
    2. 点击"添加至Chrome"安装
    3. 点击插件图标 → "选项"
    4. 新建情景模式,选择"代理服务器"
    5. 输入代理协议、IP地址和端口
    6. 保存并应用

    ', - catalogId: 4, - createTime: "2025-05-15", - keyword: "浏览器,插件,SwitchyOmega", - }, - { - id: 7, - title: "代理连接失败怎么办?", - subTitle: "", - content: - "

    代理连接失败排查指南

    如果遇到代理连接失败,请按以下步骤排查:

    1. 检查账号是否过期
    登录官网查看IP账号到期时间,过期请续费。

    2. 检查IP白名单
    确保当前设备的公网IP已添加到白名单中。

    3. 检查网络连通性
    使用 ping 或 telnet 测试代理服务器是否可达。

    4. 切换节点
    尝试更换其他线路或地区的IP节点。

    如仍无法解决,请联系在线客服。

    ", - catalogId: 3, - createTime: "2025-04-25", - keyword: "连接失败,排查,故障", - }, - { - id: 8, - title: "IP白名单添加与修改", - subTitle: "", - content: - '

    IP白名单添加与修改

    白名单用于授权特定IP地址访问代理服务。

    添加白名单:
    1. 登录聚IP官网 → 进入用户中心
    2. 找到对应IP账号 → 点击"白名单管理"
    3. 输入需要授权的IP地址 → 保存

    注意事项:
    - 每次最多可添加5个白名单IP
    - 修改后约1-3分钟生效
    - 请确保填写正确的公网IP地址

    ', - catalogId: 3, - createTime: "2025-04-20", - keyword: "白名单,IP授权", - }, - { - id: 9, - title: "如何查询剩余流量和到期时间", - subTitle: "", - content: - '

    如何查询剩余流量和到期时间

    1. 登录聚IP官网,进入"用户中心"

    2. 在"我的IP账号"列表中可以看到:
    - 到期时间
    - 剩余流量
    - 连接数上限
    - 当前在线状态

    3. 即将到期时,系统会通过短信提醒,请及时续费避免服务中断。

    ', - catalogId: 3, - createTime: "2025-04-15", - keyword: "流量,到期时间,查询", - }, - { - id: 10, - title: "如何通过API获取代理IP", - subTitle: "", - content: - '

    API获取代理IP教程

    聚IP支持通过API接口获取代理IP,方便程序化调用。

    API地址

    GET https://api.juip.com/v1/proxy/ip

    请求参数

    - apikey: 用户API密钥(在用户中心获取)
    - num: 获取IP数量
    - type: IP类型(1=HTTP, 2=SOCKS5)
    - area: 地区编码

    返回示例

    {"code":10000,"data":[{"ip":"1.2.3.4","port":8080}]}
    ', - catalogId: 4, - createTime: "2025-05-01", - keyword: "API,接口,程序调用", - }, - { - id: 11, - title: "618优惠活动说明", - subTitle: "", - content: - "

    618优惠活动

    聚IP年中大促,全场代理套餐低至5折!

    活动时间:6月1日 - 6月20日

    活动内容:
    1. 所有动态IP套餐享受8折优惠
    2. 静态IP包年套餐直降200元
    3. 新用户注册即送3次免费测试机会
    4. 老用户续费额外赠送7天时长

    数量有限,先到先得!

    ", - catalogId: 2, - createTime: "2025-06-01", - keyword: "618,优惠,促销", - }, - { - id: 12, - title: "代理合作推广计划", - subTitle: "", - content: - "

    代理合作推广计划

    聚IP诚邀各类合作伙伴加入推广计划,共享收益!

    合作模式:
    1. CPS分成:每成功推荐一位付费用户,获得30%佣金
    2. API代理:将聚IP代理能力集成到您的产品中
    3. 流量合作:大批量采购享受专属折扣

    合作要求:
    - 拥有网站、公众号或社群资源
    - 遵守相关法律法规
    - 不接受涉及违法违规业务的合作

    有意合作请联系在线客服或发送邮件至 business@juip.com

    ", - catalogId: 6, - createTime: "2025-05-10", - keyword: "合作,代理加盟,推广", - }, - { - id: 13, - title: "国庆特惠活动", - subTitle: "", - content: - "

    国庆特惠活动

    庆祝国庆,聚IP推出限时特惠!

    活动时间:10月1日 - 10月7日

    活动详情:
    1. 全场套餐9折优惠
    2. 充值满100减10,满200减30
    3. 购买年套餐额外赠送1个月

    ", - catalogId: 2, - createTime: "2025-09-28", - keyword: "国庆,特惠,活动", - }, - { - id: 14, - title: "模拟器代理配置教程", - subTitle: "", - content: - '

    模拟器代理配置教程

    适用:雷电模拟器、夜神模拟器、MuMu模拟器等

    方法一:模拟器内WiFi代理

    1. 打开模拟器 → 进入"设置" → "WLAN"
    2. 长按当前WiFi → "修改网络"
    3. 设置代理为手动,填写IP和端口

    方法二:使用代理APP

    下载Postern或ProxyDroid等代理APP,在模拟器中安装并配置代理。

    ', - catalogId: 4, - createTime: "2025-05-22", - keyword: "模拟器,代理,雷电,夜神", - }, - { - id: 15, - title: "软路由代理配置教程", - subTitle: "", - content: - "

    软路由代理配置教程

    适用:OpenWrt / LEDE / iKuai 等软路由系统

    OpenWrt配置步骤

    1. SSH登录路由器
    2. 安装代理客户端软件
    3. 配置代理服务器地址和认证信息
    4. 设置流量转发规则
    5. 重启网络服务生效

    详细配置脚本请联系技术支持获取。

    ", - catalogId: 4, - createTime: "2025-05-25", - keyword: "软路由,OpenWrt,代理配置", - }, - { - id: 16, - title: "如何联系客服", - subTitle: "", - content: - "

    如何联系客服

    我们提供多种客服渠道:

    在线客服:
    工作时间:8:00 - 23:30
    点击官网右下角客服图标即可在线咨询。

    QQ客服:
    客服QQ:123456789

    邮件支持:
    support@juip.com

    常见问题:
    建议先查看帮助中心,大部分问题已有解答。

    ", - 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 ?? "", + })) +} diff --git a/src/lib/models/http.tsx b/src/lib/models/http.tsx new file mode 100644 index 0000000..e0105c8 --- /dev/null +++ b/src/lib/models/http.tsx @@ -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 +} diff --git a/vite.config.ts b/vite.config.ts index e13eb8b..07409ad 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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",