From fc034dc971c38275ba1d2510cab17afdcc92059b Mon Sep 17 00:00:00 2001 From: Eamon <17516219072@163.com> Date: Tue, 18 Aug 2026 13:33:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8A=A8=E9=9D=99=E6=80=81ip?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E8=B7=B3=E8=BD=AC=E9=97=AE=E9=A2=98=20&=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0admin=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 + bun.lock | 3 + package.json | 1 + src/admin/clients.tsx | 14 +- src/admin/dashboard/page.tsx | 53 +-- src/admin/funds/page.tsx | 26 +- src/admin/httpRecharge/page.tsx | 32 ++ src/admin/orders/mock.ts | 401 ++++++++++++++++++ src/admin/orders/page.tsx | 290 +++++++++++++ src/admin/product/long.tsx | 3 - src/admin/product/short.tsx | 3 - src/admin/record/mock.ts | 40 ++ src/admin/record/page.tsx | 136 ++++++ src/admin/refundOrders/mock.ts | 401 ++++++++++++++++++ src/admin/refundOrders/page.tsx | 272 ++++++++++++ src/admin/resources/long.tsx | 5 + src/admin/resources/short.tsx | 5 + src/admin/rosorder/page.tsx | 5 + src/api/linedata.ts | 56 ++- src/api/product.ts | 20 + .../composites/dialogs/editInfoDialog.tsx | 210 +++++---- src/components/data-table.tsx | 20 +- src/components/productDropdown.tsx | 4 +- src/components/ui/table.tsx | 8 +- src/home/ipLine/index.tsx | 376 ++++++++-------- src/home/login/index.tsx | 393 ++++++++++------- src/home/product/index.tsx | 77 +++- src/home/product/productTabs.tsx | 4 +- src/home/product/routeros.tsx | 14 +- src/home/softDownload/index.tsx | 2 +- src/lib/models/order.ts | 67 +++ src/lib/models/product.ts | 48 ++- src/lib/models/record.ts | 30 ++ src/routers/index.tsx | 18 +- vite.config.ts | 2 +- 35 files changed, 2516 insertions(+), 524 deletions(-) create mode 100644 src/admin/httpRecharge/page.tsx create mode 100644 src/admin/orders/mock.ts create mode 100644 src/admin/orders/page.tsx delete mode 100644 src/admin/product/long.tsx delete mode 100644 src/admin/product/short.tsx create mode 100644 src/admin/record/mock.ts create mode 100644 src/admin/record/page.tsx create mode 100644 src/admin/refundOrders/mock.ts create mode 100644 src/admin/refundOrders/page.tsx create mode 100644 src/admin/resources/long.tsx create mode 100644 src/admin/resources/short.tsx create mode 100644 src/admin/rosorder/page.tsx create mode 100644 src/lib/models/order.ts create mode 100644 src/lib/models/record.ts diff --git a/.env b/.env index 1ac56d4..3f21b3b 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ VITE_API_BASE_URL= +VITE_PHP_API_BASE_URL=https://php-api.juip.com VITE_CLIENT_ID=web VITE_CLIENT_SECRET=web \ No newline at end of file diff --git a/bun.lock b/bun.lock index 65be6f9..c395492 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.4.0", "lucide-react": "^1.17.0", "next-themes": "^0.4.6", "radix-ui": "^1.5.0", @@ -547,6 +548,8 @@ "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "date-fns": ["date-fns@4.4.0", "https://registry.npmmirror.com/date-fns/-/date-fns-4.4.0.tgz", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], + "debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "https://registry.npmmirror.com/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], diff --git a/package.json b/package.json index 490e9b7..3f0757b 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.4.0", "lucide-react": "^1.17.0", "next-themes": "^0.4.6", "radix-ui": "^1.5.0", diff --git a/src/admin/clients.tsx b/src/admin/clients.tsx index 29838fd..0d54c36 100644 --- a/src/admin/clients.tsx +++ b/src/admin/clients.tsx @@ -156,13 +156,13 @@ export function Navbar() { expand={navbar} /> } label="订单管理" expand={navbar} /> } label="退货管理" expand={navbar} @@ -180,31 +180,31 @@ export function Navbar() { expand={navbar} /> } label="充值记录" expand={navbar} /> } label="短效套餐" expand={navbar} /> } label="长效管理" expand={navbar} /> } label="软路由" expand={navbar} /> } label="使用记录" expand={navbar} diff --git a/src/admin/dashboard/page.tsx b/src/admin/dashboard/page.tsx index 3f589d4..6af13a1 100644 --- a/src/admin/dashboard/page.tsx +++ b/src/admin/dashboard/page.tsx @@ -1,5 +1,5 @@ import { useState } from "react" -import { EditInfoDialog } from "@/components/composites/dialogs/editInfoDialog" +import { EditInfoDialog, type EditInfoValues } from "@/components/composites/dialogs/editInfoDialog" import DataTable from "@/components/data-table" import Page from "@/components/page" import { Button } from "@/components/ui/button" @@ -90,7 +90,7 @@ export default function DashboardPage() { yearConsumption: 2365, } const [editDialogOpen, setEditDialogOpen] = useState(false) - const handleSaveInfo = (data: any) => { + const handleSaveInfo = (data: EditInfoValues) => { console.log("保存的信息:", data) // 这里调 API 保存数据 } @@ -300,11 +300,14 @@ export default function DashboardPage() { {/* 账号信息 */} -
+

