diff --git a/src/admin/cashOut/page.tsx b/src/admin/cashOut/page.tsx index 558b329..dde018b 100644 --- a/src/admin/cashOut/page.tsx +++ b/src/admin/cashOut/page.tsx @@ -1,45 +1,60 @@ +import type { ColumnDef } from "@tanstack/react-table" +import { fetchCashOutList } from "@/api/cashOut" import DataTable from "@/components/data-table" import Page from "@/components/page" +import { useList } from "@/lib/hooks/useList" import type { CashOutItem } from "@/lib/models/cashOut" +const columns: ColumnDef[] = [ + { + accessorKey: "username", + header: "用户", + }, + { + accessorKey: "money", + header: "提现金额", + cell: ({ row }) => `¥${row.original.money}`, + }, + { + accessorKey: "apply_reason", + header: "提现理由", + cell: ({ row }) => row.original.apply_reason || "--", + }, + { + accessorKey: "alipay_account", + header: "支付宝账号", + }, + { + accessorKey: "remark", + header: "备注", + cell: ({ row }) => row.original.remark || "--", + }, + { + accessorKey: "status", + header: "状态", + }, + { + accessorKey: "create_time", + header: "提现时间", + }, +] + export default function CashOutPage() { + const { data, status } = useList(fetchCashOutList) + return (

提现管理

- data={[]} - status="done" - columns={[ - { - accessorKey: "username", - header: "用户", - }, - { - accessorKey: "money", - header: "提现金额", - }, - { - accessorKey: "reason", - header: "提现理由", - }, - { - accessorKey: "alipay_account", - header: "支付宝账号", - }, - { - accessorKey: "remark", - header: "备注", - }, - { - accessorKey: "status", - header: "状态", - }, - { - accessorKey: "create_time", - header: "提现时间", - }, - ]} + data={data} + status={status} + columns={columns} + pagination={{ + page: 0, + size: 10, + total: data.length, + }} />
diff --git a/src/admin/channels/mock.ts b/src/admin/channels/mock.ts deleted file mode 100644 index 40748a5..0000000 --- a/src/admin/channels/mock.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { ChannelRecord } from "@/lib/models/channel" - -// TODO: 后端通道列表接口就绪后删除(见 src/api/channel.ts fetchChannelPage) -export const INITIAL_CHANNELS: ChannelRecord[] = [ - { - id: "1", - startAt: "2025.04.09", - product: "极光IP", - plan: "测试卡(带宽)", - account: "ft8765", - password: "765修改", - ip: "甘肃-兰州125.75.111.127", - server: "p3.125.74.129.38.sdip.cc", - connections: 1, - expiredAt: "2025.04.09", - remaining: "已过期", - online: "查看", - download: "下载", - status: "", - }, - { - id: "2", - startAt: "2025.04.09", - product: "极光IP", - plan: "测试卡(带宽)", - account: "ft8765", - password: "765修改", - ip: "辽宁-辽阳123.190.161.192", - server: "p16.cqdwx4.sdip.cc", - connections: 1, - expiredAt: "2025.04.09", - remaining: "0天00时37分", - online: "查看", - download: "下载", - status: "", - }, - { - id: "3", - startAt: "2025.04.09", - product: "极光IP", - plan: "静态-项目-20M-月卡", - account: "ft8765", - password: "765修改", - ip: "黑龙江-鸡西100.100.4.249", - server: "p37.sdlw2.sdip.cc", - connections: 1, - expiredAt: "2025.04.09", - remaining: "0天00时37分", - online: "查看", - download: "下载", - status: "", - }, -] diff --git a/src/admin/channels/page.tsx b/src/admin/channels/page.tsx index e13f172..0f4d82f 100644 --- a/src/admin/channels/page.tsx +++ b/src/admin/channels/page.tsx @@ -1,251 +1,248 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { useMemo, useState } from "react" -import { Controller, useForm } from "react-hook-form" -import { Link } from "react-router-dom" +import type { ColumnDef } from "@tanstack/react-table" +import { SearchIcon } from "lucide-react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { toast } from "sonner" -import { z } from "zod" +import { fetchMyAccounts } from "@/api/account" import DataTable from "@/components/data-table" import Page from "@/components/page" import { Button } from "@/components/ui/button" -import { Field, FieldError, FieldLabel } from "@/components/ui/field" import { Input } from "@/components/ui/input" -import type { ChannelRecord } from "@/lib/models/channel" -import { INITIAL_CHANNELS } from "./mock" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { useProducts } from "@/lib/hooks/useProducts" +import { + EXPIRED_DAY_OPTIONS, + type MyAccountItem, + type MyAccountQueryParams, +} from "@/lib/models/account" -// TODO: 后端就绪后替换为 fetchChannelPage(见 src/api/channel.ts) +type ChannelQuery = { + expiredDay: string + productId: string + packageName: string + keyword: string +} -const filterSchema = z.object({ - expired_at_end: z.string().optional(), - product: z.string().optional(), - plan: z.string().optional(), - account: z.string().optional(), - status: z.string().optional(), -}) +const INITIAL_QUERY: ChannelQuery = { + expiredDay: "-100", + productId: "0", + packageName: "0", + keyword: "", +} -type FormValues = z.infer +const PAGE_SIZE = 10 + +function toParams(query: ChannelQuery): MyAccountQueryParams { + return { + ExpiredDay: Number(query.expiredDay), + ProductId: Number(query.productId) || undefined, + PackageId: query.packageName === "0" ? undefined : query.packageName, + Keyword: query.keyword.trim() || undefined, + } +} + +const columns: ColumnDef[] = [ + { + accessorKey: "StartTime", + header: "开通时间", + }, + { + accessorKey: "ProductName", + header: "产品", + }, + { + accessorKey: "PackageName", + header: "套餐", + }, + { + accessorKey: "Account", + header: "账号", + cell: ({ row }) => ( + {row.original.Account} + ), + }, + { + accessorKey: "Pwd", + header: "密码", + cell: ({ row }) => ( + {row.original.Pwd} + ), + }, + { + accessorKey: "ConnectCount", + header: "连接数", + }, + { + accessorKey: "EndTime", + header: "到期时间", + }, + { + accessorKey: "RestTime", + header: "剩余时间", + cell: ({ row }) => ( + + {row.original.RestTime} + + ), + }, + { + accessorKey: "Status", + header: "状态", + cell: ({ row }) => + row.original.Status === 1 ? ( + 正常 + ) : ( + 已退货 + ), + }, +] export default function ChannelsPage() { - const [filterValues, setFilterValues] = useState({}) - const [rowSelection, setRowSelection] = useState>({}) + const products = useProducts() + const [query, setQuery] = useState(INITIAL_QUERY) - const { control, handleSubmit, reset } = useForm({ - resolver: zodResolver(filterSchema), - defaultValues: { - expired_at_end: "", - product: "", - plan: "", - account: "", - status: "", - }, - }) + const [data, setData] = useState([]) + const [status, setStatus] = useState<"load" | "done" | "fail">("load") + const [page, setPage] = useState(1) - const filteredData = useMemo(() => { - if (!filterValues || Object.values(filterValues).every(value => !value)) { - return INITIAL_CHANNELS + const selectedProduct = useMemo( + () => + products.find(item => String(item.Product.Id) === query.productId) ?? + null, + [products, query.productId], + ) + + const load = useCallback(async (params: MyAccountQueryParams) => { + setStatus("load") + const res = await fetchMyAccounts(params) + if (res.success) { + setData(res.data) + setStatus("done") + } else { + setStatus("fail") + toast.error(res.message) } + setPage(1) + }, []) - return INITIAL_CHANNELS.filter(record => { - if (filterValues.expired_at_end) { - return record.expiredAt <= filterValues.expired_at_end - } + const loadedRef = useRef(false) + useEffect(() => { + if (loadedRef.current) return + loadedRef.current = true + void load(toParams(INITIAL_QUERY)) + }, [load]) - if ( - filterValues.product && - !record.product.includes(filterValues.product) - ) { - return false - } - - if (filterValues.plan && !record.plan.includes(filterValues.plan)) { - return false - } - - if ( - filterValues.account && - !record.account.includes(filterValues.account) - ) { - return false - } - - if (filterValues.status && record.status !== filterValues.status) { - return false - } - - return true - }) - }, [filterValues]) - - const onSubmit = (values: FormValues) => { - setFilterValues(values) - setRowSelection({}) + const applyFilters = (patch: Partial) => { + const next = { ...query, ...patch } + setQuery(next) + void load(toParams(next)) } - const onReset = () => { - reset() - setFilterValues({}) - setRowSelection({}) - } - - // 根据选中ID获取对应行数据 - const getSelectedRows = () => { - const ids = Object.keys(rowSelection) - return filteredData.filter(item => ids.includes(item.id)) - } - - // 批量续费 - const handleRenew = () => { - const rows = getSelectedRows() - // TODO: 后端续费接口就绪后接入(见 src/api/http.ts rebuyOrder) - toast.info(`已选择 ${rows.length} 条数据进行续费`) - } - - // 单个退货 - const handleReturn = () => { - const rows = getSelectedRows() - if (rows.length !== 1) { - toast.error("请选择一条数据进行退货") - return - } - // TODO: 后端退货接口就绪后接入 - toast.info(`退货账号:${rows[0].account}`) - } + const paged = useMemo( + () => data.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE), + [data, page], + ) return ( -
-
-
- - 为了给您带来更好的服务体验,请完善QQ号和微信号 - - 立即完善 -
-
- ( - - 到期时间 - - {fieldState.error?.message} - - )} - /> +
+
+ + 过期时间: + + +
- ( - - 全部产品 - - {fieldState.error?.message} - - )} - /> + - ( - - 全部套餐 - - {fieldState.error?.message} - - )} - /> + - ( - - 账号 - - {fieldState.error?.message} - - )} - /> - -
- - -
-
- +
+ setQuery({ ...query, keyword: e.target.value })} + onKeyDown={e => { + if (e.key === "Enter") applyFilters({}) + }} + placeholder="账号" + className="w-40" + /> + +
-
-
- - -

- 温馨提示:批量续费可通过筛选和搜索功能精准定位账号 -

-
- - data={filteredData} - status="done" - columns={[ - { accessorKey: "startAt", header: "开通时间" }, - { accessorKey: "product", header: "产品" }, - { accessorKey: "plan", header: "套餐" }, - { accessorKey: "account", header: "账号" }, - { accessorKey: "password", header: "密码" }, - { accessorKey: "ip", header: "IP地址" }, - { accessorKey: "server", header: "服务器" }, - { accessorKey: "connections", header: "连接数" }, - { accessorKey: "expiredAt", header: "到期时间" }, - { accessorKey: "remaining", header: "剩余时间" }, - { - accessorKey: "online", - header: "在线及拨线", - cell: ({ row }) => ( - - ), - }, - { accessorKey: "status", header: "选择地区" }, - { - accessorKey: "download", - header: "软件下载", - cell: ({ row }) => ( - - ), - }, - ]} +
+

IP账号管理

+ row.id} - rowSelection={rowSelection} - onSelectionChange={setRowSelection} />
diff --git a/src/admin/coupon/page.tsx b/src/admin/coupon/page.tsx index 8d66fef..8a9b0e4 100644 --- a/src/admin/coupon/page.tsx +++ b/src/admin/coupon/page.tsx @@ -1,169 +1,106 @@ import { CheckCircle, Clock, Gift, Tag, Ticket, XCircle } from "lucide-react" +import { useMemo } from "react" +import { fetchMyCoupons } from "@/api/coupon" import Page from "@/components/page" import { Badge } from "@/components/ui/badge" import { Card, CardContent } from "@/components/ui/card" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { useList } from "@/lib/hooks/useList" +import type { MyCouponItem } from "@/lib/models/coupon" +import { formatDate } from "@/lib/utils" -// TODO: 优惠券接口就绪后接入(见 src/api/coupon.ts fetchCouponPage 等) +const COUPON_TYPE_DISCOUNT = 1 -interface Coupon { - couponType: "Discount" | "Minus" - couponValue: number - name: string - allowMinAmount: number -} +function CouponCard({ item }: { item: MyCouponItem }) { + const isDiscount = item.Coupon.CouponType === COUPON_TYPE_DISCOUNT + const isUsed = item.IsUsed -interface UserCouponModel { - coupon: Coupon - orgin: { - startTime: string - endTime: string - remark: string - } - isUsed: boolean -} + return ( + + +
+
+ {isDiscount ? ( + <> + + {item.Coupon.CouponValue} + + 折 + + ) : ( + <> + ¥ + + {item.Coupon.CouponValue} + + + )} +
-// Mock 数据 -const mockCoupons: UserCouponModel[] = [ - { - coupon: { - couponType: "Discount", - couponValue: 8, - name: "八折优惠券", - allowMinAmount: 100, - }, - orgin: { - startTime: "2026-01-01", - endTime: "2026-12-31", - remark: "新用户注册赠送", - }, - isUsed: false, - }, - { - coupon: { - couponType: "Minus", - couponValue: 50, - name: "满减优惠券", - allowMinAmount: 200, - }, - orgin: { - startTime: "2026-06-01", - endTime: "2026-09-30", - remark: "活动期间领取", - }, - isUsed: false, - }, - { - coupon: { - couponType: "Discount", - couponValue: 9, - name: "九折优惠券", - allowMinAmount: 0, - }, - orgin: { - startTime: "2026-08-01", - endTime: "2026-10-01", - remark: "VIP专享", - }, - isUsed: true, - }, - { - coupon: { - couponType: "Minus", - couponValue: 20, - name: "通用优惠券", - allowMinAmount: 50, - }, - orgin: { - startTime: "2026-07-01", - endTime: "2026-08-31", - remark: "签到领取", - }, - isUsed: false, - }, -] - -export default function CouponPage() { - const availableCoupons = mockCoupons.filter(item => !item.isUsed) - const usedCoupons = mockCoupons.filter(item => item.isUsed) - - const renderCoupon = (item: UserCouponModel) => { - const isDiscount = item.coupon.couponType === "Discount" - const isUsed = item.isUsed - - return ( - - - {/* 优惠券左侧 - 面额 */} -
-
- {isDiscount ? ( - <> - - {item.coupon.couponValue} - - 折 - +
+
+

{item.Coupon.Name}

+ {isUsed ? ( + + + 已使用 + + ) : item.IsExpired ? ( + + + 已过期 + ) : ( - <> - ¥ - - {item.coupon.couponValue} - - + + + 可使用 + )}
- {/* 优惠券右侧 - 详情 */} -
-
-

{item.coupon.name}

- {isUsed ? ( - - - 已使用 - - ) : ( - - - 可使用 - - )} -
- -
- - - {item.coupon.allowMinAmount > 0 - ? `满${item.coupon.allowMinAmount}元可用` - : "无限制"} - - - - {new Date(item.orgin.startTime).toLocaleDateString()} -{" "} - {new Date(item.orgin.endTime).toLocaleDateString()} - - - - {item.orgin.remark} - -
+
+ + + {item.Coupon.AllowMinAmount > 0 + ? `满${item.Coupon.AllowMinAmount}元可用` + : "无限制"} + + + + {formatDate(item.Orgin.StartTime)} -{" "} + {formatDate(item.Orgin.EndTime)} + + + + {item.Orgin.Remark} +
+
- {/* 已使用水印效果 */} - {isUsed && ( -
- -
- )} - - - ) - } + {isUsed && ( +
+ +
+ )} + + + ) +} + +export default function CouponPage() { + const { data: coupons, status } = useList(fetchMyCoupons) + + const availableCoupons = useMemo( + () => coupons.filter(item => !item.IsUsed && !item.IsExpired), + [coupons], + ) + const usedCoupons = useMemo( + () => coupons.filter(item => item.IsUsed), + [coupons], + ) return ( @@ -172,7 +109,7 @@ export default function CouponPage() {

我的优惠券

- 共 {mockCoupons.length} 张 + 共 {coupons.length} 张
@@ -187,7 +124,16 @@ export default function CouponPage() { - {availableCoupons.length === 0 ? ( + {status === "load" ? ( +
+

加载中...

+
+ ) : status === "fail" ? ( +
+ +

加载失败,请刷新重试

+
+ ) : availableCoupons.length === 0 ? (

暂无可用优惠券

@@ -195,7 +141,9 @@ export default function CouponPage() {
) : (
- {availableCoupons.map(renderCoupon)} + {availableCoupons.map(item => ( + + ))}
)}
@@ -208,7 +156,9 @@ export default function CouponPage() {
) : (
- {usedCoupons.map(renderCoupon)} + {usedCoupons.map(item => ( + + ))}
)} diff --git a/src/admin/dashboard/page.tsx b/src/admin/dashboard/page.tsx index eb909fc..5443e60 100644 --- a/src/admin/dashboard/page.tsx +++ b/src/admin/dashboard/page.tsx @@ -1,10 +1,14 @@ import { Check } from "lucide-react" import { type ReactNode, useEffect, useState } from "react" +import { useNavigate } from "react-router-dom" +import { toast } from "sonner" import type { UserInfoRecord } from "@/api/token" import AgreementDialog from "@/components/composites/dialogs/agreementDialog" +import { CashOutDialog } from "@/components/composites/dialogs/cashOutDialog" import { ChangePwdDialog } from "@/components/composites/dialogs/changePwdDialog" import { EditInfoDialog } from "@/components/composites/dialogs/editInfoDialog" import RealNameDialog from "@/components/composites/dialogs/realNameDialog" +import { RechargeDialog } from "@/components/composites/dialogs/rechargeDialog" import Page from "@/components/page" import { QrCodeImage } from "@/components/qrCodeImage" import { useDashboardStore } from "@/components/stores/dashboard" @@ -22,6 +26,7 @@ import type { DashboardBalance, IpAccountStats, } from "@/lib/models/dashboard" +import { clearPayPaid, PAY_PAID_KEY } from "@/lib/recharge" import { cn } from "@/lib/utils" import b3 from "../assets/b3.webp" import b5 from "../assets/b5.webp" @@ -47,12 +52,12 @@ function readUserInfo(userInfo: UserInfoRecord | null) { const info = userInfo ?? {} const phone = String( info.Phone ?? - info.LoginCode ?? - info.phone ?? - info.logincode ?? - info.Mobile ?? - info.mobile ?? - "", + info.LoginCode ?? + info.phone ?? + info.logincode ?? + info.Mobile ?? + info.mobile ?? + "", ) const managerId = Number(info.ManagerId ?? info.managerId ?? 0) return { @@ -68,6 +73,7 @@ const num = (value: unknown) => typeof value === "number" && Number.isFinite(value) ? value : 0 export default function DashboardPage() { + const navigate = useNavigate() const apiIndex = useDashboardStore(state => state.data) const agreementSigned = useDashboardStore(state => state.agreementSigned) const refresh = useDashboardStore(state => state.refresh) @@ -80,6 +86,18 @@ export default function DashboardPage() { void refreshAgreement() }, [refresh, refreshAgreement]) + // 支付窗口完成支付后(storage 事件)自动刷新余额 + useEffect(() => { + const handler = (e: StorageEvent) => { + if (e.key !== PAY_PAID_KEY || !e.newValue) return + toast.success("充值成功,余额已更新") + clearPayPaid() + void refresh() + } + window.addEventListener("storage", handler) + return () => window.removeEventListener("storage", handler) + }, [refresh]) + const user = apiIndex?.UserModel const statistic = apiIndex?.Statistic const account = apiIndex?.AccountModel @@ -124,6 +142,8 @@ export default function DashboardPage() { const [realNameOpen, setRealNameOpen] = useState(false) const [agreementOpen, setAgreementOpen] = useState(false) const [managerQrOpen, setManagerQrOpen] = useState(false) + const [rechargeOpen, setRechargeOpen] = useState(false) + const [cashOutOpen, setCashOutOpen] = useState(false) // 未返回状态(请求中/失败/无手机号)时默认按「未实名/未签署」展示, // 接口成功后自动更新为真实状态 @@ -235,7 +255,7 @@ export default function DashboardPage() {
- + {/* */}
{agreementAction(true)} @@ -253,12 +273,14 @@ export default function DashboardPage() { @@ -274,6 +296,7 @@ export default function DashboardPage() { @@ -373,7 +396,7 @@ export default function DashboardPage() { initialData={{ qq: accountInfo.qq, wechat: accountInfo.wechat, - taobao: accountInfo.taobao, + // taobao: accountInfo.taobao, email: accountInfo.email, }} onSaved={refresh} @@ -384,6 +407,13 @@ export default function DashboardPage() { onOpenChange={setRealNameOpen} onVerified={refresh} /> + + [] = [ + { + accessorKey: "CreateTime", + header: "交易时间", + }, + { + accessorKey: "UserName", + header: "用户", + }, + { + accessorKey: "ScoreTypeName", + header: "资金去向", + }, + { + accessorKey: "ScoreValue", + header: "金额", + cell: ({ row }) => { + const value = Number(row.original.ScoreValue) + return ( + + {value >= 0 ? "+" : ""} + {row.original.ScoreValue} + + ) + }, + }, + { + accessorKey: "RestAmount1", + header: "操作前余额", + cell: ({ row }) => `¥${row.original.RestAmount1}`, + }, + { + accessorKey: "RestAmount2", + header: "操作后余额", + cell: ({ row }) => `¥${row.original.RestAmount2}`, + }, + { + accessorKey: "Remark", + header: "备注", + cell: ({ row }) => ( + + {row.original.Remark} + + ), + }, +] export default function FundsPage() { + const [data, setData] = useState([]) + const [status, setStatus] = useState<"load" | "done" | "fail">("load") + const [page, setPage] = useState(1) + const [hasMore, setHasMore] = useState(false) + + const load = useCallback(async (targetPage: number) => { + setStatus("load") + const res = await fetchScoreList(targetPage) + if (res.success) { + setData(res.data) + setHasMore(res.data.length >= SCORE_PAGE_SIZE) + setStatus("done") + } else { + setStatus("fail") + toast.error(res.message) + } + }, []) + + useEffect(() => { + void load(1) + }, [load]) + + const handlePageChange = (nextPage: number) => { + if (nextPage < 1) return + setPage(nextPage) + void load(nextPage) + } + + // 接口不返回总条数:还有下一页时多加 1 条保证可翻页,否则按累计条数展示 + const total = hasMore + ? page * SCORE_PAGE_SIZE + 1 + : (page - 1) * SCORE_PAGE_SIZE + data.length + return ( - +
+

资金明细

+ +
) } diff --git a/src/admin/mock/orders.ts b/src/admin/mock/orders.ts deleted file mode 100644 index 7068ded..0000000 --- a/src/admin/mock/orders.ts +++ /dev/null @@ -1,401 +0,0 @@ -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/orders/page.tsx b/src/admin/orders/page.tsx index d04524d..09b09ae 100644 --- a/src/admin/orders/page.tsx +++ b/src/admin/orders/page.tsx @@ -1,30 +1,18 @@ import type { ColumnDef } from "@tanstack/react-table" -import { SearchIcon } from "lucide-react" +import { fetchMyOrders } from "@/api/order" import AccountCell from "@/components/composites/accountCell" +import OrderFilterBar from "@/components/composites/orderFilterBar" 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 { useOrderFilters } from "@/lib/hooks/useOrderFilters" +import { MY_ORDER_PAGE_SIZE, useMyOrderPage } from "@/lib/hooks/useMyOrderPage" +import { useProducts } from "@/lib/hooks/useProducts" import { formatDate, formatMoney, formatOrderType, - ORDER_TYPE_LABELS, type OrderItem, - PACKAGE_OPTIONS, totalConnections, } from "@/lib/models/order" -import { MOCK_ORDERS, MOCK_PRODUCTS } from "../mock/orders" - -// 后端订单接口就绪后,将 MOCK_ORDERS 替换为 fetchOrderPage 返回的分页数据 const columns: ColumnDef[] = [ { @@ -82,122 +70,30 @@ const columns: ColumnDef[] = [ ] export default function OrdersPage() { - const { - filters, - patch, - handleSearch, - page, - setPage, - pageSize, - handleSizeChange, - filtered, - paged, - } = useOrderFilters(MOCK_ORDERS) + const products = useProducts() + const { filters, patch, load, total, status, page, setPage, paged } = + useMyOrderPage(fetchMyOrders) 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" - /> - -
-
+ void load()} + />

订单管理

diff --git a/src/admin/refundOrders/page.tsx b/src/admin/refundOrders/page.tsx index 8b38511..fe02a10 100644 --- a/src/admin/refundOrders/page.tsx +++ b/src/admin/refundOrders/page.tsx @@ -1,28 +1,17 @@ import type { ColumnDef } from "@tanstack/react-table" -import { SearchIcon } from "lucide-react" +import { fetchMyRefundOrders } from "@/api/order" import AccountCell from "@/components/composites/accountCell" +import OrderFilterBar from "@/components/composites/orderFilterBar" 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 { useOrderFilters } from "@/lib/hooks/useOrderFilters" +import { MY_ORDER_PAGE_SIZE, useMyOrderPage } from "@/lib/hooks/useMyOrderPage" +import { useProducts } from "@/lib/hooks/useProducts" import { formatDate, formatMoney, type OrderItem, - PACKAGE_OPTIONS, totalConnections, } from "@/lib/models/order" -import { MOCK_ORDERS, MOCK_PRODUCTS } from "../mock/orders" - -// 后端订单接口就绪后,将 MOCK_ORDERS 替换为 fetchRefundOrders 返回的分页数据 const columns: ColumnDef[] = [ { @@ -53,9 +42,9 @@ const columns: ColumnDef[] = [ cell: ({ row }) => totalConnections(row.original), }, { - accessorKey: "DayPrice", + accessorKey: "DayCount", header: "退款时长", - cell: ({ row }) => formatMoney(row.original.DayPrice), + cell: ({ row }) => `${row.original.DayCount}天`, }, { accessorKey: "DayPrice", @@ -68,112 +57,38 @@ const columns: ColumnDef[] = [ cell: ({ row }) => formatMoney(row.original.PaymentAmount), }, { - accessorKey: "CouponAmount", + accessorKey: "RefundAmount", header: "退款金额", - cell: ({ row }) => formatMoney(row.original.CouponAmount), + cell: ({ row }) => formatMoney(row.original.RefundAmount), }, ] export default function RefundOrdersPage() { - const { - filters, - patch, - handleSearch, - page, - setPage, - pageSize, - handleSizeChange, - filtered, - paged, - } = useOrderFilters(MOCK_ORDERS) + const products = useProducts() + const { filters, patch, load, total, status, page, setPage, paged } = + useMyOrderPage(fetchMyRefundOrders) 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" - /> - -
-
+ void load()} + />

退货订单管理

diff --git a/src/admin/rosorder/page.tsx b/src/admin/rosorder/page.tsx index 2974460..7585a0f 100644 --- a/src/admin/rosorder/page.tsx +++ b/src/admin/rosorder/page.tsx @@ -1,51 +1,67 @@ +import type { ColumnDef } from "@tanstack/react-table" +import { fetchRosOrderList } from "@/api/rosorder" import DataTable from "@/components/data-table" import Page from "@/components/page" +import { useList } from "@/lib/hooks/useList" import type { RosOrderItem } from "@/lib/models/rosorder" -// TODO: 软路由订单接口就绪后接入(见 src/api/rosorder.ts fetchRosOrderList) +const columns: ColumnDef[] = [ + { + accessorKey: "username", + header: "用户", + }, + { + accessorKey: "product_name", + header: "软路由", + }, + { + accessorKey: "real_price", + header: "实付", + cell: ({ row }) => `¥${row.original.real_price}`, + }, + { + accessorKey: "order_no", + header: "订单编号", + }, + { + accessorKey: "user", + header: "收货人姓名", + }, + { + accessorKey: "phone", + header: "收货人电话", + }, + { + accessorKey: "address", + header: "收货人地址", + cell: ({ row }) => ( + + {row.original.address} + + ), + }, + { + accessorKey: "create_time", + header: "购买时间", + }, +] export default function RosorderPage() { + const { data, status } = useList(fetchRosOrderList) + return ( -
+

软路由订单列表

- - data={[]} - status="done" - columns={[ - { - accessorKey: "username", - header: "用户", - }, - { - accessorKey: "product_name", - header: "软路由", - }, - { - accessorKey: "real_price", - header: "实付", - }, - { - accessorKey: "order_no", - header: "订单编号", - }, - { - accessorKey: "user", - header: "收货人姓名", - }, - { - accessorKey: "phone", - header: "收货人电话", - }, - { - accessorKey: "address", - header: "收货人地址", - }, - { - accessorKey: "create_time", - header: "购买时间", - }, - ]} +
diff --git a/src/api/account.ts b/src/api/account.ts new file mode 100644 index 0000000..f7f3c51 --- /dev/null +++ b/src/api/account.ts @@ -0,0 +1,15 @@ +import type { ApiResponse } from "@/lib/api" +import type { MyAccountItem, MyAccountQueryParams } from "@/lib/models/account" +import { callPublic } from "./base" + +export async function fetchMyAccounts( + params: MyAccountQueryParams, +): Promise> { + const res = await callPublic( + "/user/ApiMyAccounts", + params, + "GET", + ) + if (!res.success) return res + return { success: true, data: Array.isArray(res.data) ? res.data : [] } +} diff --git a/src/api/cashOut.ts b/src/api/cashOut.ts index c0c8680..196bf49 100644 --- a/src/api/cashOut.ts +++ b/src/api/cashOut.ts @@ -1,25 +1,35 @@ import type { ApiResponse } from "@/lib/api" -import type { CashOutItem } from "@/lib/models/cashOut" -import { callByUser } from "./base" +import type { CashOutItem, CashOutSubmitData } from "@/lib/models/cashOut" +import { phpPost } from "./http" -// ==================== 提现管理 ==================== -// TODO: 接口路径待后端确认(旧后台为 php-api: /order/CashOutAdmin/*)。 +// ==================== 提现处理(php /order/CashOutIndex/*,cookie 认证) ==================== +// 接口来源:参考 juipnet 老项目 Host/Views/User/CashOut.cshtml、Index.cshtml -export async function fetchCashOutList( - params: Record, -): Promise> { - return await callByUser( - "/api/course/v1/order/CashOutList", - params, - "GET", +export async function fetchCashOutList(): Promise> { + const res = await phpPost>( + "/order/CashOutIndex/getData", + { cookie: document.cookie }, ) + if (!res.success) return res + const list = res.data?.data ?? res.data + return { + success: true, + data: Array.isArray(list) ? (list as CashOutItem[]) : [], + } } -export async function handleCashOut( - data: Record, -): Promise> { - return await callByUser( - "/api/course/v1/order/CashOutProcess", - data, +// 提交提现申请 +export async function submitCashOut( + data: CashOutSubmitData, +): Promise> { + const res = await phpPost>( + "/order/CashOutIndex/index", + { ...data, cookie: document.cookie }, ) + if (!res.success) return res + const ok = Number(res.data?.Code ?? res.data?.code ?? 0) === 10000 + return { + success: true, + data: ok, + } } diff --git a/src/api/channel.ts b/src/api/channel.ts deleted file mode 100644 index d24176e..0000000 --- a/src/api/channel.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ApiResponse } from "@/lib/api" -import type { ChannelRecord } from "@/lib/models/channel" -import { callByUser } from "./base" - -// ==================== 通道/账号列表(.NET /api/course/v1/productaccount/*) ==================== - -export async function fetchChannelPage( - params: Record, -): Promise> { - return await callByUser( - "/api/course/v1/productaccount/Page", - params, - "GET", - ) -} diff --git a/src/api/coupon.ts b/src/api/coupon.ts index 824d760..0f782aa 100644 --- a/src/api/coupon.ts +++ b/src/api/coupon.ts @@ -1,8 +1,10 @@ import type { ApiResponse } from "@/lib/api" -import type { CouponItem, CouponQueryParams } from "@/lib/models/coupon" -import { callByUser } from "./base" - -// ==================== 优惠券(.NET /api/sells/v1/coupon/*) ==================== +import type { + CouponItem, + CouponQueryParams, + MyCouponItem, +} from "@/lib/models/coupon" +import { callByUser, callPublic } from "./base" export async function fetchCouponPage( params: CouponQueryParams, @@ -33,3 +35,13 @@ export async function giveCoupon( ): Promise> { return await callByUser("/api/sells/v1/coupon/Give", data) } + +export async function fetchMyCoupons(): Promise> { + const res = await callPublic( + "/user/ApiMyCoupons", + undefined, + "GET", + ) + if (!res.success) return res + return { success: true, data: Array.isArray(res.data) ? res.data : [] } +} diff --git a/src/api/funds.ts b/src/api/funds.ts new file mode 100644 index 0000000..1138c59 --- /dev/null +++ b/src/api/funds.ts @@ -0,0 +1,14 @@ +import type { ApiResponse } from "@/lib/api" +import type { ScoreItem } from "@/lib/models/funds" +import { phpPost } from "./http" + +export async function fetchScoreList( + page: number, +): Promise> { + const res = await phpPost("/user/UserScore/getList", { + cookie: document.cookie, + page, + }) + if (!res.success) return res + return { success: true, data: Array.isArray(res.data) ? res.data : [] } +} diff --git a/src/api/order.ts b/src/api/order.ts index 94d6cda..b02144f 100644 --- a/src/api/order.ts +++ b/src/api/order.ts @@ -3,10 +3,12 @@ import type { CreateOrderData, CreateOrderRequest, CreateTestAccountRequest, + MyOrderPage, + MyOrderQueryParams, OrderItem, OrderQueryParams, } from "@/lib/models/order" -import { callByUser } from "./base" +import { callByUser, callPublic } from "./base" // ==================== 购买下单(php-api /product/*) ==================== @@ -105,3 +107,74 @@ export async function exportOrders( "GET", ) } + +async function fetchMyOrderPage( + endpoint: string, + params: MyOrderQueryParams, +): Promise> { + const res = await callPublic( + endpoint, + params, + "GET", + ) + if (!res.success) return res + + const data = res.data + const raw = Array.isArray(data) ? data[0] : data + const page = raw && typeof raw === "object" ? (raw as MyOrderPage) : null + return { + success: true, + data: { + RowCount: Number(page?.RowCount ?? 0), + List: Array.isArray(page?.List) ? page.List : [], + }, + } +} + +// 我的订单(订单管理) +export async function fetchMyOrders( + params: MyOrderQueryParams, +): Promise> { + return fetchMyOrderPage("/user/ApiMyOrders", params) +} + +// 我的退货订单 +export async function fetchMyRefundOrders( + params: MyOrderQueryParams, +): Promise> { + return fetchMyOrderPage("/user/ApiMyRefundOrders", params) +} + +export type RechargeOrderResult = { + OrderInfo: { + OrderNo: string + CreateTime: string + PaymentAmount?: number + PayChannel?: number + } + PayData: string +} + +// 创建充值订单:PayChannel 50-支付宝 30-微信 +export async function createRechargeOrder(data: { + ChargeAmount: number + PayChannel: number +}): Promise> { + return await callPublic( + "/user/CreateOrder", + data, + ) +} + +// 轮询充值支付状态:返回 true 表示支付成功 +export async function checkRechargeStatus( + orderNo: string, +): Promise> { + const res = await callPublic( + `/user/IsPay?orderNo=${encodeURIComponent(orderNo)}`, + undefined, + "GET", + ) + if (!res.success) return res + return { success: true, data: res.data === 1 } +} diff --git a/src/api/product.ts b/src/api/product.ts index fa98710..480ed3b 100644 --- a/src/api/product.ts +++ b/src/api/product.ts @@ -29,3 +29,14 @@ export async function fetchWindowProducts(): Promise< "GET", ) } + +// 产品+套餐(用于账号管理筛选,接口来源:juipnet MyAccounts.cshtml) +export async function fetchProductWithPackage(): Promise< + ApiResponse +> { + return await callPublic( + "/api/course/v1/product/ProductWithPackage", + undefined, + "GET", + ) +} diff --git a/src/api/rosorder.ts b/src/api/rosorder.ts index d29b834..fb989a0 100644 --- a/src/api/rosorder.ts +++ b/src/api/rosorder.ts @@ -1,16 +1,17 @@ import type { ApiResponse } from "@/lib/api" import type { RosOrderItem } from "@/lib/models/rosorder" -import { callByUser } from "./base" +import { phpPost } from "./http" -// ==================== 软路由订单 ==================== -// TODO: 接口路径待后端确认(旧后台为 /course/v1/order/ros 类接口)。 - -export async function fetchRosOrderList( - params: Record, -): Promise> { - return await callByUser( - "/api/course/v1/order/RosOrders", - params, - "GET", - ) +export async function fetchRosOrderList(): Promise< + ApiResponse +> { + const res = await phpPost>("/ros/order/getList", { + cookie: document.cookie, + }) + if (!res.success) return res + const list = res.data?.data ?? res.data + return { + success: true, + data: Array.isArray(list) ? (list as RosOrderItem[]) : [], + } } diff --git a/src/api/user.ts b/src/api/user.ts index 2324520..775bcfa 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -45,17 +45,15 @@ export async function updateUserProfile( // ==================== 个人中心资料/密码(.NET /user/*,cookie 会话) ==================== -// 编辑资料:老项目 /user/UpdateInfo(QQ/Wx/TaoBao/Email) export async function updateUserInfo(data: { QQ: string Wx: string - TaoBao: string + // TaoBao: string Email: string }): Promise> { return await callPublic("/user/UpdateInfo", data) } -// 修改密码:老项目 /user/UpdatePwd(OldPwd/NewPwd/ConfirmPwd) export async function updateUserPwd(data: { OldPwd: string NewPwd: string diff --git a/src/components/composites/dialogs/AnnouncementModal.tsx b/src/components/composites/dialogs/AnnouncementModal.tsx index c9ec4ae..4aedcc4 100644 --- a/src/components/composites/dialogs/AnnouncementModal.tsx +++ b/src/components/composites/dialogs/AnnouncementModal.tsx @@ -1,3 +1,4 @@ +import { useNavigate } from "react-router-dom" import { Button } from "@/components/ui/button" import { Dialog, @@ -7,6 +8,11 @@ import { DialogTitle, } from "@/components/ui/dialog" +// 参考 juipnet Host/Views/Home/Index.cshtml 公告弹窗: +// 查看活动 -> /product 产品页;联系客服 -> 企点QQ客服二维码页 +const CUSTOMER_SERVICE_URL = + "https://admin.qidian.qq.com/template/blue/mp/menu/qr-code-jump.html?linkType=0&env=ol&kfuin=2852138148&fid=299&key=f377ec024ca45115a03a7632c7bda230&cate=1&source=&isLBS=&isCustomEntry=&type=16&ftype=1&_type=wpa&qidian=true&_pid=l1gunh.657dox.kjy6xnny" + interface AnnouncementModalProps { open: boolean onOpenChange: (open: boolean) => void @@ -16,6 +22,18 @@ export default function AnnouncementModal({ open, onOpenChange, }: AnnouncementModalProps) { + const navigate = useNavigate() + + const handleViewActivity = () => { + onOpenChange(false) + navigate("/product") + } + + const handleContactService = () => { + onOpenChange(false) + window.open(CUSTOMER_SERVICE_URL, "_blank", "noopener,noreferrer") + } + return ( - - diff --git a/src/components/composites/dialogs/cashOutDialog.tsx b/src/components/composites/dialogs/cashOutDialog.tsx new file mode 100644 index 0000000..685fd0a --- /dev/null +++ b/src/components/composites/dialogs/cashOutDialog.tsx @@ -0,0 +1,212 @@ +import { useEffect, useState } from "react" +import { toast } from "sonner" +import { submitCashOut } from "@/api/cashOut" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" + +const ALIPAY_ACCOUNT_RE = + /^(?:1[3-9]\d{9}|[a-zA-Z\d._-]*@[a-zA-Z\d.-]{1,10}\.[a-zA-Z\d]{1,20})$/ + + +const CUSTOMER_SERVICE_URL = + "https://admin.qidian.qq.com/template/blue/mp/menu/qr-code-jump.html?linkType=0&env=ol&kfuin=2852138148&fid=299&key=f377ec024ca45115a03a7632c7bda230&cate=1&source=&isLBS=&isCustomEntry=&type=16&ftype=1&_type=wpa&qidian=true" + +interface CashOutDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + balance: number + onSubmitted?: () => void +} + +export function CashOutDialog({ + open, + onOpenChange, + balance, + onSubmitted, +}: CashOutDialogProps) { + const [step, setStep] = useState<"notice" | "form">("notice") + const [money, setMoney] = useState("") + const [reason, setReason] = useState("") + const [alipayAccount, setAlipayAccount] = useState("") + const [realName, setRealName] = useState("") + const [submitting, setSubmitting] = useState(false) + + useEffect(() => { + if (!open) { + setSubmitting(false) + // setStep("notice") + } + }, [open]) + + const handleSubmit = async () => { + const cashOutMoney = Number(money) + if (!Number.isFinite(cashOutMoney) || cashOutMoney <= 0) { + toast.error("抱歉!请重新输入提现金额!") + return + } + if (cashOutMoney > balance) { + toast.error("抱歉!您提现的额度大于余额,请重新输入提现金额!") + return + } + if (!realName.trim()) { + toast.error("抱歉!请重新输入姓名!") + return + } + if ( + !alipayAccount.trim() || + !ALIPAY_ACCOUNT_RE.test(alipayAccount.trim()) + ) { + toast.error("抱歉!请重新输入支付宝账号!") + return + } + + setSubmitting(true) + const res = await submitCashOut({ + cash_out_money: money, + reason: reason.trim(), + alipay_account: alipayAccount.trim(), + real_name: realName.trim(), + }) + setSubmitting(false) + + if (!res.success) { + toast.error(res.message || "提现异常!请重试或联系客服。") + return + } + if (!res.data) { + toast.error("提现异常!请重试或联系客服。") + return + } + toast.success( + "提现请求发送成功,我们会在5到7个工作日进行处理,请注意查看资金明细。", + ) + setMoney("") + setReason("") + setAlipayAccount("") + setRealName("") + onOpenChange(false) + onSubmitted?.() + } + + return ( + + + {step === "notice" ? ( + <> + + + 余额提现 + + + +
+

聚IP涵盖十余种全网知名旗舰产品,总有一款适合您!

+

联系客服帮您推荐合适的产品,我们将最大努力满足您的需求!

+
+ +
+ + +
+ + ) : ( + <> + + + 余额提现 + + + +
+

+ 当前可用余额: + + ¥{balance.toFixed(2)} + +

+ +
+ + setMoney(e.target.value)} + className="h-11 bg-gray-50 border-gray-200" + placeholder="请输入提现金额" + /> +
+ +
+ + setReason(e.target.value)} + className="h-11 bg-gray-50 border-gray-200" + placeholder="请输入提现理由" + /> +
+ +
+ + setAlipayAccount(e.target.value)} + className="h-11 bg-gray-50 border-gray-200" + placeholder="请输入支付宝账号" + /> +
+ +
+ + setRealName(e.target.value)} + className="h-11 bg-gray-50 border-gray-200" + placeholder="请输入与支付宝关联的真实姓名" + /> +
+ + +
+ + )} +
+
+ ) +} diff --git a/src/components/composites/dialogs/editInfoDialog.tsx b/src/components/composites/dialogs/editInfoDialog.tsx index 810aeaf..40ac808 100644 --- a/src/components/composites/dialogs/editInfoDialog.tsx +++ b/src/components/composites/dialogs/editInfoDialog.tsx @@ -18,14 +18,14 @@ import { Label } from "@/components/ui/label" export type EditInfoValues = { qq: string wechat: string - taobao: string + // taobao: string email: string } const editInfoSchema = z.object({ qq: z.string(), wechat: z.string(), - taobao: z.string(), + // taobao: z.string(), email: z .string() .refine( @@ -44,7 +44,7 @@ interface EditInfoDialogProps { const DEFAULT_INITIAL_DATA: EditInfoValues = { qq: "", wechat: "", - taobao: "", + // taobao: "", email: "", } @@ -72,7 +72,7 @@ export function EditInfoDialog({ const res = await updateUserInfo({ QQ: data.qq, Wx: data.wechat, - TaoBao: data.taobao, + // TaoBao: data.taobao, Email: data.email, }) setSubmitting(false) @@ -118,7 +118,7 @@ export function EditInfoDialog({ />
-
+ {/*
)} /> -
+
*/}
diff --git a/src/components/composites/dialogs/rechargeDialog.tsx b/src/components/composites/dialogs/rechargeDialog.tsx new file mode 100644 index 0000000..d44b7b1 --- /dev/null +++ b/src/components/composites/dialogs/rechargeDialog.tsx @@ -0,0 +1,112 @@ +import { useCallback, useEffect, useState } from "react" +import { toast } from "sonner" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import alipay from "@/home/_assets/alipay.svg" +import wechat from "@/home/_assets/wechat.svg" + +type PayChannel = "50" | "30" + +interface RechargeDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function RechargeDialog({ open, onOpenChange }: RechargeDialogProps) { + const [amount, setAmount] = useState("100") + const [payChannel, setPayChannel] = useState("50") + + const reset = useCallback(() => { + setAmount("100") + setPayChannel("50") + }, []) + + useEffect(() => { + if (!open) reset() + }, [open, reset]) + + const handleGoPay = () => { + const chargeAmount = Number(amount) + if (!Number.isFinite(chargeAmount) || chargeAmount <= 0) { + toast.error("请输入正确的充值金额") + return + } + + window.open( + `/pay?amount=${chargeAmount}&channel=${payChannel}`, + "_blank", + "noopener,noreferrer", + ) + onOpenChange(false) + } + + return ( + + + + 账户充值 + + +
+
+ + setAmount(e.target.value)} + className="h-11 bg-gray-50 border-gray-200" + placeholder="请输入充值金额" + /> +
+ +
+ + setPayChannel(value as PayChannel)} + className="flex gap-6" + > +
+ + +
+
+ + +
+
+
+ + +
+
+
+ ) +} diff --git a/src/components/composites/orderFilterBar.tsx b/src/components/composites/orderFilterBar.tsx new file mode 100644 index 0000000..178197a --- /dev/null +++ b/src/components/composites/orderFilterBar.tsx @@ -0,0 +1,121 @@ +import { SearchIcon } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + ORDER_TYPE_LABELS, + type OrderFilters, + PACKAGE_OPTIONS, +} from "@/lib/models/order" +import type { ProductItem } from "@/lib/models/product" + +// 订单管理/退货订单共用的筛选栏 +export default function OrderFilterBar(props: { + filters: OrderFilters + products: ProductItem[] + showOrderType?: boolean + onPatch: (patch: Partial) => void + onSearch: () => void +}) { + const { filters, products, showOrderType = true, onPatch, onSearch } = props + + return ( +
+
+ + 日期筛选: + + onPatch({ bTime: e.target.value })} + className="w-36" + aria-label="开始时间" + /> + 至 + onPatch({ eTime: e.target.value })} + className="w-36" + aria-label="结束时间" + /> +
+ + {showOrderType && ( + + )} + + + + + +
+ onPatch({ keyword: e.target.value })} + onKeyDown={e => { + if (e.key === "Enter") onSearch() + }} + placeholder="账号" + className="w-40" + /> + +
+
+ ) +} diff --git a/src/components/stores/client.ts b/src/components/stores/client.ts index f7da7f5..b9e384a 100644 --- a/src/components/stores/client.ts +++ b/src/components/stores/client.ts @@ -1,6 +1,5 @@ import { create } from "zustand" - type ClientState = { breakpoint: { sm: boolean diff --git a/src/components/stores/layout.ts b/src/components/stores/layout.ts index 5d657d1..07a71e5 100644 --- a/src/components/stores/layout.ts +++ b/src/components/stores/layout.ts @@ -1,7 +1,5 @@ import { create } from "zustand" - - type LayoutState = { navbar: boolean } diff --git a/src/components/stores/profile.ts b/src/components/stores/profile.ts index 974eb36..2a652f2 100644 --- a/src/components/stores/profile.ts +++ b/src/components/stores/profile.ts @@ -6,7 +6,6 @@ import { type UserInfoRecord, } from "@/api/token" - export type ProfileState = { token: string | null userInfo: UserInfoRecord | null diff --git a/src/home/pay/index.tsx b/src/home/pay/index.tsx new file mode 100644 index 0000000..875f6ca --- /dev/null +++ b/src/home/pay/index.tsx @@ -0,0 +1,394 @@ +import { CheckCircle2, Loader2, XCircle } from "lucide-react" +import { useCallback, useEffect, useState } from "react" +import { useSearchParams } from "react-router-dom" +import { + checkPayStatus, + checkRechargeStatus, + createRechargeOrder, + type RechargeOrderResult, +} from "@/api/order" +import { QrCodeImage } from "@/components/qrCodeImage" +import { Button } from "@/components/ui/button" +import { clearOrderPay, notifyPayPaid, readOrderPay } from "@/lib/recharge" +import alipay from "../_assets/alipay.svg" +import wechat from "../_assets/wechat.svg" + +const POLL_INTERVAL = 3000 + +// 收银台(独立窗口): +// - 充值模式 /pay?amount=100&channel=30:页面内下单 /user/CreateOrder,轮询 /user/IsPay +// - 购买模式 /pay?mode=order:主页面已下单,读取 localStorage 支付数据,轮询 /product/IsPay +// 参考 juipnet Host/Views/User/Index.cshtml(charge)与 Product/buy.cshtml(payCallback/isPay) + +type PollState = "paid" | "pending" | "error" + +function usePayPolling( + orderNo: string | undefined, + checkPaid: (orderNo: string) => Promise, +) { + const [paid, setPaid] = useState(false) + const [failCount, setFailCount] = useState(0) + + useEffect(() => { + if (!orderNo) return + let stopped = false + + const poll = async () => { + const state = await checkPaid(orderNo) + if (stopped) return + if (state === "paid") { + setPaid(true) + notifyPayPaid(orderNo) + } else if (state === "error") { + setFailCount(count => count + 1) + } + } + + void poll() + const timer = window.setInterval(() => void poll(), POLL_INTERVAL) + return () => { + stopped = true + window.clearInterval(timer) + } + }, [orderNo, checkPaid]) + + return { paid, failCount } +} + +export default function PayPage() { + const [searchParams] = useSearchParams() + const mode = searchParams.get("mode") + + return mode === "order" ? : +} + +// ==================== 充值模式 ==================== + +function RechargePay() { + const [searchParams] = useSearchParams() + const amountParam = Number(searchParams.get("amount")) + const channelParam = searchParams.get("channel") + + const valid = + Number.isFinite(amountParam) && + amountParam > 0 && + (channelParam === "50" || channelParam === "30") + + const [order, setOrder] = useState(null) + const [orderError, setOrderError] = useState("") + const [autoOpened, setAutoOpened] = useState(false) + + useEffect(() => { + if (!valid) return + let cancelled = false + createRechargeOrder({ + ChargeAmount: amountParam, + PayChannel: Number(channelParam), + }).then(res => { + if (cancelled) return + if (res.success) { + setOrder(res.data) + } else { + setOrderError(res.message) + } + }) + return () => { + cancelled = true + } + }, [valid, amountParam, channelParam]) + + useEffect(() => { + if (!order?.PayData || autoOpened) return + + // 支付宝:PayData 是支付链接或自动提交表单,直接跳转; + // 微信:交给 CashierLayout 判断(链接/表单跳转,weixin:// 扫码内容显示二维码) + if (channelParam !== "50") return + + setAutoOpened(true) + const payData = order.PayData + if (/^https?:\/\//i.test(payData)) { + window.location.href = payData + } else { + document.open() + document.write(payData) + document.close() + } + }, [order, channelParam, autoOpened]) + + const checkPaid = useCallback(async (orderNo: string): Promise => { + const res = await checkRechargeStatus(orderNo) + if (!res.success) return "error" + return res.data ? "paid" : "pending" + }, []) + + const { paid, failCount } = usePayPolling(order?.OrderInfo.OrderNo, checkPaid) + + const isAlipay = channelParam === "50" + + if (isAlipay) { + return ( +
+
+ {orderError ? ( + <> + +

{orderError}

+

请回到原页面重新发起

+ + ) : !order ? ( + <> + +

正在生成订单...

+ + ) : paid ? ( + <> + +

支付成功

+

+ 订单已生效,可以关闭本页面 +

+ + ) : ( + <> + +

正在跳转支付宝...

+

+ 如未自动跳转,请检查浏览器是否拦截 +

+ + )} +
+
+ ) + } + + return ( + + ) +} +// ==================== 购买模式 ==================== + +function OrderPay() { + const [pay] = useState(readOrderPay) + + const checkPaid = useCallback(async (orderNo: string): Promise => { + const res = await checkPayStatus(orderNo) + if (!res.success) return "error" + return res.data === 1 ? "paid" : "pending" + }, []) + + const { paid, failCount } = usePayPolling(pay?.orderNo, checkPaid) + + useEffect(() => { + if (paid) clearOrderPay() + }, [paid]) + + return ( + + ) +} + +// ==================== 收银台界面 ==================== + +function CashierLayout(props: { + isWechat: boolean + amount: number + orderNo?: string + payData: string + paid: boolean + loading: boolean + error: string + failCount: number + invalid: boolean +}) { + const { + isWechat, + amount, + orderNo, + payData, + paid, + loading, + error, + failCount, + invalid, + } = props + + const payDataIsUrl = /^https?:\/\//i.test(payData) + const payDataIsHtml = /^\s* { + if (!wechatShouldRedirect || paid || error || loading || invalid) return + if (payDataIsUrl) { + window.location.href = payData + } else { + document.open() + document.write(payData) + document.close() + } + }, [ + wechatShouldRedirect, + payDataIsUrl, + payData, + paid, + error, + loading, + invalid, + ]) + + if (invalid) { + return ( +
+
+ +