账号信息

-
-
- {accountInfo.phone} +
+ {/* 头像/二维码区域 */} +
+ + {accountInfo.phone} + -
+
账号二维码
-
-
- 密码 - + {/* 信息列表 */} +
+
+ 密码 + ********
-
- QQ - +
+ QQ + {accountInfo.qq || "未绑定"}
-
- 微信号 - +
+ 微信号 + {accountInfo.wechat || "未绑定"}
-
- 淘宝会员 - +
+ 淘宝 + {accountInfo.taobao || "未绑定"}
-
- 邮箱 +
+ 邮箱 {accountInfo.email || "未绑定"}
-
+ +
+
+
+ +
+

订单管理

+ { + setPageSize(size) + setPage(1) + }, + }} + /> +
+ + ) +} diff --git a/src/admin/product/long.tsx b/src/admin/product/long.tsx deleted file mode 100644 index 2c9ffb8..0000000 --- a/src/admin/product/long.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function LongPage() { - return
LongPage
-} diff --git a/src/admin/product/short.tsx b/src/admin/product/short.tsx deleted file mode 100644 index 6bceb7a..0000000 --- a/src/admin/product/short.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function ShortPage() { - return
ShortPage
-} diff --git a/src/admin/record/mock.ts b/src/admin/record/mock.ts new file mode 100644 index 0000000..9c2e754 --- /dev/null +++ b/src/admin/record/mock.ts @@ -0,0 +1,40 @@ +import type { HttpUsedLogItem, HttpUsedSummary } from "@/lib/models/record" + +export const MOCK_USED_SUMMARY: HttpUsedSummary = { + todayShortPackIpUsed: 3, + shortPackIpUsed: 45, +} + +const PACK_IP_PREFIX = [ + { packType: 21, prefix: "211.90.13" }, + { packType: 22, prefix: "112.65.28" }, +] + +function pad(value: number): string { + return String(value).padStart(2, "0") +} + +function buildLogs(count: number): HttpUsedLogItem[] { + const now = new Date() + const logs: HttpUsedLogItem[] = [] + + for (let i = 0; i < count; i += 1) { + const source = PACK_IP_PREFIX[i % PACK_IP_PREFIX.length] + const time = new Date(now.getTime() - i * 37 * 60 * 1000) + const createTime = + `${time.getFullYear()}-${pad(time.getMonth() + 1)}-${pad(time.getDate())} ` + + `${pad(time.getHours())}:${pad(time.getMinutes())}:${pad(time.getSeconds())}` + + logs.push({ + packType: source.packType, + userIp: `113.118.${(i % 200) + 10}.${(i % 250) + 3}`, + ip: `${source.prefix}.${(i % 240) + 10}`, + port: 6000 + (i % 300), + createTime, + }) + } + + return logs +} + +export const MOCK_USED_LOGS = buildLogs(56) diff --git a/src/admin/record/page.tsx b/src/admin/record/page.tsx new file mode 100644 index 0000000..c7316f4 --- /dev/null +++ b/src/admin/record/page.tsx @@ -0,0 +1,136 @@ +import type { ColumnDef } from "@tanstack/react-table" +import { useMemo, useState } from "react" +import DataTable from "@/components/data-table" +import Page from "@/components/page" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + formatPackType, + HTTP_USE_PACK_TYPE_OPTIONS, + type HttpUsedLogItem, +} from "@/lib/models/record" +import { MOCK_USED_LOGS, MOCK_USED_SUMMARY } from "./mock" + +const columns: ColumnDef[] = [ + { + accessorKey: "packType", + header: "使用类型", + cell: ({ row }) => formatPackType(row.original.packType), + }, + { + accessorKey: "userIp", + header: "客户端IP", + }, + { + accessorKey: "ip", + header: "使用IP", + }, + { + accessorKey: "port", + header: "端口号", + }, + { + accessorKey: "createTime", + header: "使用时间", + }, +] + +function StatCard(props: { + label: string + value: number + accent: "amber" | "blue" +}) { + return ( +
+ {props.label} + {props.value} 个 +
+ ) +} + +export default function RecordPage() { + const [packType, setPackType] = useState(HTTP_USE_PACK_TYPE_OPTIONS[0]) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + + const filtered = useMemo( + () => MOCK_USED_LOGS.filter(log => log.packType === packType), + [packType], + ) + + const paged = useMemo( + () => filtered.slice((page - 1) * pageSize, page * pageSize), + [filtered, page, pageSize], + ) + + const handlePackTypeChange = (value: string) => { + setPackType(Number(value)) + setPage(1) + } + + return ( + +
+

包天包量使用概况

+
+ + +
+
+ +
+
+

+ 明细 最多允许查看72小时内记录(短效无限量套餐暂无记录) +

+ + +
+ + { + setPageSize(size) + setPage(1) + }, + }} + /> +
+
+ ) +} diff --git a/src/admin/refundOrders/mock.ts b/src/admin/refundOrders/mock.ts new file mode 100644 index 0000000..7068ded --- /dev/null +++ b/src/admin/refundOrders/mock.ts @@ -0,0 +1,401 @@ +import type { OrderItem } from "@/lib/models/order" + +export const MOCK_ORDERS: OrderItem[] = [ + { + Id: 23, + OrderNo: "JU202508150001", + OrderType: 1, + ProductId: 1, + ProductName: "动静态IP", + PackageName: "天卡", + DayPrice: 3, + ConnectCount: 1, + AccountCount: 1, + Accounts: "jx1234", + OrderAmount: 3, + CouponAmount: 0, + PaymentAmount: 3, + CreateTime: "2025-08-15 10:23:45", + UpdateTime: "2025-08-15 10:24:01", + }, + { + Id: 22, + OrderNo: "JU202508150002", + OrderType: 1, + ProductId: 1, + ProductName: "动静态IP", + PackageName: "周卡", + DayPrice: 18, + ConnectCount: 1, + AccountCount: 1, + Accounts: "jx1235", + OrderAmount: 18, + CouponAmount: 2, + PaymentAmount: 16, + CreateTime: "2025-08-15 09:12:33", + UpdateTime: "2025-08-15 09:12:50", + }, + { + Id: 21, + OrderNo: "JU202508140003", + OrderType: 2, + ProductId: 1, + ProductName: "动静态IP", + PackageName: "月卡", + DayPrice: 68, + ConnectCount: 2, + AccountCount: 1, + Accounts: "jx1236", + OrderAmount: 68, + CouponAmount: 0, + PaymentAmount: 68, + CreateTime: "2025-08-14 16:45:21", + UpdateTime: "2025-08-14 16:45:39", + }, + { + Id: 20, + OrderNo: "JU202508140004", + OrderType: 3, + ProductId: 2, + ProductName: "新HTTP/S5", + PackageName: "月卡(活动)", + DayPrice: 88, + ConnectCount: 5, + AccountCount: 3, + Accounts: "https-001,https-002,https-003", + OrderAmount: 264, + CouponAmount: 10, + PaymentAmount: 254, + CreateTime: "2025-08-14 14:02:17", + UpdateTime: "2025-08-14 14:02:55", + }, + { + Id: 19, + OrderNo: "JU202508140005", + OrderType: 1, + ProductId: 3, + ProductName: "HTTP/S5", + PackageName: "季卡", + DayPrice: 168, + ConnectCount: 1, + AccountCount: 1, + Accounts: "global-001", + OrderAmount: 168, + CouponAmount: 0, + PaymentAmount: 168, + CreateTime: "2025-08-14 11:30:08", + UpdateTime: "2025-08-14 11:30:26", + }, + { + Id: 18, + OrderNo: "JU202508130006", + OrderType: 4, + ProductId: 1, + ProductName: "动静态IP", + PackageName: "年卡", + DayPrice: 688, + ConnectCount: 10, + AccountCount: 5, + Accounts: "batch-jx-01,batch-jx-02,batch-jx-03,batch-jx-04,batch-jx-05", + OrderAmount: 3440, + CouponAmount: 100, + PaymentAmount: 3340, + CreateTime: "2025-08-13 20:15:44", + UpdateTime: "2025-08-13 20:16:02", + }, + { + Id: 17, + OrderNo: "JU202508130007", + OrderType: 1, + ProductId: 1, + ProductName: "动静态IP", + PackageName: "测试卡", + DayPrice: 0.5, + ConnectCount: 1, + AccountCount: 1, + Accounts: "jx1237", + OrderAmount: 0.5, + CouponAmount: 0, + PaymentAmount: 0.5, + CreateTime: "2025-08-13 18:03:12", + UpdateTime: "2025-08-13 18:03:28", + }, + { + Id: 16, + OrderNo: "JU202508120008", + OrderType: 2, + ProductId: 2, + ProductName: "新HTTP/S5", + PackageName: "双月卡(活动)", + DayPrice: 128, + ConnectCount: 3, + AccountCount: 1, + Accounts: "https-004", + OrderAmount: 128, + CouponAmount: 0, + PaymentAmount: 128, + CreateTime: "2025-08-12 15:22:37", + UpdateTime: "2025-08-12 15:22:59", + }, + { + Id: 15, + OrderNo: "JU202508120009", + OrderType: 1, + ProductId: 3, + ProductName: "HTTP/S5", + PackageName: "月卡", + DayPrice: 98, + ConnectCount: 1, + AccountCount: 1, + Accounts: "global-002", + OrderAmount: 98, + CouponAmount: 5, + PaymentAmount: 93, + CreateTime: "2025-08-12 13:40:55", + UpdateTime: "2025-08-12 13:41:13", + }, + { + Id: 14, + OrderNo: "JU202508110010", + OrderType: 1, + ProductId: 1, + ProductName: "动静态IP", + PackageName: "天卡", + DayPrice: 3, + ConnectCount: 1, + AccountCount: 1, + Accounts: "jx1238", + OrderAmount: 3, + CouponAmount: 0, + PaymentAmount: 3, + CreateTime: "2025-08-11 10:05:29", + UpdateTime: "2025-08-11 10:05:44", + }, + { + Id: 13, + OrderNo: "JU202508110011", + OrderType: 3, + ProductId: 1, + ProductName: "动静态IP", + PackageName: "周卡", + DayPrice: 18, + ConnectCount: 4, + AccountCount: 4, + Accounts: "batch-jx-06,batch-jx-07,batch-jx-08,batch-jx-09", + OrderAmount: 72, + CouponAmount: 0, + PaymentAmount: 72, + CreateTime: "2025-08-11 08:56:48", + UpdateTime: "2025-08-11 08:57:06", + }, + { + Id: 12, + OrderNo: "JU202508100012", + OrderType: 2, + ProductId: 1, + ProductName: "动静态IP", + PackageName: "季卡", + DayPrice: 168, + ConnectCount: 1, + AccountCount: 1, + Accounts: "jx1239", + OrderAmount: 168, + CouponAmount: 0, + PaymentAmount: 168, + CreateTime: "2025-08-10 19:33:20", + UpdateTime: "2025-08-10 19:33:41", + }, + { + Id: 11, + OrderNo: "JU202508090013", + OrderType: 1, + ProductId: 2, + ProductName: "新HTTP/S5", + PackageName: "年卡", + DayPrice: 880, + ConnectCount: 20, + AccountCount: 2, + Accounts: "https-005,https-006", + OrderAmount: 1760, + CouponAmount: 50, + PaymentAmount: 1710, + CreateTime: "2025-08-09 21:18:07", + UpdateTime: "2025-08-09 21:18:30", + }, + { + Id: 10, + OrderNo: "JU202508090014", + OrderType: 1, + ProductId: 3, + ProductName: "HTTP/S5", + PackageName: "测试卡", + DayPrice: 1, + ConnectCount: 1, + AccountCount: 1, + Accounts: "global-003", + OrderAmount: 1, + CouponAmount: 0, + PaymentAmount: 1, + CreateTime: "2025-08-09 17:47:52", + UpdateTime: "2025-08-09 17:48:09", + }, + { + Id: 9, + OrderNo: "JU202508080015", + OrderType: 4, + ProductId: 2, + ProductName: "新HTTP/S5", + PackageName: "月卡(活动)", + DayPrice: 88, + ConnectCount: 6, + AccountCount: 6, + Accounts: "https-b1,https-b2,https-b3,https-b4,https-b5,https-b6", + OrderAmount: 528, + CouponAmount: 20, + PaymentAmount: 508, + CreateTime: "2025-08-08 12:26:34", + UpdateTime: "2025-08-08 12:26:58", + }, + { + Id: 8, + OrderNo: "JU202508070016", + OrderType: 1, + ProductId: 1, + ProductName: "动静态IP", + PackageName: "月卡(活动)", + DayPrice: 58, + ConnectCount: 2, + AccountCount: 1, + Accounts: "jx1240", + OrderAmount: 58, + CouponAmount: 0, + PaymentAmount: 58, + CreateTime: "2025-08-07 09:11:26", + UpdateTime: "2025-08-07 09:11:43", + }, + { + Id: 7, + OrderNo: "JU202508060017", + OrderType: 2, + ProductId: 3, + ProductName: "HTTP/S5", + PackageName: "季卡", + DayPrice: 268, + ConnectCount: 1, + AccountCount: 1, + Accounts: "global-004", + OrderAmount: 268, + CouponAmount: 8, + PaymentAmount: 260, + CreateTime: "2025-08-06 22:05:11", + UpdateTime: "2025-08-06 22:05:35", + }, + { + Id: 6, + OrderNo: "JU202508050018", + OrderType: 1, + ProductId: 1, + ProductName: "动静态IP", + PackageName: "天卡", + DayPrice: 3, + ConnectCount: 1, + AccountCount: 1, + Accounts: "jx1241", + OrderAmount: 3, + CouponAmount: 0, + PaymentAmount: 3, + CreateTime: "2025-08-05 14:38:49", + UpdateTime: "2025-08-05 14:39:05", + }, + { + Id: 5, + OrderNo: "JU202508040019", + OrderType: 3, + ProductId: 3, + ProductName: "HTTP/S5", + PackageName: "月卡", + DayPrice: 98, + ConnectCount: 3, + AccountCount: 3, + Accounts: "global-b1,global-b2,global-b3", + OrderAmount: 294, + CouponAmount: 0, + PaymentAmount: 294, + CreateTime: "2025-08-04 10:52:18", + UpdateTime: "2025-08-04 10:52:37", + }, + { + Id: 4, + OrderNo: "JU202508030020", + OrderType: 1, + ProductId: 1, + ProductName: "动静态IP", + PackageName: "双月卡(活动)", + DayPrice: 98, + ConnectCount: 1, + AccountCount: 1, + Accounts: "jx1242", + OrderAmount: 98, + CouponAmount: 0, + PaymentAmount: 98, + CreateTime: "2025-08-03 16:07:23", + UpdateTime: "2025-08-03 16:07:44", + }, + { + Id: 3, + OrderNo: "JU202508020021", + OrderType: 2, + ProductId: 1, + ProductName: "动静态IP", + PackageName: "年卡", + DayPrice: 688, + ConnectCount: 1, + AccountCount: 1, + Accounts: "jx1243", + OrderAmount: 688, + CouponAmount: 30, + PaymentAmount: 658, + CreateTime: "2025-08-02 20:29:56", + UpdateTime: "2025-08-02 20:30:14", + }, + { + Id: 2, + OrderNo: "JU202508010022", + OrderType: 1, + ProductId: 2, + ProductName: "新HTTP/S5", + PackageName: "周卡", + DayPrice: 25, + ConnectCount: 2, + AccountCount: 1, + Accounts: "https-007", + OrderAmount: 25, + CouponAmount: 0, + PaymentAmount: 25, + CreateTime: "2025-08-01 11:44:02", + UpdateTime: "2025-08-01 11:44:21", + }, + { + Id: 1, + OrderNo: "JU202507310023", + OrderType: 1, + ProductId: 1, + ProductName: "动静态IP", + PackageName: "月卡", + DayPrice: 68, + ConnectCount: 1, + AccountCount: 1, + Accounts: "jx1244", + OrderAmount: 68, + CouponAmount: 0, + PaymentAmount: 68, + CreateTime: "2025-07-31 09:58:37", + UpdateTime: "2025-07-31 09:58:55", + }, +] + +export const MOCK_PRODUCTS = [ + { id: 1, name: "动静态IP" }, + { id: 2, name: "新HTTP/S5" }, + { id: 3, name: "HTTP/S5" }, +] diff --git a/src/admin/refundOrders/page.tsx b/src/admin/refundOrders/page.tsx new file mode 100644 index 0000000..9960606 --- /dev/null +++ b/src/admin/refundOrders/page.tsx @@ -0,0 +1,272 @@ +import type { ColumnDef } from "@tanstack/react-table" +import { SearchIcon } from "lucide-react" +import { useMemo, useState } from "react" +import DataTable from "@/components/data-table" +import Page from "@/components/page" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + cutAccount, + DEFAULT_FILTERS, + formatOrderType, + type OrderFilters, + type OrderItem, + PACKAGE_OPTIONS, + totalConnections, +} from "@/lib/models/order" +import { MOCK_ORDERS, MOCK_PRODUCTS } from "./mock" + +function formatMoney(value: number): string { + return value.toFixed(2) +} + +function formatDate(value: string): string { + const date = new Date(value) + if (Number.isNaN(date.getTime())) return value + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, "0") + const day = String(date.getDate()).padStart(2, "0") + return `${year}.${month}.${day}` +} + +function AccountCell({ accounts }: { accounts: string }) { + const isCut = accounts.length > 15 + if (!isCut) return {accounts} + + return ( + + + {cutAccount(accounts)} + + {accounts} + + ) +} + +const columns: ColumnDef[] = [ + { + accessorKey: "CreateTime", + header: "日期", + cell: ({ row }) => formatDate(row.original.CreateTime), + }, + { + accessorKey: "OrderNo", + header: "订单编号", + }, + { + accessorKey: "OrderType", + header: "类型", + cell: ({ row }) => formatOrderType(row.original.OrderType), + }, + { + accessorKey: "ProductName", + header: "产品", + }, + { + accessorKey: "PackageName", + header: "套餐", + }, + { + accessorKey: "Accounts", + header: "账号", + cell: ({ row }) => , + }, + { + accessorKey: "ConnectCount", + header: "连接数", + cell: ({ row }) => totalConnections(row.original), + }, + { + accessorKey: "DayPrice", + header: "退款时长", + cell: ({ row }) => formatMoney(row.original.DayPrice), + }, + { + accessorKey: "DayPrice", + header: "退款单价", + cell: ({ row }) => formatMoney(row.original.DayPrice), + }, + { + accessorKey: "PaymentAmount", + header: "实付金额", + cell: ({ row }) => formatMoney(row.original.PaymentAmount), + }, + { + accessorKey: "CouponAmount", + header: "退款金额", + cell: ({ row }) => formatMoney(row.original.CouponAmount), + }, +] + +function matchOrders(list: typeof MOCK_ORDERS, filters: OrderFilters) { + const keyword = filters.keyword.trim() + + return list.filter(order => { + if ( + filters.orderType !== "0" && + order.OrderType !== Number(filters.orderType) + ) { + return false + } + if ( + filters.productId !== "0" && + order.ProductId !== Number(filters.productId) + ) { + return false + } + if ( + filters.packageName !== "0" && + order.PackageName !== filters.packageName + ) { + return false + } + if ( + keyword && + !order.OrderNo.includes(keyword) && + !order.Accounts.includes(keyword) + ) { + return false + } + if (filters.bTime && order.CreateTime.slice(0, 10) < filters.bTime) { + return false + } + if (filters.eTime && order.CreateTime.slice(0, 10) > filters.eTime) { + return false + } + return true + }) +} + +export default function RefundOrdersPage() { + const [filters, setFilters] = useState(DEFAULT_FILTERS) + const [appliedFilters, setAppliedFilters] = + useState(DEFAULT_FILTERS) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + + const filtered = useMemo( + () => matchOrders(MOCK_ORDERS, appliedFilters), + [appliedFilters], + ) + + const paged = useMemo( + () => filtered.slice((page - 1) * pageSize, page * pageSize), + [filtered, page, pageSize], + ) + + const patch = (partial: Partial) => + setFilters({ ...filters, ...partial }) + + const handleSearch = () => { + setAppliedFilters(filters) + setPage(1) + } + + return ( + +
+
+ + 日期筛选: + + patch({ bTime: e.target.value })} + className="w-36" + aria-label="开始时间" + /> + + patch({ eTime: e.target.value })} + className="w-36" + aria-label="结束时间" + /> +
+ + + + + +
+ patch({ keyword: e.target.value })} + onKeyDown={e => { + if (e.key === "Enter") handleSearch() + }} + placeholder="账号" + className="w-40" + /> + +
+
+ +
+