支付信息已失效,请重新发起

+ +
+
+ ) + } + + const openAlipayWindow = () => { + if (/^https?:\/\//i.test(payData)) { + window.open(payData, "_blank", "noopener,noreferrer") + return + } + const win = window.open("", "_blank") + if (win) { + win.document.open() + win.document.write(payData) + win.document.close() + } + } + + return ( +
+
+
+ {isWechat +

+ {isWechat ? "微信支付" : "支付宝"} | 收银台 +

+
+ +
+
+ 应付金额 + + ¥{Number(amount).toFixed(2)} + +
+
+ 收款方 + 聚IP +
+
+ 订单号 + + {orderNo ?? "--"} + +
+
+ + {paid ? ( +
+ +

支付成功

+

订单已生效,可以关闭本页面

+
+ ) : error ? ( +
+ +

{error}

+

请回到原页面重新发起

+
+ ) : loading ? ( +
+ +

正在生成订单...

+
+ ) : isWechat ? ( + wechatShouldRedirect ? ( +
+ +

正在跳转微信支付...

+

+ 如未自动跳转,请检查浏览器是否拦截 +

+ +
+ ) : ( +
+ {payData ? ( + <> +
+ +
+

+ 请使用微信扫码支付,支付完成后本页将自动更新 +

+ + ) : ( + <> + +

+ 未获取到支付二维码,请回到原页面重新发起 +

+ + )} + +
+ ) + ) : ( +
+ +

+ 请在打开的页面完成支付,完成后回到本页等待自动确认 +

+ +
+ )} +
+
+ ) +} + +function PollingHint({ failCount }: { failCount: number }) { + return ( +
+ + 正在等待支付结果 + {failCount > 2 && "(网络波动,请稍候)"} +
+ ) +} diff --git a/src/home/product/buy.tsx b/src/home/product/buy.tsx index 7c1478c..e360731 100644 --- a/src/home/product/buy.tsx +++ b/src/home/product/buy.tsx @@ -16,6 +16,7 @@ import { import Wrap from "@/components/wrap" import type { CreateOrderRequest } from "@/lib/models/order" import type { Product } from "@/lib/models/product" +import { clearPayPaid, PAY_PAID_KEY, saveOrderPay } from "@/lib/recharge" import { cn } from "@/lib/utils" import alipay from "../_assets/alipay.svg" import balance from "../_assets/balance.svg" @@ -221,6 +222,18 @@ export default function BuyPage() { const [payType, setPayType] = useState("100") const [submitting, setSubmitting] = useState(false) + // 支付窗口完成后自动刷新并跳转订单管理 + useEffect(() => { + const handler = (e: StorageEvent) => { + if (e.key !== PAY_PAID_KEY || !e.newValue) return + toast.success("支付成功") + clearPayPaid() + navigate("/admin/orders") + } + window.addEventListener("storage", handler) + return () => window.removeEventListener("storage", handler) + }, [navigate]) + useEffect(() => { if (!product) { navigate("/product", { replace: true }) @@ -324,11 +337,40 @@ export default function BuyPage() { const result = await createOrder(params) - if (result.success) { - toast.success("购买成功!") - } else { + if (!result.success) { toast.error(result.message) + return } + + // 参考 juipnet Product/buy.cshtml: + // Data == "00" 为余额支付直接成功;否则需要微信/支付宝支付 + const data = result.data + if (typeof data === "string") { + if (data === "00") { + toast.success("购买成功!") + } else { + toast.error(data || "下单失败,请重试") + } + return + } + + saveOrderPay({ + orderNo: data.OrderInfo.OrderNo, + amount: Number(data.OrderInfo.OtherPayAmount) || total, + payChannel, + payData: data.PayData, + }) + const win = window.open( + "/pay?mode=order", + "_blank", + "noopener,noreferrer", + ) + if (!win) { + // 弹窗被拦截时直接在当前页跳转收银台 + navigate("/pay?mode=order") + return + } + toast.success("订单已生成,请在新窗口中完成支付") } catch { toast.error("网络错误,请重试") } finally { diff --git a/src/home/product/buyStatic.tsx b/src/home/product/buyStatic.tsx index 32150ba..dd9f22c 100644 --- a/src/home/product/buyStatic.tsx +++ b/src/home/product/buyStatic.tsx @@ -1,10 +1,14 @@ import { useEffect, useState } from "react" import { useLocation, useNavigate } from "react-router-dom" +import { toast } from "sonner" +import { createOrder } from "@/api/order" import { Button } from "@/components/ui/button" 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 { CreateOrderRequest } from "@/lib/models/order" +import { clearPayPaid, PAY_PAID_KEY, saveOrderPay } from "@/lib/recharge" import { cn } from "@/lib/utils" import alipay from "../_assets/alipay.svg" import balance from "../_assets/balance.svg" @@ -29,7 +33,8 @@ function randomDigits(length: number): string { } type BuyStaticState = { - pool?: { id?: string; title?: string } + // packageId:待后端静态池套餐数据就绪后由 ShuidiPools 传入 + pool?: { id?: string; title?: string; packageId?: number } } // ============ 独享类型切换 Tab ============ @@ -273,14 +278,100 @@ export default function BuyStaticPage() { const [bandwidth, setBandwidth] = useState("2M") const [duration, setDuration] = useState("test") const [payType, setPayType] = useState("100") + const [submitting, setSubmitting] = useState(false) useEffect(() => { setAccount(randomChars(2) + randomDigits(4)) setPwd(randomDigits(3)) }, []) + // 支付窗口完成后自动跳转订单管理(与 buy.tsx 一致) + useEffect(() => { + const handler = (e: StorageEvent) => { + if (e.key !== PAY_PAID_KEY || !e.newValue) return + toast.success("支付成功") + clearPayPaid() + navigate("/admin/orders") + } + window.addEventListener("storage", handler) + return () => window.removeEventListener("storage", handler) + }, [navigate]) + const handleBack = () => navigate(-1) + // 确认支付:框架已就绪,套餐 ID(pool.packageId)待后端提供后即可真实下单 + const handlePay = async () => { + const packageId = state.pool?.packageId + if (!packageId) { + toast.info("该产品暂未开放线上购买,请联系客服购买") + return + } + if (!account.trim() || !pwd.trim()) { + toast.error("账号和密码不能为空") + return + } + if (type === "project" && project === "请选择") { + toast.error("请选择项目") + return + } + if (!region) { + toast.error("请选择地区") + return + } + + setSubmitting(true) + try { + const oPayType = Number(payType) + const params: CreateOrderRequest = { + PackageId: packageId, + OrderType: 1, + Account: account.trim(), + Pwd: pwd.trim(), + ConnectCount: 1, + CouponId: 0, + UseAccountAmount: oPayType === 1 ? 1 : 0, + OPayType: oPayType, + PayChannel: oPayType === 70 ? 30 : 50, + Price: Number(getPrice()), + } + + const result = await createOrder(params) + if (!result.success) { + toast.error(result.message) + return + } + + const data = result.data + if (typeof data === "string") { + toast[data === "00" ? "success" : "error"]( + data === "00" ? "购买成功!" : data || "下单失败,请重试", + ) + return + } + + saveOrderPay({ + orderNo: data.OrderInfo.OrderNo, + amount: Number(data.OrderInfo.OtherPayAmount) || Number(getPrice()), + payChannel: oPayType === 70 ? 30 : 50, + payData: data.PayData, + }) + const win = window.open( + "/pay?mode=order", + "_blank", + "noopener,noreferrer", + ) + if (!win) { + navigate("/pay?mode=order") + return + } + toast.success("订单已生成,请在新窗口中完成支付") + } catch { + toast.error("网络错误,请重试") + } finally { + setSubmitting(false) + } + } + // 根据类型计算价格 const getPrice = () => { const bwMap: Record = { @@ -523,8 +614,12 @@ export default function BuyStaticPage() { > 上一步 -
diff --git a/src/lib/hooks/useList.ts b/src/lib/hooks/useList.ts new file mode 100644 index 0000000..720a956 --- /dev/null +++ b/src/lib/hooks/useList.ts @@ -0,0 +1,30 @@ +import { useEffect, useState } from "react" +import { toast } from "sonner" +import type { ApiResponse } from "@/lib/api" + +export type ListStatus = "load" | "done" | "fail" + +// 挂载时加载一次列表(rosorder / cashOut 等无筛选列表页共用) +export function useList(fetcher: () => Promise>) { + const [data, setData] = useState([]) + const [status, setStatus] = useState("load") + + useEffect(() => { + let cancelled = false + fetcher().then(res => { + if (cancelled) return + if (res.success) { + setData(res.data) + setStatus("done") + } else { + setStatus("fail") + toast.error(res.message) + } + }) + return () => { + cancelled = true + } + }, [fetcher]) + + return { data, status } +} diff --git a/src/lib/hooks/useMyOrderPage.ts b/src/lib/hooks/useMyOrderPage.ts new file mode 100644 index 0000000..2be2c61 --- /dev/null +++ b/src/lib/hooks/useMyOrderPage.ts @@ -0,0 +1,70 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { toast } from "sonner" +import type { ApiResponse } from "@/lib/api" +import { + DEFAULT_FILTERS, + type MyOrderPage, + type MyOrderQueryParams, + type OrderFilters, + type OrderItem, +} from "@/lib/models/order" +import type { ListStatus } from "./useList" + +const PAGE_SIZE = 10 + +// 订单管理/退货订单共用的筛选+分页逻辑(.NET /user/ApiMyOrders、ApiMyRefundOrders) +export function useMyOrderPage( + fetchPage: (params: MyOrderQueryParams) => Promise>, +) { + const [filters, setFilters] = useState(DEFAULT_FILTERS) + const [data, setData] = useState([]) + const [total, setTotal] = useState(0) + const [status, setStatus] = useState("load") + const [page, setPage] = useState(1) + + const patch = useCallback( + (partial: Partial) => + setFilters(prev => ({ ...prev, ...partial })), + [], + ) + + const load = useCallback(async () => { + setStatus("load") + const res = await fetchPage({ + BTime: filters.bTime || undefined, + ETime: filters.eTime || undefined, + OrderType: + filters.orderType === "0" ? undefined : Number(filters.orderType), + ProductId: + filters.productId === "0" ? undefined : Number(filters.productId), + PackageId: filters.packageName === "0" ? undefined : filters.packageName, + KeyWord: filters.keyword.trim() || undefined, + }) + if (res.success) { + setData(res.data.List) + setTotal(res.data.RowCount) + setStatus("done") + } else { + setStatus("fail") + toast.error(res.message) + } + setPage(1) + }, [fetchPage, filters]) + + const loadedRef = useRef(false) + useEffect(() => { + if (loadedRef.current) return + loadedRef.current = true + void load() + }, [load]) + + // 接口返回全量列表(含 RowCount),本地做前端分页 + const paged = useMemo( + () => data.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE), + [data, page], + ) + + return { filters, patch, load, data, total, status, page, setPage, paged } +} + +export { PAGE_SIZE as MY_ORDER_PAGE_SIZE } diff --git a/src/lib/hooks/useOrderFilters.ts b/src/lib/hooks/useOrderFilters.ts deleted file mode 100644 index efa0c73..0000000 --- a/src/lib/hooks/useOrderFilters.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useMemo, useState } from "react" -import { - DEFAULT_FILTERS, - matchOrders, - type OrderFilters, - type OrderItem, -} from "@/lib/models/order" - -// 订单列表通用筛选/分页逻辑(orders / refundOrders 页面共用) -export function useOrderFilters(list: OrderItem[]) { - 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(list, appliedFilters), - [list, 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) - } - - const handleSizeChange = (size: number) => { - setPageSize(size) - setPage(1) - } - - return { - filters, - patch, - handleSearch, - page, - setPage, - pageSize, - handleSizeChange, - filtered, - paged, - } -} diff --git a/src/lib/hooks/useProducts.ts b/src/lib/hooks/useProducts.ts new file mode 100644 index 0000000..59ccb9a --- /dev/null +++ b/src/lib/hooks/useProducts.ts @@ -0,0 +1,26 @@ +import { useEffect, useState } from "react" +import { toast } from "sonner" +import { fetchProductWithPackage } from "@/api/product" +import type { ProductItem } from "@/lib/models/product" + +// 加载「产品+套餐」下拉数据(账号管理/订单筛选共用) +export function useProducts() { + const [products, setProducts] = useState([]) + + useEffect(() => { + let cancelled = false + fetchProductWithPackage().then(res => { + if (cancelled) return + if (res.success) { + setProducts(res.data) + } else { + toast.error(res.message) + } + }) + return () => { + cancelled = true + } + }, []) + + return products +} diff --git a/src/lib/models/account.ts b/src/lib/models/account.ts new file mode 100644 index 0000000..22da4a9 --- /dev/null +++ b/src/lib/models/account.ts @@ -0,0 +1,46 @@ +export type MyAccountItem = { + TenantId: number + UserId: number + ProductId: number + PackageId: number + UserCode: string + ProductName: string + PackageName: string + AccountType: number + Account: string + Pwd: string + ConnectCount: number + Status: number + ChargeStatus: number + StartTime: string + EndTime: string + RestTime: string + RestDay: number + agent_id: number + im: number + CreateTime: string + UpdateTime: string + DeleteTag: number + Id: number +} + +export type MyAccountQueryParams = { + ExpiredDay?: number + ProductId?: number + PackageId?: string + Keyword?: string +} + +// 到期时间筛选枚举(-100:全部) +export const EXPIRED_DAY_OPTIONS: { value: number; label: string }[] = [ + { value: -100, label: "全部" }, + { value: 1, label: "1天内到期" }, + { value: 3, label: "三天内到期" }, + { value: 7, label: "一周内到期" }, + { value: 10, label: "10天内到期" }, + { value: -1, label: "已经到期1天内" }, + { value: -3, label: "已经到期3天内" }, + { value: -4, label: "已经到期3天以上" }, + { value: -5, label: "全部未过期" }, + { value: -6, label: "全部已过期" }, +] diff --git a/src/lib/models/cashOut.ts b/src/lib/models/cashOut.ts index 71442f9..4e543c7 100644 --- a/src/lib/models/cashOut.ts +++ b/src/lib/models/cashOut.ts @@ -1,10 +1,16 @@ export type CashOutItem = { - id: number username: string - money: number - reason: string + money: string + remark: string | null + apply_reason: string alipay_account: string - remark: string status: string create_time: string } + +export type CashOutSubmitData = { + cash_out_money: string + reason: string + alipay_account: string + real_name: string +} diff --git a/src/lib/models/channel.ts b/src/lib/models/channel.ts deleted file mode 100644 index 61939fb..0000000 --- a/src/lib/models/channel.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type ChannelRecord = { - id: string - startAt: string - product: string - plan: string - account: string - password: string - ip: string - server: string - connections: number - expiredAt: string - remaining: string - online: string - download: string - status: string -} diff --git a/src/lib/models/coupon.ts b/src/lib/models/coupon.ts index 1dda510..3b13dc4 100644 --- a/src/lib/models/coupon.ts +++ b/src/lib/models/coupon.ts @@ -1,4 +1,3 @@ -// 优惠券(.NET /api/sells/v1/coupon/*,对应 CouponEntity) export type CouponItem = { Id: number Name: string @@ -26,3 +25,52 @@ export type CouponQueryParams = { PageSize?: number KeyWord?: string } + +export type MyCouponOrgin = { + TenantId: number + From: string + ToUser: number + ToUserRef: string + CouponId: number + CouponCount: number + OriginType: number + Remark: string + CreateDate: string + StartTime: string + EndTime: string + Status: number + DeleteTag: number + Id: number +} + +export type MyCouponInfo = { + TenantId: number + StoreId: number + Name: string + CouponType: number + GrantCount: number + UsedCount: number + UseRange: number + AllowMinAmount: number + CouponValue: number + DateRule: number + ValidDay: number + StartDate: string + EndDate: string + CreateDate: string + IsOverlay: number + IsOpen: number + Disabled: number + DeleteTag: number + Id: number +} + +export type MyCouponItem = { + Count: number + UserId: number + Orgin: MyCouponOrgin + Coupon: MyCouponInfo + ValidDays: number + IsUsed: boolean + IsExpired: boolean +} diff --git a/src/lib/models/funds.ts b/src/lib/models/funds.ts new file mode 100644 index 0000000..0ce6b6d --- /dev/null +++ b/src/lib/models/funds.ts @@ -0,0 +1,19 @@ +export type ScoreItem = { + Id: string + TenantId: string + UserId: string + ScoreType: string + ScoreTypeName: string + ScoreValue: string + ScoreTarget: string | null + Remark: string + CreateTime: string + UpdateTime: string + DeleteTag: string + UserName: string + OperateUserName: string + RestAmount1: string + RestAmount2: string +} + +export const SCORE_PAGE_SIZE = 50 diff --git a/src/lib/models/order.ts b/src/lib/models/order.ts index a13fdb4..15fef46 100644 --- a/src/lib/models/order.ts +++ b/src/lib/models/order.ts @@ -12,12 +12,13 @@ export type OrderItem = { OrderAmount: number CouponAmount: number PaymentAmount: number + RefundCount: number + RefundAmount: number + DayCount: number CreateTime: string UpdateTime: string } -// ==================== 下单接口(php-api /product/CreateOrder) ==================== - export type CreateOrderRequest = { PackageId: number OrderType: number @@ -45,6 +46,8 @@ export type OrderInfo = { OtherPayAmount: number CreateTime: string PayType: number + PayChannel?: number + PaymentAmount?: number } export type CreateOrderData = { @@ -52,8 +55,6 @@ export type CreateOrderData = { PayData: string } -// ==================== 管理端订单查询(.NET /api/course/v1/order/*) ==================== - export type OrderQueryParams = { PageIndex?: number PageSize?: number @@ -68,6 +69,20 @@ export type OrderQueryParams = { PackageNames?: string[] } +export type MyOrderQueryParams = { + BTime?: string + ETime?: string + ProductId?: number + PackageId?: string + OrderType?: number + KeyWord?: string +} + +export type MyOrderPage = { + RowCount: number + List: OrderItem[] +} + export type OrderFilters = { orderType: string productId: string @@ -118,60 +133,4 @@ export function totalConnections(item: OrderItem): number { return item.ConnectCount * item.AccountCount } -// ==================== 表格展示辅助 ==================== - -export function formatMoney(value: number): string { - return value.toFixed(2) -} - -export 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}` -} - -// 前端筛选(后端筛选接口就绪后可移除) -export function matchOrders( - list: OrderItem[], - filters: OrderFilters, -): OrderItem[] { - 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 { formatDate, formatMoney } from "@/lib/utils" diff --git a/src/lib/models/rosorder.ts b/src/lib/models/rosorder.ts index 055b17e..7e4c7a3 100644 --- a/src/lib/models/rosorder.ts +++ b/src/lib/models/rosorder.ts @@ -1,11 +1,19 @@ export type RosOrderItem = { - id: number + id: string + user_id: string username: string + product_id: string product_name: string - real_price: number + order_price: string + real_price: string + channel: string | null order_no: string + ali_wx_no: string | null user: string phone: string address: string + status: string + is_send: string create_time: string + update_time: string } diff --git a/src/lib/recharge.ts b/src/lib/recharge.ts new file mode 100644 index 0000000..8a378fc --- /dev/null +++ b/src/lib/recharge.ts @@ -0,0 +1,43 @@ +// ==================== 支付收银台:数据传递与完成通知 ==================== +// 购买流程:主页面下单成功后,把订单支付数据写入 localStorage(跨窗口实时共享), +// 再打开 /pay?mode=order 收银台完成支付。 +// 收银台轮询到支付成功后,通过 localStorage + storage 事件通知主窗口。 + +export const PAY_PAID_KEY = "juip_pay_paid" + +export type OrderPayData = { + orderNo: string + amount: number + // 30-微信 50-支付宝(对应下单请求的 PayChannel) + payChannel: number + payData: string +} + +const ORDER_PAY_KEY = "juip_order_pay" + +export function saveOrderPay(data: OrderPayData) { + localStorage.setItem(ORDER_PAY_KEY, JSON.stringify(data)) +} + +export function readOrderPay(): OrderPayData | null { + const raw = localStorage.getItem(ORDER_PAY_KEY) + if (!raw) return null + try { + const data = JSON.parse(raw) as OrderPayData + return data?.orderNo ? data : null + } catch { + return null + } +} + +export function clearOrderPay() { + localStorage.removeItem(ORDER_PAY_KEY) +} + +export function notifyPayPaid(orderNo: string) { + localStorage.setItem(PAY_PAID_KEY, orderNo) +} + +export function clearPayPaid() { + localStorage.removeItem(PAY_PAID_KEY) +} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index c2879d3..5810396 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -54,3 +54,18 @@ export function estimateTotal( if (pagerTotal && pagerTotal > 0) return pagerTotal return itemCount > 0 ? (page - 1) * pageSize + itemCount : 0 } + +// 金额格式化:保留两位小数 +export function formatMoney(value: number): string { + return value.toFixed(2) +} + +// 日期格式化:"2026-08-17 15:25:24" → "2026.08.17" +export 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}` +} diff --git a/src/routers/index.tsx b/src/routers/index.tsx index a58c5c6..3602850 100644 --- a/src/routers/index.tsx +++ b/src/routers/index.tsx @@ -21,6 +21,7 @@ import HelpPage from "@/home/help" import IpLinePage from "@/home/ipLine" import LayoutPage from "@/home/layout" import LoginPage from "@/home/login" +import PayPage from "@/home/pay" import ProductPage from "@/home/product" import BuyPage from "@/home/product/buy" import BuyStaticPage from "@/home/product/buyStatic" @@ -48,6 +49,14 @@ export const router = createBrowserRouter([ path: "/login", element: , }, + { + path: "/pay", + element: ( + + + + ), + }, { path: "/admin", element: ( diff --git a/vite.config.ts b/vite.config.ts index 4bc826b..602e045 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,6 +15,11 @@ export default defineConfig({ proxy: { "/api": "http://192.168.3.6:5000", "/article": "http://192.168.3.6:5000", + "/user/UserScore": { + target: "https://php-api.juip.com", + changeOrigin: true, + secure: false, + }, "/user": "http://192.168.3.6:5000", "/product": { target: "http://192.168.3.6:5000", @@ -45,6 +50,16 @@ export default defineConfig({ changeOrigin: true, secure: false, }, + "/order": { + target: "https://php-api.juip.com", + changeOrigin: true, + secure: false, + }, + "/ros": { + target: "https://php-api.juip.com", + changeOrigin: true, + secure: false, + }, }, }, })