订单管理

+ { + setPageSize(size) + setPage(1) + }, + }} + /> +
+
+ ) +} diff --git a/src/admin/resources/long.tsx b/src/admin/resources/long.tsx new file mode 100644 index 0000000..d555a13 --- /dev/null +++ b/src/admin/resources/long.tsx @@ -0,0 +1,5 @@ +import Page from "@/components/page" + +export default function LongPage() { + return 长效管理 +} diff --git a/src/admin/resources/short.tsx b/src/admin/resources/short.tsx new file mode 100644 index 0000000..e943627 --- /dev/null +++ b/src/admin/resources/short.tsx @@ -0,0 +1,5 @@ +import Page from "@/components/page" + +export default function ShortPage() { + return 短效套餐 +} diff --git a/src/admin/rosorder/page.tsx b/src/admin/rosorder/page.tsx new file mode 100644 index 0000000..ad46e00 --- /dev/null +++ b/src/admin/rosorder/page.tsx @@ -0,0 +1,5 @@ +import Page from "@/components/page" + +export default function RosorderPage() { + return 软路由页面 +} diff --git a/src/api/linedata.ts b/src/api/linedata.ts index 8796db7..95574e9 100644 --- a/src/api/linedata.ts +++ b/src/api/linedata.ts @@ -1,5 +1,9 @@ import type { ApiResponse } from "@/lib/api" -import { callPublic } from "./base" + +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 LineItem = { city: string | number @@ -20,24 +24,50 @@ export type LineSearchResult = { data: LineItem[] } -export async function fetchLineData( - product: number, -): Promise> { - return await callPublic( - `/script/linedata/display.php?product=${product}`, - undefined, - "GET", +async function getJson(url: string): Promise> { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT) + try { + const response = await fetch(url, { signal: controller.signal }) + if (!response.ok) { + return { + success: false, + status: response.status, + message: "请求失败", + } + } + const data = (await response.json()) as T + return { success: true, data } + } catch (e) { + if (controller.signal.aborted) { + return { + success: false, + status: 408, + message: "请求超时,请稍后重试", + } + } + return { + success: false, + status: 500, + message: (e as Error).message || "网络错误", + } + } finally { + clearTimeout(timer) + } +} + +export function fetchLineData(product: number): Promise> { + return getJson( + `${PHP_API_BASE_URL}/script/linedata/display.php?product=${product}`, ) } -export async function searchLineData( +export function searchLineData( productid: number, info: string, ): Promise> { const query = `type=0&productid=${productid}&info=${encodeURIComponent(info)}` - return await callPublic( - `/script/linedata/search.php?${query}`, - undefined, - "GET", + return getJson( + `${PHP_API_BASE_URL}/script/linedata/search.php?${query}`, ) } diff --git a/src/api/product.ts b/src/api/product.ts index 22df5f0..fa98710 100644 --- a/src/api/product.ts +++ b/src/api/product.ts @@ -9,3 +9,23 @@ export async function fetchProducts(): Promise> { "GET", ) } + +export async function fetchStaticProducts(): Promise< + ApiResponse +> { + return await callPublic( + "/product/ApiProductStatic", + undefined, + "GET", + ) +} + +export async function fetchWindowProducts(): Promise< + ApiResponse +> { + return await callPublic( + "/product/ApiProductWindow", + undefined, + "GET", + ) +} diff --git a/src/components/composites/dialogs/editInfoDialog.tsx b/src/components/composites/dialogs/editInfoDialog.tsx index 2b97feb..70cd5ff 100644 --- a/src/components/composites/dialogs/editInfoDialog.tsx +++ b/src/components/composites/dialogs/editInfoDialog.tsx @@ -1,5 +1,8 @@ +import { zodResolver } from "@hookform/resolvers/zod" import { Eye, EyeOff } from "lucide-react" -import { useState } from "react" +import { useEffect, useState } from "react" +import { Controller, useForm } from "react-hook-form" +import { z } from "zod" import { Button } from "@/components/ui/button" import { Dialog, @@ -7,45 +10,70 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" +import { FieldError } from "@/components/ui/field" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" +export type EditInfoValues = { + username: string + taobao: string + password: string + email: string + wechat: string + qq: string +} + +const editInfoSchema = z.object({ + username: z.string().trim().min(1, "请输入用户名"), + taobao: z.string(), + password: z.string().min(1, "请输入密码"), + email: z + .string() + .refine( + v => !v || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), + "请输入正确的邮箱", + ), + wechat: z.string(), + qq: z.string(), +}) + interface EditInfoDialogProps { open: boolean onOpenChange: (open: boolean) => void - initialData?: { - username: string - taobao: string - password: string - email: string - wechat: string - qq: string - } - onSave?: (data: any) => void + initialData?: EditInfoValues + onSave?: (data: EditInfoValues) => void +} + +const DEFAULT_INITIAL_DATA: EditInfoValues = { + username: "191****1234", + taobao: "191****1234", + password: "********", + email: "123456123@qq.com", + wechat: "", + qq: "", } export function EditInfoDialog({ open, onOpenChange, - initialData = { - username: "191****1234", - taobao: "191****1234", - password: "********", - email: "123456123@qq.com", - wechat: "", - qq: "", - }, + initialData = DEFAULT_INITIAL_DATA, onSave, }: EditInfoDialogProps) { - const [formData, setFormData] = useState(initialData) const [showPassword, setShowPassword] = useState(false) - const handleChange = (field: string, value: string) => { - setFormData(prev => ({ ...prev, [field]: value })) - } + const form = useForm({ + resolver: zodResolver(editInfoSchema), + defaultValues: initialData, + }) - const handleSubmit = () => { - onSave?.(formData) + useEffect(() => { + if (open) { + form.reset(initialData) + } + }, [open, initialData, form]) + + const onSubmit = (data: EditInfoValues) => { + onSave?.(data) onOpenChange(false) } @@ -58,91 +86,123 @@ export function EditInfoDialog({ -
- {/* 用户名 */} +
- handleChange("username", e.target.value)} - className="h-11 bg-gray-50 border-gray-200" + ( + <> + + {fieldState.error?.message} + + )} />
- {/* 淘宝会员名 */}
- handleChange("taobao", e.target.value)} - className="h-11 bg-gray-50 border-gray-200" + ( + + )} />
- {/* 密码 */}
-
- handleChange("password", e.target.value)} - className="h-11 bg-gray-50 border-gray-200 pr-10" - /> - -
+ ( + <> +
+ + +
+ {fieldState.error?.message} + + )} + />
- {/* 邮箱 */}
- handleChange("email", e.target.value)} - className="h-11 bg-gray-50 border-gray-200" + ( + <> + + {fieldState.error?.message} + + )} />
- {/* 微信 */}
- handleChange("wechat", e.target.value)} - placeholder="请输入" - className="h-11 bg-gray-50 border-gray-200" + ( + + )} />
- {/* QQ */}
- handleChange("qq", e.target.value)} - placeholder="请输入" - className="h-11 bg-gray-50 border-gray-200" + ( + + )} />
- {/* 保存按钮 */} -
+ ) diff --git a/src/components/data-table.tsx b/src/components/data-table.tsx index 60e7e1d..b62624b 100644 --- a/src/components/data-table.tsx +++ b/src/components/data-table.tsx @@ -27,14 +27,12 @@ export type DataTableProps = { headRow?: string dataRow?: string } - /** 是否开启行选择复选框 */ enableSelection?: boolean - /** 受控选中状态 */ rowSelection?: Record - /** 选中状态变化回调 */ onSelectionChange?: (selection: Record) => void - /** 行唯一ID生成函数 */ getRowId?: (row: T, index: number) => string + stickyHeader?: boolean + maxHeight?: string } export default function DataTable>( @@ -112,8 +110,18 @@ export default function DataTable>( <> {/* 数据表 */}
- - + + {table.getHeaderGroups().map(group => ( {group.headers.map(header => ( diff --git a/src/components/productDropdown.tsx b/src/components/productDropdown.tsx index 05b689c..8d1627b 100644 --- a/src/components/productDropdown.tsx +++ b/src/components/productDropdown.tsx @@ -49,7 +49,7 @@ export default function ProductDropdown({ title="动静态IP" desc="千万级动态短效IP,自动去重,高并发采集" tag="热门" - onClick={() => handleClick("/product?tab=short")} + onClick={() => handleClick("/product")} /> 热门推荐: + + ) + }, + }, + { + accessorKey: "daikuan", + header: "带宽", + cell: ({ row }) => toText(row.original.daikuan), + }, + { + accessorKey: "online", + header: "服务器状态", + cell: ({ row }) => { + const text = toText(row.original.online) + return ( + + {text} + + ) + }, + }, + ], + [handleCopy], + )} + stickyHeader + maxHeight="max-h-[640px]" + classNames={{ headRow: "bg-gray-50 text-gray-600" }} + pagination={ + rows.length > 0 + ? { + page: pageIndex + 1, + size: pageSize, + total: rows.length, + sizeOptions: PAGE_SIZE_OPTIONS, + onPageChange: page => setPageIndex(page - 1), + onSizeChange: size => { + setPageSize(size) + setPageIndex(0) + }, + } + : undefined + } + /> ) diff --git a/src/home/login/index.tsx b/src/home/login/index.tsx index 395effc..06acb97 100644 --- a/src/home/login/index.tsx +++ b/src/home/login/index.tsx @@ -1,15 +1,38 @@ +import { zodResolver } from "@hookform/resolvers/zod" import { EyeIcon, EyeOffIcon } from "lucide-react" import { useEffect, useState } from "react" +import { Controller, useForm } from "react-hook-form" import { Link, useLocation, useNavigate } from "react-router-dom" import { toast } from "sonner" +import { z } from "zod" import { apiLogin, apiRegister } from "@/api/auth" import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" import { Checkbox } from "@/components/ui/checkbox" +import { FieldError } from "@/components/ui/field" import { Input } from "@/components/ui/input" import { cn } from "@/lib/utils" import bg from "./_assets/bg.webp" +const loginSchema = z.object({ + account: z.string().trim().min(1, "请输入账号"), + password: z.string().min(1, "请输入密码"), + remember: z.boolean(), +}) + +type LoginValues = z.infer + +const registerSchema = z.object({ + phone: z.string().regex(/^1\d{10}$/, "请输入正确的手机号"), + smsCode: z.string().min(3, "请输入短信验证码").max(6, "请输入短信验证码"), + password: z.string().trim().min(1, "请输入密码"), + wx: z.string(), + qq: z.string(), + agreed: z.boolean().refine(v => v, "请先阅读并同意用户协议和隐私政策"), +}) + +type RegisterValues = z.infer + export default function LoginPage() { const [tab, setTab] = useState<"login" | "register">("login") const location = useLocation() @@ -69,29 +92,25 @@ export default function LoginPage() { function LoginForm() { const navigate = useNavigate() - const [account, setAccount] = useState("") - const [password, setPassword] = useState("") const [showPassword, setShowPassword] = useState(false) - const [remember, setRemember] = useState(false) const [submitting, setSubmitting] = useState(false) - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!account.trim()) { - toast.error("请输入账号") - return - } - if (!password) { - toast.error("请输入密码") - return - } + const form = useForm({ + resolver: zodResolver(loginSchema), + defaultValues: { + account: "", + password: "", + remember: false, + }, + }) + + const onSubmit = async (data: LoginValues) => { setSubmitting(true) try { const resp = await apiLogin({ - logincode: account, - password, + logincode: data.account, + password: data.password, }) - console.log(resp, "resp") if (resp.success) { toast.success("登录成功", { @@ -109,53 +128,75 @@ function LoginForm() { } return ( -
+
- setAccount(e.target.value)} + ( + <> + + {fieldState.error?.message} + + )} />
-
- setPassword(e.target.value)} - /> - -
+ ( + <> +
+ + +
+ {fieldState.error?.message} + + )} + />
- setRemember(!!v)} + ( + + )} />