Compare commits
2 Commits
f59e929843
...
8d8c9deba7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d8c9deba7 | ||
|
|
7f03dbfb56 |
@@ -1,16 +1,11 @@
|
|||||||
|
import type { ColumnDef } from "@tanstack/react-table"
|
||||||
|
import { fetchCashOutList } from "@/api/cashOut"
|
||||||
import DataTable from "@/components/data-table"
|
import DataTable from "@/components/data-table"
|
||||||
import Page from "@/components/page"
|
import Page from "@/components/page"
|
||||||
|
import { useList } from "@/lib/hooks/useList"
|
||||||
import type { CashOutItem } from "@/lib/models/cashOut"
|
import type { CashOutItem } from "@/lib/models/cashOut"
|
||||||
|
|
||||||
export default function CashOutPage() {
|
const columns: ColumnDef<CashOutItem>[] = [
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<div className="bg-white rounded-lg p-4">
|
|
||||||
<h3 className="font-semibold text-lg">提现管理</h3>
|
|
||||||
<DataTable<CashOutItem>
|
|
||||||
data={[]}
|
|
||||||
status="done"
|
|
||||||
columns={[
|
|
||||||
{
|
{
|
||||||
accessorKey: "username",
|
accessorKey: "username",
|
||||||
header: "用户",
|
header: "用户",
|
||||||
@@ -18,10 +13,12 @@ export default function CashOutPage() {
|
|||||||
{
|
{
|
||||||
accessorKey: "money",
|
accessorKey: "money",
|
||||||
header: "提现金额",
|
header: "提现金额",
|
||||||
|
cell: ({ row }) => `¥${row.original.money}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "reason",
|
accessorKey: "apply_reason",
|
||||||
header: "提现理由",
|
header: "提现理由",
|
||||||
|
cell: ({ row }) => row.original.apply_reason || "--",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "alipay_account",
|
accessorKey: "alipay_account",
|
||||||
@@ -30,6 +27,7 @@ export default function CashOutPage() {
|
|||||||
{
|
{
|
||||||
accessorKey: "remark",
|
accessorKey: "remark",
|
||||||
header: "备注",
|
header: "备注",
|
||||||
|
cell: ({ row }) => row.original.remark || "--",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "status",
|
accessorKey: "status",
|
||||||
@@ -39,7 +37,24 @@ export default function CashOutPage() {
|
|||||||
accessorKey: "create_time",
|
accessorKey: "create_time",
|
||||||
header: "提现时间",
|
header: "提现时间",
|
||||||
},
|
},
|
||||||
]}
|
]
|
||||||
|
|
||||||
|
export default function CashOutPage() {
|
||||||
|
const { data, status } = useList(fetchCashOutList)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="bg-white rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold text-lg">提现管理</h3>
|
||||||
|
<DataTable<CashOutItem>
|
||||||
|
data={data}
|
||||||
|
status={status}
|
||||||
|
columns={columns}
|
||||||
|
pagination={{
|
||||||
|
page: 0,
|
||||||
|
size: 10,
|
||||||
|
total: data.length,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -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: "",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -1,251 +1,248 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import type { ColumnDef } from "@tanstack/react-table"
|
||||||
import { useMemo, useState } from "react"
|
import { SearchIcon } from "lucide-react"
|
||||||
import { Controller, useForm } from "react-hook-form"
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { Link } from "react-router-dom"
|
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { z } from "zod"
|
import { fetchMyAccounts } from "@/api/account"
|
||||||
import DataTable from "@/components/data-table"
|
import DataTable from "@/components/data-table"
|
||||||
import Page from "@/components/page"
|
import Page from "@/components/page"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
|
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import type { ChannelRecord } from "@/lib/models/channel"
|
import {
|
||||||
import { INITIAL_CHANNELS } from "./mock"
|
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({
|
const INITIAL_QUERY: ChannelQuery = {
|
||||||
expired_at_end: z.string().optional(),
|
expiredDay: "-100",
|
||||||
product: z.string().optional(),
|
productId: "0",
|
||||||
plan: z.string().optional(),
|
packageName: "0",
|
||||||
account: z.string().optional(),
|
keyword: "",
|
||||||
status: z.string().optional(),
|
}
|
||||||
})
|
|
||||||
|
|
||||||
type FormValues = z.infer<typeof filterSchema>
|
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<MyAccountItem>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "StartTime",
|
||||||
|
header: "开通时间",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "ProductName",
|
||||||
|
header: "产品",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "PackageName",
|
||||||
|
header: "套餐",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "Account",
|
||||||
|
header: "账号",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="font-mono text-sm">{row.original.Account}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "Pwd",
|
||||||
|
header: "密码",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="font-mono text-sm">{row.original.Pwd}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "ConnectCount",
|
||||||
|
header: "连接数",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "EndTime",
|
||||||
|
header: "到期时间",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "RestTime",
|
||||||
|
header: "剩余时间",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
row.original.RestTime === "已过期" ? "text-red-500" : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row.original.RestTime}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "Status",
|
||||||
|
header: "状态",
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.original.Status === 1 ? (
|
||||||
|
<span className="text-emerald-600">正常</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-red-500">已退货</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export default function ChannelsPage() {
|
export default function ChannelsPage() {
|
||||||
const [filterValues, setFilterValues] = useState<FormValues>({})
|
const products = useProducts()
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({})
|
const [query, setQuery] = useState<ChannelQuery>(INITIAL_QUERY)
|
||||||
|
|
||||||
const { control, handleSubmit, reset } = useForm<FormValues>({
|
const [data, setData] = useState<MyAccountItem[]>([])
|
||||||
resolver: zodResolver(filterSchema),
|
const [status, setStatus] = useState<"load" | "done" | "fail">("load")
|
||||||
defaultValues: {
|
const [page, setPage] = useState(1)
|
||||||
expired_at_end: "",
|
|
||||||
product: "",
|
|
||||||
plan: "",
|
|
||||||
account: "",
|
|
||||||
status: "",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const selectedProduct = useMemo(
|
||||||
if (!filterValues || Object.values(filterValues).every(value => !value)) {
|
() =>
|
||||||
return INITIAL_CHANNELS
|
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)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const loadedRef = useRef(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (loadedRef.current) return
|
||||||
|
loadedRef.current = true
|
||||||
|
void load(toParams(INITIAL_QUERY))
|
||||||
|
}, [load])
|
||||||
|
|
||||||
|
const applyFilters = (patch: Partial<ChannelQuery>) => {
|
||||||
|
const next = { ...query, ...patch }
|
||||||
|
setQuery(next)
|
||||||
|
void load(toParams(next))
|
||||||
}
|
}
|
||||||
|
|
||||||
return INITIAL_CHANNELS.filter(record => {
|
const paged = useMemo(
|
||||||
if (filterValues.expired_at_end) {
|
() => data.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
|
||||||
return record.expiredAt <= filterValues.expired_at_end
|
[data, page],
|
||||||
}
|
)
|
||||||
|
|
||||||
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 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}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div>
|
<div className="bg-white rounded-lg p-4 flex flex-wrap items-end gap-x-4 gap-y-3">
|
||||||
<form className="bg-card rounded-lg" onSubmit={handleSubmit(onSubmit)}>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center justify-center gap-4 bg-amber-200 px-3 py-3">
|
<span className="text-sm text-gray-500 whitespace-nowrap">
|
||||||
<span className="text-orange-600 text-sm">
|
过期时间:
|
||||||
为了给您带来更好的服务体验,请完善QQ号和微信号
|
|
||||||
</span>
|
</span>
|
||||||
<Link to="/">立即完善</Link>
|
<Select
|
||||||
</div>
|
value={query.expiredDay}
|
||||||
<div className="flex flex-wrap p-4 items-end gap-4">
|
onValueChange={value => applyFilters({ expiredDay: value })}
|
||||||
<Controller
|
|
||||||
name="expired_at_end"
|
|
||||||
control={control}
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<Field
|
|
||||||
data-invalid={fieldState.invalid}
|
|
||||||
className="w-48 flex-none"
|
|
||||||
>
|
>
|
||||||
<FieldLabel>到期时间</FieldLabel>
|
<SelectTrigger className="w-40">
|
||||||
<Input type="datetime-local" step="1" {...field} />
|
<SelectValue />
|
||||||
<FieldError>{fieldState.error?.message}</FieldError>
|
</SelectTrigger>
|
||||||
</Field>
|
<SelectContent>
|
||||||
)}
|
{EXPIRED_DAY_OPTIONS.map(option => (
|
||||||
/>
|
<SelectItem key={option.value} value={String(option.value)}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Controller
|
<Select
|
||||||
name="product"
|
value={query.productId}
|
||||||
control={control}
|
onValueChange={value =>
|
||||||
render={({ field, fieldState }) => (
|
applyFilters({ productId: value, packageName: "0" })
|
||||||
<Field
|
}
|
||||||
data-invalid={fieldState.invalid}
|
|
||||||
className="w-48 flex-none"
|
|
||||||
>
|
>
|
||||||
<FieldLabel>全部产品</FieldLabel>
|
<SelectTrigger className="w-36">
|
||||||
<Input {...field} placeholder="请输入产品名称" />
|
<SelectValue placeholder="全部产品" />
|
||||||
<FieldError>{fieldState.error?.message}</FieldError>
|
</SelectTrigger>
|
||||||
</Field>
|
<SelectContent>
|
||||||
)}
|
<SelectItem value="0">全部产品</SelectItem>
|
||||||
/>
|
{products.map(item => (
|
||||||
|
<SelectItem key={item.Product.Id} value={String(item.Product.Id)}>
|
||||||
|
{item.Product.Name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
<Controller
|
<Select
|
||||||
name="plan"
|
value={query.packageName}
|
||||||
control={control}
|
onValueChange={value => applyFilters({ packageName: value })}
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<Field
|
|
||||||
data-invalid={fieldState.invalid}
|
|
||||||
className="w-48 flex-none"
|
|
||||||
>
|
>
|
||||||
<FieldLabel>全部套餐</FieldLabel>
|
<SelectTrigger className="w-36">
|
||||||
<Input {...field} placeholder="请输入套餐名称" />
|
<SelectValue placeholder="全部套餐" />
|
||||||
<FieldError>{fieldState.error?.message}</FieldError>
|
</SelectTrigger>
|
||||||
</Field>
|
<SelectContent>
|
||||||
)}
|
<SelectItem value="0">全部套餐</SelectItem>
|
||||||
/>
|
{selectedProduct?.Packages.filter(pkg => pkg.Status === 1).map(
|
||||||
|
pkg => (
|
||||||
<Controller
|
<SelectItem key={pkg.Id} value={pkg.Name}>
|
||||||
name="account"
|
{pkg.Name}
|
||||||
control={control}
|
</SelectItem>
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<Field
|
|
||||||
data-invalid={fieldState.invalid}
|
|
||||||
className="w-40 flex-none"
|
|
||||||
>
|
|
||||||
<FieldLabel>账号</FieldLabel>
|
|
||||||
<Input {...field} placeholder="请输入账号" />
|
|
||||||
<FieldError>{fieldState.error?.message}</FieldError>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button type="submit">查询</Button>
|
|
||||||
<Button type="button" variant="outline" onClick={onReset}>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-card rounded-lg p-4">
|
|
||||||
<div className="flex gap-4 items-center">
|
|
||||||
<Button type="button" variant="outline" onClick={handleRenew}>
|
|
||||||
续费
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="outline" onClick={handleReturn}>
|
|
||||||
退货
|
|
||||||
</Button>
|
|
||||||
<p className="text-orange-600 text-sm">
|
|
||||||
温馨提示:批量续费可通过筛选和搜索功能精准定位账号
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<DataTable<ChannelRecord>
|
|
||||||
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 }) => (
|
|
||||||
<Button size="sm" variant="outline">
|
|
||||||
{row.original.online}
|
|
||||||
</Button>
|
|
||||||
),
|
),
|
||||||
},
|
)}
|
||||||
{ accessorKey: "status", header: "选择地区" },
|
</SelectContent>
|
||||||
{
|
</Select>
|
||||||
accessorKey: "download",
|
|
||||||
header: "软件下载",
|
<div className="flex items-center gap-2">
|
||||||
cell: ({ row }) => (
|
<Input
|
||||||
<Button size="sm" variant="ghost">
|
value={query.keyword}
|
||||||
{row.original.download}
|
onChange={e => setQuery({ ...query, keyword: e.target.value })}
|
||||||
</Button>
|
onKeyDown={e => {
|
||||||
),
|
if (e.key === "Enter") applyFilters({})
|
||||||
},
|
}}
|
||||||
]}
|
placeholder="账号"
|
||||||
pagination={{
|
className="w-40"
|
||||||
page: 0,
|
/>
|
||||||
size: 10,
|
<Button onClick={() => applyFilters({})}>
|
||||||
total: filteredData.length,
|
<SearchIcon data-icon="inline-start" />
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
||||||
|
<h3 className="font-semibold text-lg">IP账号管理</h3>
|
||||||
|
<DataTable
|
||||||
|
data={paged}
|
||||||
|
status={status}
|
||||||
|
columns={columns}
|
||||||
|
pagination={{
|
||||||
|
page: page - 1,
|
||||||
|
size: PAGE_SIZE,
|
||||||
|
total: data.length,
|
||||||
|
onPageChange: setPage,
|
||||||
}}
|
}}
|
||||||
enableSelection
|
|
||||||
getRowId={row => row.id}
|
|
||||||
rowSelection={rowSelection}
|
|
||||||
onSelectionChange={setRowSelection}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -1,109 +1,31 @@
|
|||||||
import { CheckCircle, Clock, Gift, Tag, Ticket, XCircle } from "lucide-react"
|
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 Page from "@/components/page"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
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 {
|
function CouponCard({ item }: { item: MyCouponItem }) {
|
||||||
couponType: "Discount" | "Minus"
|
const isDiscount = item.Coupon.CouponType === COUPON_TYPE_DISCOUNT
|
||||||
couponValue: number
|
const isUsed = item.IsUsed
|
||||||
name: string
|
|
||||||
allowMinAmount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserCouponModel {
|
|
||||||
coupon: Coupon
|
|
||||||
orgin: {
|
|
||||||
startTime: string
|
|
||||||
endTime: string
|
|
||||||
remark: string
|
|
||||||
}
|
|
||||||
isUsed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={`${item.coupon.name}-${item.orgin.startTime}`}
|
|
||||||
className={`relative overflow-hidden transition-shadow hover:shadow-md ${isUsed ? "opacity-60" : ""}`}
|
className={`relative overflow-hidden transition-shadow hover:shadow-md ${isUsed ? "opacity-60" : ""}`}
|
||||||
>
|
>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
{/* 优惠券左侧 - 面额 */}
|
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className="flex min-w-20 flex-col items-center justify-center rounded-lg bg-primary/10 p-3">
|
<div className="flex min-w-20 flex-col items-center justify-center rounded-lg bg-primary/10 p-3">
|
||||||
{isDiscount ? (
|
{isDiscount ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-2xl font-bold text-primary">
|
<span className="text-2xl font-bold text-primary">
|
||||||
{item.coupon.couponValue}
|
{item.Coupon.CouponValue}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">折</span>
|
<span className="text-xs text-muted-foreground">折</span>
|
||||||
</>
|
</>
|
||||||
@@ -111,21 +33,25 @@ export default function CouponPage() {
|
|||||||
<>
|
<>
|
||||||
<span className="text-xs text-muted-foreground">¥</span>
|
<span className="text-xs text-muted-foreground">¥</span>
|
||||||
<span className="text-2xl font-bold text-primary">
|
<span className="text-2xl font-bold text-primary">
|
||||||
{item.coupon.couponValue}
|
{item.Coupon.CouponValue}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 优惠券右侧 - 详情 */}
|
|
||||||
<div className="flex-1 space-y-1">
|
<div className="flex-1 space-y-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h4 className="font-medium">{item.coupon.name}</h4>
|
<h4 className="font-medium">{item.Coupon.Name}</h4>
|
||||||
{isUsed ? (
|
{isUsed ? (
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
<CheckCircle className="mr-1 h-3 w-3" />
|
<CheckCircle className="mr-1 h-3 w-3" />
|
||||||
已使用
|
已使用
|
||||||
</Badge>
|
</Badge>
|
||||||
|
) : item.IsExpired ? (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
<XCircle className="mr-1 h-3 w-3" />
|
||||||
|
已过期
|
||||||
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="default" className="text-xs">
|
<Badge variant="default" className="text-xs">
|
||||||
<Ticket className="mr-1 h-3 w-3" />
|
<Ticket className="mr-1 h-3 w-3" />
|
||||||
@@ -137,24 +63,23 @@ export default function CouponPage() {
|
|||||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Tag className="h-3 w-3" />
|
<Tag className="h-3 w-3" />
|
||||||
{item.coupon.allowMinAmount > 0
|
{item.Coupon.AllowMinAmount > 0
|
||||||
? `满${item.coupon.allowMinAmount}元可用`
|
? `满${item.Coupon.AllowMinAmount}元可用`
|
||||||
: "无限制"}
|
: "无限制"}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Clock className="h-3 w-3" />
|
<Clock className="h-3 w-3" />
|
||||||
{new Date(item.orgin.startTime).toLocaleDateString()} -{" "}
|
{formatDate(item.Orgin.StartTime)} -{" "}
|
||||||
{new Date(item.orgin.endTime).toLocaleDateString()}
|
{formatDate(item.Orgin.EndTime)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Gift className="h-3 w-3" />
|
<Gift className="h-3 w-3" />
|
||||||
{item.orgin.remark}
|
{item.Orgin.Remark}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 已使用水印效果 */}
|
|
||||||
{isUsed && (
|
{isUsed && (
|
||||||
<div className="absolute right-4 top-4">
|
<div className="absolute right-4 top-4">
|
||||||
<XCircle className="h-6 w-6 text-muted-foreground/50" />
|
<XCircle className="h-6 w-6 text-muted-foreground/50" />
|
||||||
@@ -163,7 +88,19 @@ export default function CouponPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
@@ -172,7 +109,7 @@ export default function CouponPage() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold">我的优惠券</h3>
|
<h3 className="text-lg font-semibold">我的优惠券</h3>
|
||||||
<Badge variant="secondary" className="text-sm">
|
<Badge variant="secondary" className="text-sm">
|
||||||
共 {mockCoupons.length} 张
|
共 {coupons.length} 张
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -187,7 +124,16 @@ export default function CouponPage() {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="available" className="mt-4">
|
<TabsContent value="available" className="mt-4">
|
||||||
{availableCoupons.length === 0 ? (
|
{status === "load" ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
|
<p>加载中...</p>
|
||||||
|
</div>
|
||||||
|
) : status === "fail" ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
|
<XCircle className="mb-4 h-12 w-12 opacity-50" />
|
||||||
|
<p>加载失败,请刷新重试</p>
|
||||||
|
</div>
|
||||||
|
) : availableCoupons.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
<Ticket className="mb-4 h-12 w-12 opacity-50" />
|
<Ticket className="mb-4 h-12 w-12 opacity-50" />
|
||||||
<p>暂无可用优惠券</p>
|
<p>暂无可用优惠券</p>
|
||||||
@@ -195,7 +141,9 @@ export default function CouponPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{availableCoupons.map(renderCoupon)}
|
{availableCoupons.map(item => (
|
||||||
|
<CouponCard key={item.Orgin.Id} item={item} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -208,7 +156,9 @@ export default function CouponPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{usedCoupons.map(renderCoupon)}
|
{usedCoupons.map(item => (
|
||||||
|
<CouponCard key={item.Orgin.Id} item={item} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { Check } from "lucide-react"
|
import { Check } from "lucide-react"
|
||||||
import { type ReactNode, useEffect, useState } from "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 type { UserInfoRecord } from "@/api/token"
|
||||||
import AgreementDialog from "@/components/composites/dialogs/agreementDialog"
|
import AgreementDialog from "@/components/composites/dialogs/agreementDialog"
|
||||||
|
import { CashOutDialog } from "@/components/composites/dialogs/cashOutDialog"
|
||||||
import { ChangePwdDialog } from "@/components/composites/dialogs/changePwdDialog"
|
import { ChangePwdDialog } from "@/components/composites/dialogs/changePwdDialog"
|
||||||
import { EditInfoDialog } from "@/components/composites/dialogs/editInfoDialog"
|
import { EditInfoDialog } from "@/components/composites/dialogs/editInfoDialog"
|
||||||
import RealNameDialog from "@/components/composites/dialogs/realNameDialog"
|
import RealNameDialog from "@/components/composites/dialogs/realNameDialog"
|
||||||
|
import { RechargeDialog } from "@/components/composites/dialogs/rechargeDialog"
|
||||||
import Page from "@/components/page"
|
import Page from "@/components/page"
|
||||||
import { QrCodeImage } from "@/components/qrCodeImage"
|
import { QrCodeImage } from "@/components/qrCodeImage"
|
||||||
import { useDashboardStore } from "@/components/stores/dashboard"
|
import { useDashboardStore } from "@/components/stores/dashboard"
|
||||||
@@ -22,6 +26,7 @@ import type {
|
|||||||
DashboardBalance,
|
DashboardBalance,
|
||||||
IpAccountStats,
|
IpAccountStats,
|
||||||
} from "@/lib/models/dashboard"
|
} from "@/lib/models/dashboard"
|
||||||
|
import { clearPayPaid, PAY_PAID_KEY } from "@/lib/recharge"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import b3 from "../assets/b3.webp"
|
import b3 from "../assets/b3.webp"
|
||||||
import b5 from "../assets/b5.webp"
|
import b5 from "../assets/b5.webp"
|
||||||
@@ -68,6 +73,7 @@ const num = (value: unknown) =>
|
|||||||
typeof value === "number" && Number.isFinite(value) ? value : 0
|
typeof value === "number" && Number.isFinite(value) ? value : 0
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
const apiIndex = useDashboardStore(state => state.data)
|
const apiIndex = useDashboardStore(state => state.data)
|
||||||
const agreementSigned = useDashboardStore(state => state.agreementSigned)
|
const agreementSigned = useDashboardStore(state => state.agreementSigned)
|
||||||
const refresh = useDashboardStore(state => state.refresh)
|
const refresh = useDashboardStore(state => state.refresh)
|
||||||
@@ -80,6 +86,18 @@ export default function DashboardPage() {
|
|||||||
void refreshAgreement()
|
void refreshAgreement()
|
||||||
}, [refresh, 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 user = apiIndex?.UserModel
|
||||||
const statistic = apiIndex?.Statistic
|
const statistic = apiIndex?.Statistic
|
||||||
const account = apiIndex?.AccountModel
|
const account = apiIndex?.AccountModel
|
||||||
@@ -124,6 +142,8 @@ export default function DashboardPage() {
|
|||||||
const [realNameOpen, setRealNameOpen] = useState(false)
|
const [realNameOpen, setRealNameOpen] = useState(false)
|
||||||
const [agreementOpen, setAgreementOpen] = useState(false)
|
const [agreementOpen, setAgreementOpen] = useState(false)
|
||||||
const [managerQrOpen, setManagerQrOpen] = 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() {
|
|||||||
<dl className="divide-y divide-gray-100">
|
<dl className="divide-y divide-gray-100">
|
||||||
<InfoRow label="QQ号" value={accountInfo.qq} />
|
<InfoRow label="QQ号" value={accountInfo.qq} />
|
||||||
<InfoRow label="微信号" value={accountInfo.wechat} />
|
<InfoRow label="微信号" value={accountInfo.wechat} />
|
||||||
<InfoRow label="淘宝会员名" value={accountInfo.taobao} />
|
{/* <InfoRow label="淘宝会员名" value={accountInfo.taobao} /> */}
|
||||||
<InfoRow label="邮箱" value={accountInfo.email} />
|
<InfoRow label="邮箱" value={accountInfo.email} />
|
||||||
</dl>
|
</dl>
|
||||||
{agreementAction(true)}
|
{agreementAction(true)}
|
||||||
@@ -253,12 +273,14 @@ export default function DashboardPage() {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-white text-blue-600 shadow-sm hover:bg-white/80"
|
className="bg-white text-blue-600 shadow-sm hover:bg-white/80"
|
||||||
|
onClick={() => setCashOutOpen(true)}
|
||||||
>
|
>
|
||||||
去提现
|
去提现
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-blue-600 text-white hover:bg-blue-700"
|
className="bg-blue-600 text-white hover:bg-blue-700"
|
||||||
|
onClick={() => setRechargeOpen(true)}
|
||||||
>
|
>
|
||||||
去充值
|
去充值
|
||||||
</Button>
|
</Button>
|
||||||
@@ -274,6 +296,7 @@ export default function DashboardPage() {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-blue-600 text-white hover:bg-blue-700"
|
className="bg-blue-600 text-white hover:bg-blue-700"
|
||||||
|
onClick={() => navigate("/product/http")}
|
||||||
>
|
>
|
||||||
去充值
|
去充值
|
||||||
</Button>
|
</Button>
|
||||||
@@ -373,7 +396,7 @@ export default function DashboardPage() {
|
|||||||
initialData={{
|
initialData={{
|
||||||
qq: accountInfo.qq,
|
qq: accountInfo.qq,
|
||||||
wechat: accountInfo.wechat,
|
wechat: accountInfo.wechat,
|
||||||
taobao: accountInfo.taobao,
|
// taobao: accountInfo.taobao,
|
||||||
email: accountInfo.email,
|
email: accountInfo.email,
|
||||||
}}
|
}}
|
||||||
onSaved={refresh}
|
onSaved={refresh}
|
||||||
@@ -384,6 +407,13 @@ export default function DashboardPage() {
|
|||||||
onOpenChange={setRealNameOpen}
|
onOpenChange={setRealNameOpen}
|
||||||
onVerified={refresh}
|
onVerified={refresh}
|
||||||
/>
|
/>
|
||||||
|
<RechargeDialog open={rechargeOpen} onOpenChange={setRechargeOpen} />
|
||||||
|
<CashOutDialog
|
||||||
|
open={cashOutOpen}
|
||||||
|
onOpenChange={setCashOutOpen}
|
||||||
|
balance={balanceData.balance}
|
||||||
|
onSubmitted={refresh}
|
||||||
|
/>
|
||||||
<AgreementDialog
|
<AgreementDialog
|
||||||
open={agreementOpen}
|
open={agreementOpen}
|
||||||
onOpenChange={setAgreementOpen}
|
onOpenChange={setAgreementOpen}
|
||||||
|
|||||||
@@ -1,29 +1,109 @@
|
|||||||
|
import type { ColumnDef } from "@tanstack/react-table"
|
||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import { fetchScoreList } from "@/api/funds"
|
||||||
import DataTable from "@/components/data-table"
|
import DataTable from "@/components/data-table"
|
||||||
import Page from "@/components/page"
|
import Page from "@/components/page"
|
||||||
|
import { SCORE_PAGE_SIZE, type ScoreItem } from "@/lib/models/funds"
|
||||||
|
|
||||||
|
const columns: ColumnDef<ScoreItem>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "CreateTime",
|
||||||
|
header: "交易时间",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "UserName",
|
||||||
|
header: "用户",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "ScoreTypeName",
|
||||||
|
header: "资金去向",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "ScoreValue",
|
||||||
|
header: "金额",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const value = Number(row.original.ScoreValue)
|
||||||
|
return (
|
||||||
|
<span className={value < 0 ? "text-red-500" : "text-emerald-600"}>
|
||||||
|
{value >= 0 ? "+" : ""}
|
||||||
|
{row.original.ScoreValue}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "RestAmount1",
|
||||||
|
header: "操作前余额",
|
||||||
|
cell: ({ row }) => `¥${row.original.RestAmount1}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "RestAmount2",
|
||||||
|
header: "操作后余额",
|
||||||
|
cell: ({ row }) => `¥${row.original.RestAmount2}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "Remark",
|
||||||
|
header: "备注",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="block max-w-80 truncate" title={row.original.Remark}>
|
||||||
|
{row.original.Remark}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export default function FundsPage() {
|
export default function FundsPage() {
|
||||||
|
const [data, setData] = useState<ScoreItem[]>([])
|
||||||
|
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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
|
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
||||||
|
<h3 className="font-semibold text-lg">资金明细</h3>
|
||||||
<DataTable
|
<DataTable
|
||||||
data={[]}
|
data={data}
|
||||||
status="done"
|
status={status}
|
||||||
columns={[
|
columns={columns}
|
||||||
{ accessorKey: "date", header: "交易时间" },
|
|
||||||
{ accessorKey: "product", header: "用户" },
|
|
||||||
{ accessorKey: "plan", header: "资金去向" },
|
|
||||||
{
|
|
||||||
accessorKey: "price",
|
|
||||||
header: "金额",
|
|
||||||
},
|
|
||||||
{ accessorKey: "connections", header: "操作前约(元)" },
|
|
||||||
{ accessorKey: "account", header: "备注" },
|
|
||||||
]}
|
|
||||||
pagination={{
|
pagination={{
|
||||||
page: 0,
|
page: page - 1,
|
||||||
size: 10,
|
size: SCORE_PAGE_SIZE,
|
||||||
total: 0,
|
sizeOptions: [SCORE_PAGE_SIZE],
|
||||||
|
total,
|
||||||
|
onPageChange: handlePageChange,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" },
|
|
||||||
]
|
|
||||||
@@ -1,30 +1,18 @@
|
|||||||
import type { ColumnDef } from "@tanstack/react-table"
|
import type { ColumnDef } from "@tanstack/react-table"
|
||||||
import { SearchIcon } from "lucide-react"
|
import { fetchMyOrders } from "@/api/order"
|
||||||
import AccountCell from "@/components/composites/accountCell"
|
import AccountCell from "@/components/composites/accountCell"
|
||||||
|
import OrderFilterBar from "@/components/composites/orderFilterBar"
|
||||||
import DataTable from "@/components/data-table"
|
import DataTable from "@/components/data-table"
|
||||||
import Page from "@/components/page"
|
import Page from "@/components/page"
|
||||||
import { Button } from "@/components/ui/button"
|
import { MY_ORDER_PAGE_SIZE, useMyOrderPage } from "@/lib/hooks/useMyOrderPage"
|
||||||
import { Input } from "@/components/ui/input"
|
import { useProducts } from "@/lib/hooks/useProducts"
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select"
|
|
||||||
import { useOrderFilters } from "@/lib/hooks/useOrderFilters"
|
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatMoney,
|
formatMoney,
|
||||||
formatOrderType,
|
formatOrderType,
|
||||||
ORDER_TYPE_LABELS,
|
|
||||||
type OrderItem,
|
type OrderItem,
|
||||||
PACKAGE_OPTIONS,
|
|
||||||
totalConnections,
|
totalConnections,
|
||||||
} from "@/lib/models/order"
|
} from "@/lib/models/order"
|
||||||
import { MOCK_ORDERS, MOCK_PRODUCTS } from "../mock/orders"
|
|
||||||
|
|
||||||
// 后端订单接口就绪后,将 MOCK_ORDERS 替换为 fetchOrderPage 返回的分页数据
|
|
||||||
|
|
||||||
const columns: ColumnDef<OrderItem>[] = [
|
const columns: ColumnDef<OrderItem>[] = [
|
||||||
{
|
{
|
||||||
@@ -82,122 +70,30 @@ const columns: ColumnDef<OrderItem>[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export default function OrdersPage() {
|
export default function OrdersPage() {
|
||||||
const {
|
const products = useProducts()
|
||||||
filters,
|
const { filters, patch, load, total, status, page, setPage, paged } =
|
||||||
patch,
|
useMyOrderPage(fetchMyOrders)
|
||||||
handleSearch,
|
|
||||||
page,
|
|
||||||
setPage,
|
|
||||||
pageSize,
|
|
||||||
handleSizeChange,
|
|
||||||
filtered,
|
|
||||||
paged,
|
|
||||||
} = useOrderFilters(MOCK_ORDERS)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="bg-white rounded-lg p-4 flex flex-wrap items-end gap-x-4 gap-y-3">
|
<OrderFilterBar
|
||||||
<div className="flex items-center gap-2">
|
filters={filters}
|
||||||
<span className="text-sm text-gray-500 whitespace-nowrap">
|
products={products}
|
||||||
日期筛选:
|
onPatch={patch}
|
||||||
</span>
|
onSearch={() => void load()}
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={filters.bTime}
|
|
||||||
onChange={e => patch({ bTime: e.target.value })}
|
|
||||||
className="w-36"
|
|
||||||
aria-label="开始时间"
|
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-400">至</span>
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={filters.eTime}
|
|
||||||
onChange={e => patch({ eTime: e.target.value })}
|
|
||||||
className="w-36"
|
|
||||||
aria-label="结束时间"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
value={filters.orderType}
|
|
||||||
onValueChange={value => patch({ orderType: value })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-30">
|
|
||||||
<SelectValue placeholder="全部类型" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="0">全部类型</SelectItem>
|
|
||||||
{Object.entries(ORDER_TYPE_LABELS).map(([value, label]) => (
|
|
||||||
<SelectItem key={value} value={value}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
value={filters.productId}
|
|
||||||
onValueChange={value => patch({ productId: value })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-32">
|
|
||||||
<SelectValue placeholder="全部产品" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="0">全部产品</SelectItem>
|
|
||||||
{MOCK_PRODUCTS.map(product => (
|
|
||||||
<SelectItem key={product.id} value={String(product.id)}>
|
|
||||||
{product.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
value={filters.packageName}
|
|
||||||
onValueChange={value => patch({ packageName: value })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-32">
|
|
||||||
<SelectValue placeholder="全部套餐" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="0">全部套餐</SelectItem>
|
|
||||||
{PACKAGE_OPTIONS.map(pkg => (
|
|
||||||
<SelectItem key={pkg} value={pkg}>
|
|
||||||
{pkg}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Input
|
|
||||||
value={filters.keyword}
|
|
||||||
onChange={e => patch({ keyword: e.target.value })}
|
|
||||||
onKeyDown={e => {
|
|
||||||
if (e.key === "Enter") handleSearch()
|
|
||||||
}}
|
|
||||||
placeholder="账号"
|
|
||||||
className="w-40"
|
|
||||||
/>
|
|
||||||
<Button onClick={handleSearch}>
|
|
||||||
<SearchIcon data-icon="inline-start" />
|
|
||||||
查询
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
||||||
<h3 className="font-semibold text-lg">订单管理</h3>
|
<h3 className="font-semibold text-lg">订单管理</h3>
|
||||||
<DataTable
|
<DataTable
|
||||||
data={paged}
|
data={paged}
|
||||||
status="done"
|
status={status}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
page: page - 1,
|
page: page - 1,
|
||||||
size: pageSize,
|
size: MY_ORDER_PAGE_SIZE,
|
||||||
total: filtered.length,
|
total,
|
||||||
onPageChange: setPage,
|
onPageChange: setPage,
|
||||||
onSizeChange: handleSizeChange,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,28 +1,17 @@
|
|||||||
import type { ColumnDef } from "@tanstack/react-table"
|
import type { ColumnDef } from "@tanstack/react-table"
|
||||||
import { SearchIcon } from "lucide-react"
|
import { fetchMyRefundOrders } from "@/api/order"
|
||||||
import AccountCell from "@/components/composites/accountCell"
|
import AccountCell from "@/components/composites/accountCell"
|
||||||
|
import OrderFilterBar from "@/components/composites/orderFilterBar"
|
||||||
import DataTable from "@/components/data-table"
|
import DataTable from "@/components/data-table"
|
||||||
import Page from "@/components/page"
|
import Page from "@/components/page"
|
||||||
import { Button } from "@/components/ui/button"
|
import { MY_ORDER_PAGE_SIZE, useMyOrderPage } from "@/lib/hooks/useMyOrderPage"
|
||||||
import { Input } from "@/components/ui/input"
|
import { useProducts } from "@/lib/hooks/useProducts"
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select"
|
|
||||||
import { useOrderFilters } from "@/lib/hooks/useOrderFilters"
|
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatMoney,
|
formatMoney,
|
||||||
type OrderItem,
|
type OrderItem,
|
||||||
PACKAGE_OPTIONS,
|
|
||||||
totalConnections,
|
totalConnections,
|
||||||
} from "@/lib/models/order"
|
} from "@/lib/models/order"
|
||||||
import { MOCK_ORDERS, MOCK_PRODUCTS } from "../mock/orders"
|
|
||||||
|
|
||||||
// 后端订单接口就绪后,将 MOCK_ORDERS 替换为 fetchRefundOrders 返回的分页数据
|
|
||||||
|
|
||||||
const columns: ColumnDef<OrderItem>[] = [
|
const columns: ColumnDef<OrderItem>[] = [
|
||||||
{
|
{
|
||||||
@@ -53,9 +42,9 @@ const columns: ColumnDef<OrderItem>[] = [
|
|||||||
cell: ({ row }) => totalConnections(row.original),
|
cell: ({ row }) => totalConnections(row.original),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "DayPrice",
|
accessorKey: "DayCount",
|
||||||
header: "退款时长",
|
header: "退款时长",
|
||||||
cell: ({ row }) => formatMoney(row.original.DayPrice),
|
cell: ({ row }) => `${row.original.DayCount}天`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "DayPrice",
|
accessorKey: "DayPrice",
|
||||||
@@ -68,112 +57,38 @@ const columns: ColumnDef<OrderItem>[] = [
|
|||||||
cell: ({ row }) => formatMoney(row.original.PaymentAmount),
|
cell: ({ row }) => formatMoney(row.original.PaymentAmount),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "CouponAmount",
|
accessorKey: "RefundAmount",
|
||||||
header: "退款金额",
|
header: "退款金额",
|
||||||
cell: ({ row }) => formatMoney(row.original.CouponAmount),
|
cell: ({ row }) => formatMoney(row.original.RefundAmount),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function RefundOrdersPage() {
|
export default function RefundOrdersPage() {
|
||||||
const {
|
const products = useProducts()
|
||||||
filters,
|
const { filters, patch, load, total, status, page, setPage, paged } =
|
||||||
patch,
|
useMyOrderPage(fetchMyRefundOrders)
|
||||||
handleSearch,
|
|
||||||
page,
|
|
||||||
setPage,
|
|
||||||
pageSize,
|
|
||||||
handleSizeChange,
|
|
||||||
filtered,
|
|
||||||
paged,
|
|
||||||
} = useOrderFilters(MOCK_ORDERS)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="bg-white rounded-lg p-4 flex flex-wrap items-end gap-x-4 gap-y-3">
|
<OrderFilterBar
|
||||||
<div className="flex items-center gap-2">
|
filters={filters}
|
||||||
<span className="text-sm text-gray-500 whitespace-nowrap">
|
products={products}
|
||||||
日期筛选:
|
showOrderType={false}
|
||||||
</span>
|
onPatch={patch}
|
||||||
<Input
|
onSearch={() => void load()}
|
||||||
type="date"
|
|
||||||
value={filters.bTime}
|
|
||||||
onChange={e => patch({ bTime: e.target.value })}
|
|
||||||
className="w-36"
|
|
||||||
aria-label="开始时间"
|
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-400">至</span>
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={filters.eTime}
|
|
||||||
onChange={e => patch({ eTime: e.target.value })}
|
|
||||||
className="w-36"
|
|
||||||
aria-label="结束时间"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
value={filters.productId}
|
|
||||||
onValueChange={value => patch({ productId: value })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-32">
|
|
||||||
<SelectValue placeholder="全部产品" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="0">全部产品</SelectItem>
|
|
||||||
{MOCK_PRODUCTS.map(product => (
|
|
||||||
<SelectItem key={product.id} value={String(product.id)}>
|
|
||||||
{product.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
value={filters.packageName}
|
|
||||||
onValueChange={value => patch({ packageName: value })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-32">
|
|
||||||
<SelectValue placeholder="全部套餐" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="0">全部套餐</SelectItem>
|
|
||||||
{PACKAGE_OPTIONS.map(pkg => (
|
|
||||||
<SelectItem key={pkg} value={pkg}>
|
|
||||||
{pkg}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Input
|
|
||||||
value={filters.keyword}
|
|
||||||
onChange={e => patch({ keyword: e.target.value })}
|
|
||||||
onKeyDown={e => {
|
|
||||||
if (e.key === "Enter") handleSearch()
|
|
||||||
}}
|
|
||||||
placeholder="账号"
|
|
||||||
className="w-40"
|
|
||||||
/>
|
|
||||||
<Button onClick={handleSearch}>
|
|
||||||
<SearchIcon data-icon="inline-start" />
|
|
||||||
查询
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
||||||
<h3 className="font-semibold text-lg">退货订单管理</h3>
|
<h3 className="font-semibold text-lg">退货订单管理</h3>
|
||||||
<DataTable
|
<DataTable
|
||||||
data={paged}
|
data={paged}
|
||||||
status="done"
|
status={status}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
page: page - 1,
|
page: page - 1,
|
||||||
size: pageSize,
|
size: MY_ORDER_PAGE_SIZE,
|
||||||
total: filtered.length,
|
total,
|
||||||
onPageChange: setPage,
|
onPageChange: setPage,
|
||||||
onSizeChange: handleSizeChange,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
|
import type { ColumnDef } from "@tanstack/react-table"
|
||||||
|
import { fetchRosOrderList } from "@/api/rosorder"
|
||||||
import DataTable from "@/components/data-table"
|
import DataTable from "@/components/data-table"
|
||||||
import Page from "@/components/page"
|
import Page from "@/components/page"
|
||||||
|
import { useList } from "@/lib/hooks/useList"
|
||||||
import type { RosOrderItem } from "@/lib/models/rosorder"
|
import type { RosOrderItem } from "@/lib/models/rosorder"
|
||||||
|
|
||||||
// TODO: 软路由订单接口就绪后接入(见 src/api/rosorder.ts fetchRosOrderList)
|
const columns: ColumnDef<RosOrderItem>[] = [
|
||||||
|
|
||||||
export default function RosorderPage() {
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<div className="bg-white rounded-lg p-4">
|
|
||||||
<h3 className="font-semibold text-lg">软路由订单列表</h3>
|
|
||||||
<DataTable<RosOrderItem>
|
|
||||||
data={[]}
|
|
||||||
status="done"
|
|
||||||
columns={[
|
|
||||||
{
|
{
|
||||||
accessorKey: "username",
|
accessorKey: "username",
|
||||||
header: "用户",
|
header: "用户",
|
||||||
@@ -24,6 +17,7 @@ export default function RosorderPage() {
|
|||||||
{
|
{
|
||||||
accessorKey: "real_price",
|
accessorKey: "real_price",
|
||||||
header: "实付",
|
header: "实付",
|
||||||
|
cell: ({ row }) => `¥${row.original.real_price}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "order_no",
|
accessorKey: "order_no",
|
||||||
@@ -40,12 +34,34 @@ export default function RosorderPage() {
|
|||||||
{
|
{
|
||||||
accessorKey: "address",
|
accessorKey: "address",
|
||||||
header: "收货人地址",
|
header: "收货人地址",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="block max-w-60 truncate" title={row.original.address}>
|
||||||
|
{row.original.address}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "create_time",
|
accessorKey: "create_time",
|
||||||
header: "购买时间",
|
header: "购买时间",
|
||||||
},
|
},
|
||||||
]}
|
]
|
||||||
|
|
||||||
|
export default function RosorderPage() {
|
||||||
|
const { data, status } = useList(fetchRosOrderList)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
||||||
|
<h3 className="font-semibold text-lg">软路由订单列表</h3>
|
||||||
|
<DataTable
|
||||||
|
data={data}
|
||||||
|
status={status}
|
||||||
|
columns={columns}
|
||||||
|
pagination={{
|
||||||
|
page: 0,
|
||||||
|
size: 10,
|
||||||
|
total: data.length,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
15
src/api/account.ts
Normal file
15
src/api/account.ts
Normal file
@@ -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<ApiResponse<MyAccountItem[]>> {
|
||||||
|
const res = await callPublic<MyAccountItem[] | unknown>(
|
||||||
|
"/user/ApiMyAccounts",
|
||||||
|
params,
|
||||||
|
"GET",
|
||||||
|
)
|
||||||
|
if (!res.success) return res
|
||||||
|
return { success: true, data: Array.isArray(res.data) ? res.data : [] }
|
||||||
|
}
|
||||||
@@ -1,25 +1,35 @@
|
|||||||
import type { ApiResponse } from "@/lib/api"
|
import type { ApiResponse } from "@/lib/api"
|
||||||
import type { CashOutItem } from "@/lib/models/cashOut"
|
import type { CashOutItem, CashOutSubmitData } from "@/lib/models/cashOut"
|
||||||
import { callByUser } from "./base"
|
import { phpPost } from "./http"
|
||||||
|
|
||||||
// ==================== 提现管理 ====================
|
// ==================== 提现处理(php /order/CashOutIndex/*,cookie 认证) ====================
|
||||||
// TODO: 接口路径待后端确认(旧后台为 php-api: /order/CashOutAdmin/*)。
|
// 接口来源:参考 juipnet 老项目 Host/Views/User/CashOut.cshtml、Index.cshtml
|
||||||
|
|
||||||
export async function fetchCashOutList(
|
export async function fetchCashOutList(): Promise<ApiResponse<CashOutItem[]>> {
|
||||||
params: Record<string, unknown>,
|
const res = await phpPost<Record<string, unknown>>(
|
||||||
): Promise<ApiResponse<CashOutItem[]>> {
|
"/order/CashOutIndex/getData",
|
||||||
return await callByUser<CashOutItem[]>(
|
{ cookie: document.cookie },
|
||||||
"/api/course/v1/order/CashOutList",
|
|
||||||
params,
|
|
||||||
"GET",
|
|
||||||
)
|
)
|
||||||
|
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<string, unknown>,
|
export async function submitCashOut(
|
||||||
): Promise<ApiResponse<undefined>> {
|
data: CashOutSubmitData,
|
||||||
return await callByUser<undefined>(
|
): Promise<ApiResponse<boolean>> {
|
||||||
"/api/course/v1/order/CashOutProcess",
|
const res = await phpPost<Record<string, unknown>>(
|
||||||
data,
|
"/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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<string, unknown>,
|
|
||||||
): Promise<ApiResponse<ChannelRecord[]>> {
|
|
||||||
return await callByUser<ChannelRecord[]>(
|
|
||||||
"/api/course/v1/productaccount/Page",
|
|
||||||
params,
|
|
||||||
"GET",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { ApiResponse } from "@/lib/api"
|
import type { ApiResponse } from "@/lib/api"
|
||||||
import type { CouponItem, CouponQueryParams } from "@/lib/models/coupon"
|
import type {
|
||||||
import { callByUser } from "./base"
|
CouponItem,
|
||||||
|
CouponQueryParams,
|
||||||
// ==================== 优惠券(.NET /api/sells/v1/coupon/*) ====================
|
MyCouponItem,
|
||||||
|
} from "@/lib/models/coupon"
|
||||||
|
import { callByUser, callPublic } from "./base"
|
||||||
|
|
||||||
export async function fetchCouponPage(
|
export async function fetchCouponPage(
|
||||||
params: CouponQueryParams,
|
params: CouponQueryParams,
|
||||||
@@ -33,3 +35,13 @@ export async function giveCoupon(
|
|||||||
): Promise<ApiResponse<undefined>> {
|
): Promise<ApiResponse<undefined>> {
|
||||||
return await callByUser<undefined>("/api/sells/v1/coupon/Give", data)
|
return await callByUser<undefined>("/api/sells/v1/coupon/Give", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchMyCoupons(): Promise<ApiResponse<MyCouponItem[]>> {
|
||||||
|
const res = await callPublic<MyCouponItem[] | unknown>(
|
||||||
|
"/user/ApiMyCoupons",
|
||||||
|
undefined,
|
||||||
|
"GET",
|
||||||
|
)
|
||||||
|
if (!res.success) return res
|
||||||
|
return { success: true, data: Array.isArray(res.data) ? res.data : [] }
|
||||||
|
}
|
||||||
|
|||||||
14
src/api/funds.ts
Normal file
14
src/api/funds.ts
Normal file
@@ -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<ApiResponse<ScoreItem[]>> {
|
||||||
|
const res = await phpPost<ScoreItem[] | unknown>("/user/UserScore/getList", {
|
||||||
|
cookie: document.cookie,
|
||||||
|
page,
|
||||||
|
})
|
||||||
|
if (!res.success) return res
|
||||||
|
return { success: true, data: Array.isArray(res.data) ? res.data : [] }
|
||||||
|
}
|
||||||
@@ -3,10 +3,12 @@ import type {
|
|||||||
CreateOrderData,
|
CreateOrderData,
|
||||||
CreateOrderRequest,
|
CreateOrderRequest,
|
||||||
CreateTestAccountRequest,
|
CreateTestAccountRequest,
|
||||||
|
MyOrderPage,
|
||||||
|
MyOrderQueryParams,
|
||||||
OrderItem,
|
OrderItem,
|
||||||
OrderQueryParams,
|
OrderQueryParams,
|
||||||
} from "@/lib/models/order"
|
} from "@/lib/models/order"
|
||||||
import { callByUser } from "./base"
|
import { callByUser, callPublic } from "./base"
|
||||||
|
|
||||||
// ==================== 购买下单(php-api /product/*) ====================
|
// ==================== 购买下单(php-api /product/*) ====================
|
||||||
|
|
||||||
@@ -105,3 +107,74 @@ export async function exportOrders(
|
|||||||
"GET",
|
"GET",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchMyOrderPage(
|
||||||
|
endpoint: string,
|
||||||
|
params: MyOrderQueryParams,
|
||||||
|
): Promise<ApiResponse<MyOrderPage>> {
|
||||||
|
const res = await callPublic<MyOrderPage[] | MyOrderPage | unknown>(
|
||||||
|
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<ApiResponse<MyOrderPage>> {
|
||||||
|
return fetchMyOrderPage("/user/ApiMyOrders", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 我的退货订单
|
||||||
|
export async function fetchMyRefundOrders(
|
||||||
|
params: MyOrderQueryParams,
|
||||||
|
): Promise<ApiResponse<MyOrderPage>> {
|
||||||
|
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<ApiResponse<RechargeOrderResult>> {
|
||||||
|
return await callPublic<CreateOrderData | RechargeOrderResult>(
|
||||||
|
"/user/CreateOrder",
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮询充值支付状态:返回 true 表示支付成功
|
||||||
|
export async function checkRechargeStatus(
|
||||||
|
orderNo: string,
|
||||||
|
): Promise<ApiResponse<boolean>> {
|
||||||
|
const res = await callPublic<number | null>(
|
||||||
|
`/user/IsPay?orderNo=${encodeURIComponent(orderNo)}`,
|
||||||
|
undefined,
|
||||||
|
"GET",
|
||||||
|
)
|
||||||
|
if (!res.success) return res
|
||||||
|
return { success: true, data: res.data === 1 }
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,3 +29,14 @@ export async function fetchWindowProducts(): Promise<
|
|||||||
"GET",
|
"GET",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 产品+套餐(用于账号管理筛选,接口来源:juipnet MyAccounts.cshtml)
|
||||||
|
export async function fetchProductWithPackage(): Promise<
|
||||||
|
ApiResponse<ProductItem[]>
|
||||||
|
> {
|
||||||
|
return await callPublic<ProductItem[]>(
|
||||||
|
"/api/course/v1/product/ProductWithPackage",
|
||||||
|
undefined,
|
||||||
|
"GET",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import type { ApiResponse } from "@/lib/api"
|
import type { ApiResponse } from "@/lib/api"
|
||||||
import type { RosOrderItem } from "@/lib/models/rosorder"
|
import type { RosOrderItem } from "@/lib/models/rosorder"
|
||||||
import { callByUser } from "./base"
|
import { phpPost } from "./http"
|
||||||
|
|
||||||
// ==================== 软路由订单 ====================
|
export async function fetchRosOrderList(): Promise<
|
||||||
// TODO: 接口路径待后端确认(旧后台为 /course/v1/order/ros 类接口)。
|
ApiResponse<RosOrderItem[]>
|
||||||
|
> {
|
||||||
export async function fetchRosOrderList(
|
const res = await phpPost<Record<string, unknown>>("/ros/order/getList", {
|
||||||
params: Record<string, unknown>,
|
cookie: document.cookie,
|
||||||
): Promise<ApiResponse<RosOrderItem[]>> {
|
})
|
||||||
return await callByUser<RosOrderItem[]>(
|
if (!res.success) return res
|
||||||
"/api/course/v1/order/RosOrders",
|
const list = res.data?.data ?? res.data
|
||||||
params,
|
return {
|
||||||
"GET",
|
success: true,
|
||||||
)
|
data: Array.isArray(list) ? (list as RosOrderItem[]) : [],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import type { ApiResponse } from "@/lib/api"
|
import type { ApiResponse } from "@/lib/api"
|
||||||
import type { UserItem, UserQueryParams } from "@/lib/models/user"
|
import type {
|
||||||
|
PhpUserModel,
|
||||||
|
UpdateUserInfoParams,
|
||||||
|
UpdateUserPwdParams,
|
||||||
|
UserItem,
|
||||||
|
UserQueryParams,
|
||||||
|
} from "@/lib/models/user"
|
||||||
import { callByUser, callPublic } from "./base"
|
import { callByUser, callPublic } from "./base"
|
||||||
|
|
||||||
// ==================== 用户管理(.NET /api/baseinfo/v1/user/*) ====================
|
// ==================== 用户管理 ====================
|
||||||
|
|
||||||
export async function fetchUserPage(
|
export async function fetchUserPage(
|
||||||
params: UserQueryParams,
|
params: UserQueryParams,
|
||||||
@@ -43,23 +49,16 @@ export async function updateUserProfile(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 个人中心资料/密码(.NET /user/*,cookie 会话) ====================
|
// ==================== 个人中心资料/密码 ====================
|
||||||
|
|
||||||
// 编辑资料:老项目 /user/UpdateInfo(QQ/Wx/TaoBao/Email)
|
export async function updateUserInfo(
|
||||||
export async function updateUserInfo(data: {
|
data: UpdateUserInfoParams,
|
||||||
QQ: string
|
): Promise<ApiResponse<undefined>> {
|
||||||
Wx: string
|
return await callPublic<undefined>("/user/ApiUpdateInfo", data)
|
||||||
TaoBao: string
|
|
||||||
Email: string
|
|
||||||
}): Promise<ApiResponse<undefined>> {
|
|
||||||
return await callPublic<undefined>("/user/UpdateInfo", data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改密码:老项目 /user/UpdatePwd(OldPwd/NewPwd/ConfirmPwd)
|
export async function updateUserPwd(
|
||||||
export async function updateUserPwd(data: {
|
data: UpdateUserPwdParams,
|
||||||
OldPwd: string
|
): Promise<ApiResponse<PhpUserModel>> {
|
||||||
NewPwd: string
|
return await callPublic<PhpUserModel>("/user/UpdatePwd", data)
|
||||||
ConfirmPwd: string
|
|
||||||
}): Promise<ApiResponse<undefined>> {
|
|
||||||
return await callPublic<undefined>("/user/UpdatePwd", data)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -7,6 +8,11 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} 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 {
|
interface AnnouncementModalProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
@@ -16,6 +22,18 @@ export default function AnnouncementModal({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: AnnouncementModalProps) {
|
}: AnnouncementModalProps) {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleViewActivity = () => {
|
||||||
|
onOpenChange(false)
|
||||||
|
navigate("/product")
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleContactService = () => {
|
||||||
|
onOpenChange(false)
|
||||||
|
window.open(CUSTOMER_SERVICE_URL, "_blank", "noopener,noreferrer")
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
@@ -50,13 +68,19 @@ export default function AnnouncementModal({
|
|||||||
|
|
||||||
<div className="flex mt-2">
|
<div className="flex mt-2">
|
||||||
<DialogClose asChild className="flex-1">
|
<DialogClose asChild className="flex-1">
|
||||||
<Button className="w-full h-14 rounded-none bg-linear-to-r from-blue-500 to-cyan-400 hover:opacity-90 text-white text-lg font-medium border-0 rounded-bl-xl">
|
<Button
|
||||||
|
onClick={handleViewActivity}
|
||||||
|
className="w-full h-14 rounded-none bg-linear-to-r from-blue-500 to-cyan-400 hover:opacity-90 text-white text-lg font-medium border-0 rounded-bl-xl"
|
||||||
|
>
|
||||||
查看活动
|
查看活动
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
|
|
||||||
<DialogClose asChild className="flex-1">
|
<DialogClose asChild className="flex-1">
|
||||||
<Button className="w-full h-14 rounded-none bg-linear-to-r from-cyan-400 to-blue-500 hover:opacity-90 text-white text-lg font-medium border-0 rounded-br-xl">
|
<Button
|
||||||
|
onClick={handleContactService}
|
||||||
|
className="w-full h-14 rounded-none bg-linear-to-r from-cyan-400 to-blue-500 hover:opacity-90 text-white text-lg font-medium border-0 rounded-br-xl"
|
||||||
|
>
|
||||||
联系客服
|
联系客服
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
|
|||||||
212
src/components/composites/dialogs/cashOutDialog.tsx
Normal file
212
src/components/composites/dialogs/cashOutDialog.tsx
Normal file
@@ -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 (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-h-[85dvh] overflow-y-auto sm:max-w-sm p-6">
|
||||||
|
{step === "notice" ? (
|
||||||
|
<>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-xl font-semibold text-center">
|
||||||
|
余额提现
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="mt-3 space-y-2 text-sm text-gray-600">
|
||||||
|
<p>聚IP涵盖十余种全网知名旗舰产品,总有一款适合您!</p>
|
||||||
|
<p>联系客服帮您推荐合适的产品,我们将最大努力满足您的需求!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
CUSTOMER_SERVICE_URL,
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
联系客服
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => setStep("form")}
|
||||||
|
>
|
||||||
|
我不需要
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-xl font-semibold text-center">
|
||||||
|
余额提现
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 mt-2">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
当前可用余额:
|
||||||
|
<span className="font-bold text-orange-500">
|
||||||
|
¥{balance.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-sm text-gray-600">提现金额</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={money}
|
||||||
|
onChange={e => setMoney(e.target.value)}
|
||||||
|
className="h-11 bg-gray-50 border-gray-200"
|
||||||
|
placeholder="请输入提现金额"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-sm text-gray-600">提现理由</Label>
|
||||||
|
<Input
|
||||||
|
value={reason}
|
||||||
|
onChange={e => setReason(e.target.value)}
|
||||||
|
className="h-11 bg-gray-50 border-gray-200"
|
||||||
|
placeholder="请输入提现理由"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-sm text-gray-600">
|
||||||
|
提现支付宝账号(目前仅支持支付宝)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
value={alipayAccount}
|
||||||
|
onChange={e => setAlipayAccount(e.target.value)}
|
||||||
|
className="h-11 bg-gray-50 border-gray-200"
|
||||||
|
placeholder="请输入支付宝账号"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-sm text-gray-600">支付宝真实姓名</Label>
|
||||||
|
<Input
|
||||||
|
value={realName}
|
||||||
|
onChange={e => setRealName(e.target.value)}
|
||||||
|
className="h-11 bg-gray-50 border-gray-200"
|
||||||
|
placeholder="请输入与支付宝关联的真实姓名"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={submitting}
|
||||||
|
onClick={() => void handleSubmit()}
|
||||||
|
className="w-full h-11 mt-2 bg-blue-600 hover:bg-blue-700 text-white font-medium"
|
||||||
|
>
|
||||||
|
{submitting ? "提交中..." : "确认提现"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -18,14 +18,14 @@ import { Label } from "@/components/ui/label"
|
|||||||
export type EditInfoValues = {
|
export type EditInfoValues = {
|
||||||
qq: string
|
qq: string
|
||||||
wechat: string
|
wechat: string
|
||||||
taobao: string
|
// taobao: string
|
||||||
email: string
|
email: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const editInfoSchema = z.object({
|
const editInfoSchema = z.object({
|
||||||
qq: z.string(),
|
qq: z.string(),
|
||||||
wechat: z.string(),
|
wechat: z.string(),
|
||||||
taobao: z.string(),
|
// taobao: z.string(),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
.refine(
|
.refine(
|
||||||
@@ -44,7 +44,7 @@ interface EditInfoDialogProps {
|
|||||||
const DEFAULT_INITIAL_DATA: EditInfoValues = {
|
const DEFAULT_INITIAL_DATA: EditInfoValues = {
|
||||||
qq: "",
|
qq: "",
|
||||||
wechat: "",
|
wechat: "",
|
||||||
taobao: "",
|
// taobao: "",
|
||||||
email: "",
|
email: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ export function EditInfoDialog({
|
|||||||
const res = await updateUserInfo({
|
const res = await updateUserInfo({
|
||||||
QQ: data.qq,
|
QQ: data.qq,
|
||||||
Wx: data.wechat,
|
Wx: data.wechat,
|
||||||
TaoBao: data.taobao,
|
// TaoBao: data.taobao,
|
||||||
Email: data.email,
|
Email: data.email,
|
||||||
})
|
})
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
@@ -118,7 +118,7 @@ export function EditInfoDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
{/* <div className="space-y-1.5">
|
||||||
<Label className="text-sm text-gray-600">淘宝会员名</Label>
|
<Label className="text-sm text-gray-600">淘宝会员名</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="taobao"
|
name="taobao"
|
||||||
@@ -127,7 +127,7 @@ export function EditInfoDialog({
|
|||||||
<Input {...field} className="h-11 bg-gray-50 border-gray-200" />
|
<Input {...field} className="h-11 bg-gray-50 border-gray-200" />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-sm text-gray-600">邮箱</Label>
|
<Label className="text-sm text-gray-600">邮箱</Label>
|
||||||
|
|||||||
112
src/components/composites/dialogs/rechargeDialog.tsx
Normal file
112
src/components/composites/dialogs/rechargeDialog.tsx
Normal file
@@ -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<PayChannel>("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 (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-sm">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-center">账户充值</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 mt-1">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-sm text-gray-600">充值金额</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={amount}
|
||||||
|
onChange={e => setAmount(e.target.value)}
|
||||||
|
className="h-11 bg-gray-50 border-gray-200"
|
||||||
|
placeholder="请输入充值金额"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm text-gray-600">支付方式</Label>
|
||||||
|
<RadioGroup
|
||||||
|
value={payChannel}
|
||||||
|
onValueChange={value => setPayChannel(value as PayChannel)}
|
||||||
|
className="flex gap-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="50" id="recharge-alipay" />
|
||||||
|
<Label
|
||||||
|
htmlFor="recharge-alipay"
|
||||||
|
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<img src={alipay} alt="支付宝" className="w-5 h-5" />
|
||||||
|
支付宝
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="30" id="recharge-wechat" />
|
||||||
|
<Label
|
||||||
|
htmlFor="recharge-wechat"
|
||||||
|
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<img src={wechat} alt="微信" className="w-5 h-5" />
|
||||||
|
微信
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleGoPay}
|
||||||
|
className="w-full h-11 bg-blue-600 hover:bg-blue-700 text-white font-medium"
|
||||||
|
>
|
||||||
|
充值
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
121
src/components/composites/orderFilterBar.tsx
Normal file
121
src/components/composites/orderFilterBar.tsx
Normal file
@@ -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<OrderFilters>) => void
|
||||||
|
onSearch: () => void
|
||||||
|
}) {
|
||||||
|
const { filters, products, showOrderType = true, onPatch, onSearch } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg p-4 flex flex-wrap items-end gap-x-4 gap-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-gray-500 whitespace-nowrap">
|
||||||
|
日期筛选:
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={filters.bTime}
|
||||||
|
onChange={e => onPatch({ bTime: e.target.value })}
|
||||||
|
className="w-36"
|
||||||
|
aria-label="开始时间"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-400">至</span>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={filters.eTime}
|
||||||
|
onChange={e => onPatch({ eTime: e.target.value })}
|
||||||
|
className="w-36"
|
||||||
|
aria-label="结束时间"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showOrderType && (
|
||||||
|
<Select
|
||||||
|
value={filters.orderType}
|
||||||
|
onValueChange={value => onPatch({ orderType: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-30">
|
||||||
|
<SelectValue placeholder="全部类型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="0">全部类型</SelectItem>
|
||||||
|
{Object.entries(ORDER_TYPE_LABELS).map(([value, label]) => (
|
||||||
|
<SelectItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={filters.productId}
|
||||||
|
onValueChange={value => onPatch({ productId: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-32">
|
||||||
|
<SelectValue placeholder="全部产品" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="0">全部产品</SelectItem>
|
||||||
|
{products.map(item => (
|
||||||
|
<SelectItem key={item.Product.Id} value={String(item.Product.Id)}>
|
||||||
|
{item.Product.Name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={filters.packageName}
|
||||||
|
onValueChange={value => onPatch({ packageName: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-32">
|
||||||
|
<SelectValue placeholder="全部套餐" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="0">全部套餐</SelectItem>
|
||||||
|
{PACKAGE_OPTIONS.map(pkg => (
|
||||||
|
<SelectItem key={pkg} value={pkg}>
|
||||||
|
{pkg}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
value={filters.keyword}
|
||||||
|
onChange={e => onPatch({ keyword: e.target.value })}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === "Enter") onSearch()
|
||||||
|
}}
|
||||||
|
placeholder="账号"
|
||||||
|
className="w-40"
|
||||||
|
/>
|
||||||
|
<Button onClick={onSearch}>
|
||||||
|
<SearchIcon data-icon="inline-start" />
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { create } from "zustand"
|
import { create } from "zustand"
|
||||||
|
|
||||||
|
|
||||||
type ClientState = {
|
type ClientState = {
|
||||||
breakpoint: {
|
breakpoint: {
|
||||||
sm: boolean
|
sm: boolean
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { create } from "zustand"
|
import { create } from "zustand"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type LayoutState = {
|
type LayoutState = {
|
||||||
navbar: boolean
|
navbar: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
type UserInfoRecord,
|
type UserInfoRecord,
|
||||||
} from "@/api/token"
|
} from "@/api/token"
|
||||||
|
|
||||||
|
|
||||||
export type ProfileState = {
|
export type ProfileState = {
|
||||||
token: string | null
|
token: string | null
|
||||||
userInfo: UserInfoRecord | null
|
userInfo: UserInfoRecord | null
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ export default function LoginPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setTab("login")}
|
onClick={() => setTab("login")}
|
||||||
className={`flex-1 py-2 rounded-md transition-all ${
|
className={`flex-1 py-2 rounded-md transition-all ${tab === "login"
|
||||||
tab === "login"
|
|
||||||
? "bg-white shadow-sm text-blue-600 font-medium"
|
? "bg-white shadow-sm text-blue-600 font-medium"
|
||||||
: "text-gray-600 hover:text-gray-900"
|
: "text-gray-600 hover:text-gray-900"
|
||||||
}`}
|
}`}
|
||||||
@@ -70,8 +69,7 @@ export default function LoginPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setTab("register")}
|
onClick={() => setTab("register")}
|
||||||
className={`flex-1 py-2 rounded-md transition-all ${
|
className={`flex-1 py-2 rounded-md transition-all ${tab === "register"
|
||||||
tab === "register"
|
|
||||||
? "bg-white shadow-sm text-blue-600 font-medium"
|
? "bg-white shadow-sm text-blue-600 font-medium"
|
||||||
: "text-gray-600 hover:text-gray-900"
|
: "text-gray-600 hover:text-gray-900"
|
||||||
}`}
|
}`}
|
||||||
@@ -174,9 +172,9 @@ function LoginForm() {
|
|||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{showPassword ? (
|
{showPassword ? (
|
||||||
<EyeOffIcon className="size-4" />
|
|
||||||
) : (
|
|
||||||
<EyeIcon className="size-4" />
|
<EyeIcon className="size-4" />
|
||||||
|
) : (
|
||||||
|
<EyeOffIcon className="size-4" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
394
src/home/pay/index.tsx
Normal file
394
src/home/pay/index.tsx
Normal file
@@ -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<PollState>,
|
||||||
|
) {
|
||||||
|
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" ? <OrderPay /> : <RechargePay />
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 充值模式 ====================
|
||||||
|
|
||||||
|
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<RechargeOrderResult | null>(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<PollState> => {
|
||||||
|
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 (
|
||||||
|
<div className="min-h-dvh flex items-center justify-center bg-gray-50 p-4">
|
||||||
|
<div className="w-full max-w-sm rounded-xl bg-white p-6 shadow-sm text-center">
|
||||||
|
{orderError ? (
|
||||||
|
<>
|
||||||
|
<XCircle className="mx-auto size-10 text-red-400" />
|
||||||
|
<p className="mt-2 text-sm text-red-500">{orderError}</p>
|
||||||
|
<p className="text-xs text-gray-500">请回到原页面重新发起</p>
|
||||||
|
</>
|
||||||
|
) : !order ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mx-auto size-8 animate-spin text-blue-500" />
|
||||||
|
<p className="mt-2 text-sm text-gray-500">正在生成订单...</p>
|
||||||
|
</>
|
||||||
|
) : paid ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle2 className="mx-auto size-12 text-emerald-500" />
|
||||||
|
<p className="mt-2 font-medium text-emerald-600">支付成功</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
订单已生效,可以关闭本页面
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mx-auto size-8 animate-spin text-blue-500" />
|
||||||
|
<p className="mt-2 text-sm text-gray-500">正在跳转支付宝...</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
如未自动跳转,请检查浏览器是否拦截
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CashierLayout
|
||||||
|
isWechat={true}
|
||||||
|
amount={amountParam}
|
||||||
|
orderNo={order?.OrderInfo.OrderNo}
|
||||||
|
payData={order?.PayData ?? ""}
|
||||||
|
paid={paid}
|
||||||
|
loading={valid && !order && !orderError}
|
||||||
|
error={orderError}
|
||||||
|
failCount={failCount}
|
||||||
|
invalid={!valid}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// ==================== 购买模式 ====================
|
||||||
|
|
||||||
|
function OrderPay() {
|
||||||
|
const [pay] = useState(readOrderPay)
|
||||||
|
|
||||||
|
const checkPaid = useCallback(async (orderNo: string): Promise<PollState> => {
|
||||||
|
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 (
|
||||||
|
<CashierLayout
|
||||||
|
isWechat={pay?.payChannel === 30}
|
||||||
|
amount={pay?.amount ?? 0}
|
||||||
|
orderNo={pay?.orderNo}
|
||||||
|
payData={pay?.payData ?? ""}
|
||||||
|
paid={paid}
|
||||||
|
loading={false}
|
||||||
|
error=""
|
||||||
|
failCount={failCount}
|
||||||
|
invalid={!pay}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 收银台界面 ====================
|
||||||
|
|
||||||
|
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*</.test(payData)
|
||||||
|
// 微信支付:PayData 是支付链接或自动提交表单时直接跳转,扫码内容才展示二维码
|
||||||
|
const wechatShouldRedirect = isWechat && (payDataIsUrl || payDataIsHtml)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
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 (
|
||||||
|
<div className="min-h-dvh flex items-center justify-center bg-gray-50 p-4">
|
||||||
|
<div className="flex flex-col items-center gap-3 rounded-xl bg-white p-10 shadow-sm">
|
||||||
|
<XCircle className="size-10 text-gray-300" />
|
||||||
|
<p className="text-sm text-gray-500">支付信息已失效,请重新发起</p>
|
||||||
|
<Button onClick={() => window.close()}>关闭页面</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="min-h-dvh flex items-center justify-center bg-gray-50 p-4">
|
||||||
|
<div className="w-full max-w-sm rounded-xl bg-white p-6 shadow-sm">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<img
|
||||||
|
src={isWechat ? wechat : alipay}
|
||||||
|
alt={isWechat ? "微信支付" : "支付宝"}
|
||||||
|
className="size-6"
|
||||||
|
/>
|
||||||
|
<h1 className="text-lg font-semibold">
|
||||||
|
{isWechat ? "微信支付" : "支付宝"} | 收银台
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-1 rounded-lg bg-gray-50 p-4 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">应付金额</span>
|
||||||
|
<span className="font-bold text-orange-500">
|
||||||
|
¥{Number(amount).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">收款方</span>
|
||||||
|
<span>聚IP</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">订单号</span>
|
||||||
|
<span className="font-mono text-xs leading-5">
|
||||||
|
{orderNo ?? "--"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{paid ? (
|
||||||
|
<div className="mt-6 flex flex-col items-center gap-2">
|
||||||
|
<CheckCircle2 className="size-12 text-emerald-500" />
|
||||||
|
<p className="font-medium text-emerald-600">支付成功</p>
|
||||||
|
<p className="text-xs text-gray-500">订单已生效,可以关闭本页面</p>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="mt-6 flex flex-col items-center gap-2">
|
||||||
|
<XCircle className="size-10 text-red-400" />
|
||||||
|
<p className="text-sm text-red-500">{error}</p>
|
||||||
|
<p className="text-xs text-gray-500">请回到原页面重新发起</p>
|
||||||
|
</div>
|
||||||
|
) : loading ? (
|
||||||
|
<div className="mt-6 flex flex-col items-center gap-2 text-gray-400">
|
||||||
|
<Loader2 className="size-8 animate-spin" />
|
||||||
|
<p className="text-xs">正在生成订单...</p>
|
||||||
|
</div>
|
||||||
|
) : isWechat ? (
|
||||||
|
wechatShouldRedirect ? (
|
||||||
|
<div className="mt-6 flex flex-col items-center gap-2">
|
||||||
|
<Loader2 className="size-8 animate-spin text-emerald-500" />
|
||||||
|
<p className="text-sm text-gray-500">正在跳转微信支付...</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
如未自动跳转,请检查浏览器是否拦截
|
||||||
|
</p>
|
||||||
|
<PollingHint failCount={failCount} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-6 flex flex-col items-center gap-3">
|
||||||
|
{payData ? (
|
||||||
|
<>
|
||||||
|
<div className="rounded-lg border border-gray-200 p-2">
|
||||||
|
<QrCodeImage text={payData} size={200} />
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
请使用微信扫码支付,支付完成后本页将自动更新
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<XCircle className="size-10 text-red-400" />
|
||||||
|
<p className="text-sm text-red-500">
|
||||||
|
未获取到支付二维码,请回到原页面重新发起
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<PollingHint failCount={failCount} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="mt-6 flex flex-col items-center gap-3">
|
||||||
|
<Button
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||||
|
onClick={openAlipayWindow}
|
||||||
|
>
|
||||||
|
前往支付宝支付
|
||||||
|
</Button>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
请在打开的页面完成支付,完成后回到本页等待自动确认
|
||||||
|
</p>
|
||||||
|
<PollingHint failCount={failCount} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PollingHint({ failCount }: { failCount: number }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-gray-400">
|
||||||
|
<Loader2 className="size-3 animate-spin" />
|
||||||
|
正在等待支付结果
|
||||||
|
{failCount > 2 && "(网络波动,请稍候)"}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import Wrap from "@/components/wrap"
|
import Wrap from "@/components/wrap"
|
||||||
import type { CreateOrderRequest } from "@/lib/models/order"
|
import type { CreateOrderRequest } from "@/lib/models/order"
|
||||||
import type { Product } from "@/lib/models/product"
|
import type { Product } from "@/lib/models/product"
|
||||||
|
import { clearPayPaid, PAY_PAID_KEY, saveOrderPay } from "@/lib/recharge"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import alipay from "../_assets/alipay.svg"
|
import alipay from "../_assets/alipay.svg"
|
||||||
import balance from "../_assets/balance.svg"
|
import balance from "../_assets/balance.svg"
|
||||||
@@ -221,6 +222,18 @@ export default function BuyPage() {
|
|||||||
const [payType, setPayType] = useState("100")
|
const [payType, setPayType] = useState("100")
|
||||||
const [submitting, setSubmitting] = useState(false)
|
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(() => {
|
useEffect(() => {
|
||||||
if (!product) {
|
if (!product) {
|
||||||
navigate("/product", { replace: true })
|
navigate("/product", { replace: true })
|
||||||
@@ -324,11 +337,40 @@ export default function BuyPage() {
|
|||||||
|
|
||||||
const result = await createOrder(params)
|
const result = await createOrder(params)
|
||||||
|
|
||||||
if (result.success) {
|
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("购买成功!")
|
toast.success("购买成功!")
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message)
|
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 {
|
} catch {
|
||||||
toast.error("网络错误,请重试")
|
toast.error("网络错误,请重试")
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { useLocation, useNavigate } from "react-router-dom"
|
import { useLocation, useNavigate } from "react-router-dom"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import { createOrder } from "@/api/order"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||||
import Wrap from "@/components/wrap"
|
import Wrap from "@/components/wrap"
|
||||||
|
import type { CreateOrderRequest } from "@/lib/models/order"
|
||||||
|
import { clearPayPaid, PAY_PAID_KEY, saveOrderPay } from "@/lib/recharge"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import alipay from "../_assets/alipay.svg"
|
import alipay from "../_assets/alipay.svg"
|
||||||
import balance from "../_assets/balance.svg"
|
import balance from "../_assets/balance.svg"
|
||||||
@@ -29,7 +33,8 @@ function randomDigits(length: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BuyStaticState = {
|
type BuyStaticState = {
|
||||||
pool?: { id?: string; title?: string }
|
// packageId:待后端静态池套餐数据就绪后由 ShuidiPools 传入
|
||||||
|
pool?: { id?: string; title?: string; packageId?: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 独享类型切换 Tab ============
|
// ============ 独享类型切换 Tab ============
|
||||||
@@ -273,14 +278,100 @@ export default function BuyStaticPage() {
|
|||||||
const [bandwidth, setBandwidth] = useState("2M")
|
const [bandwidth, setBandwidth] = useState("2M")
|
||||||
const [duration, setDuration] = useState("test")
|
const [duration, setDuration] = useState("test")
|
||||||
const [payType, setPayType] = useState("100")
|
const [payType, setPayType] = useState("100")
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAccount(randomChars(2) + randomDigits(4))
|
setAccount(randomChars(2) + randomDigits(4))
|
||||||
setPwd(randomDigits(3))
|
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)
|
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 getPrice = () => {
|
||||||
const bwMap: Record<string, number> = {
|
const bwMap: Record<string, number> = {
|
||||||
@@ -523,8 +614,12 @@ export default function BuyStaticPage() {
|
|||||||
>
|
>
|
||||||
上一步
|
上一步
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="flex-1 h-12 rounded-xl text-base bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 transition-all">
|
<Button
|
||||||
确认支付
|
className="flex-1 h-12 rounded-xl text-base bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 transition-all"
|
||||||
|
onClick={() => void handlePay()}
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
{submitting ? "提交中..." : "确认支付"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
30
src/lib/hooks/useList.ts
Normal file
30
src/lib/hooks/useList.ts
Normal file
@@ -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<T>(fetcher: () => Promise<ApiResponse<T[]>>) {
|
||||||
|
const [data, setData] = useState<T[]>([])
|
||||||
|
const [status, setStatus] = useState<ListStatus>("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 }
|
||||||
|
}
|
||||||
70
src/lib/hooks/useMyOrderPage.ts
Normal file
70
src/lib/hooks/useMyOrderPage.ts
Normal file
@@ -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<ApiResponse<MyOrderPage>>,
|
||||||
|
) {
|
||||||
|
const [filters, setFilters] = useState<OrderFilters>(DEFAULT_FILTERS)
|
||||||
|
const [data, setData] = useState<OrderItem[]>([])
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
const [status, setStatus] = useState<ListStatus>("load")
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
|
const patch = useCallback(
|
||||||
|
(partial: Partial<OrderFilters>) =>
|
||||||
|
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 }
|
||||||
@@ -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<OrderFilters>(DEFAULT_FILTERS)
|
|
||||||
const [appliedFilters, setAppliedFilters] =
|
|
||||||
useState<OrderFilters>(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<OrderFilters>) =>
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
src/lib/hooks/useProducts.ts
Normal file
26
src/lib/hooks/useProducts.ts
Normal file
@@ -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<ProductItem[]>([])
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
46
src/lib/models/account.ts
Normal file
46
src/lib/models/account.ts
Normal file
@@ -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: "全部已过期" },
|
||||||
|
]
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
export type CashOutItem = {
|
export type CashOutItem = {
|
||||||
id: number
|
|
||||||
username: string
|
username: string
|
||||||
money: number
|
money: string
|
||||||
reason: string
|
remark: string | null
|
||||||
|
apply_reason: string
|
||||||
alipay_account: string
|
alipay_account: string
|
||||||
remark: string
|
|
||||||
status: string
|
status: string
|
||||||
create_time: string
|
create_time: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CashOutSubmitData = {
|
||||||
|
cash_out_money: string
|
||||||
|
reason: string
|
||||||
|
alipay_account: string
|
||||||
|
real_name: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// 优惠券(.NET /api/sells/v1/coupon/*,对应 CouponEntity)
|
|
||||||
export type CouponItem = {
|
export type CouponItem = {
|
||||||
Id: number
|
Id: number
|
||||||
Name: string
|
Name: string
|
||||||
@@ -26,3 +25,52 @@ export type CouponQueryParams = {
|
|||||||
PageSize?: number
|
PageSize?: number
|
||||||
KeyWord?: string
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,39 +1,8 @@
|
|||||||
// ==================== /user/ApiIndex 账户总览接口返回结构 ====================
|
import type { PhpUserModel } from "@/lib/models/user"
|
||||||
|
|
||||||
export type ApiIndexUserModel = {
|
// ==================== 账户总览接口返回结构 ====================
|
||||||
apikey: string
|
|
||||||
TenantId: number
|
export type ApiIndexUserModel = PhpUserModel
|
||||||
agent_id: number
|
|
||||||
jgday: number
|
|
||||||
LoginCode: string
|
|
||||||
Password: string
|
|
||||||
Name: string
|
|
||||||
Phone: string
|
|
||||||
Enabled: number
|
|
||||||
LastLoginDate: string
|
|
||||||
Sex: number
|
|
||||||
CreateType: number
|
|
||||||
ProductAccountCount: number
|
|
||||||
ExpiredProductAccountCount: number
|
|
||||||
RestAmount: number
|
|
||||||
ju_money: number
|
|
||||||
ConsumeAmount: number
|
|
||||||
Wx: string
|
|
||||||
QQ: string
|
|
||||||
Email: string
|
|
||||||
TestCountLimit: number
|
|
||||||
UseTestCount: number
|
|
||||||
ManagerId: number
|
|
||||||
ManagerName: string
|
|
||||||
id_code: string
|
|
||||||
is_verify: number
|
|
||||||
is_agent: number
|
|
||||||
parent_id: number
|
|
||||||
CreateTime: string
|
|
||||||
UpdateTime: string
|
|
||||||
DeleteTag: number
|
|
||||||
Id: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ApiIndexAccountModel = {
|
export type ApiIndexAccountModel = {
|
||||||
TotalCount: number
|
TotalCount: number
|
||||||
|
|||||||
19
src/lib/models/funds.ts
Normal file
19
src/lib/models/funds.ts
Normal file
@@ -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
|
||||||
@@ -12,12 +12,13 @@ export type OrderItem = {
|
|||||||
OrderAmount: number
|
OrderAmount: number
|
||||||
CouponAmount: number
|
CouponAmount: number
|
||||||
PaymentAmount: number
|
PaymentAmount: number
|
||||||
|
RefundCount: number
|
||||||
|
RefundAmount: number
|
||||||
|
DayCount: number
|
||||||
CreateTime: string
|
CreateTime: string
|
||||||
UpdateTime: string
|
UpdateTime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 下单接口(php-api /product/CreateOrder) ====================
|
|
||||||
|
|
||||||
export type CreateOrderRequest = {
|
export type CreateOrderRequest = {
|
||||||
PackageId: number
|
PackageId: number
|
||||||
OrderType: number
|
OrderType: number
|
||||||
@@ -45,6 +46,8 @@ export type OrderInfo = {
|
|||||||
OtherPayAmount: number
|
OtherPayAmount: number
|
||||||
CreateTime: string
|
CreateTime: string
|
||||||
PayType: number
|
PayType: number
|
||||||
|
PayChannel?: number
|
||||||
|
PaymentAmount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateOrderData = {
|
export type CreateOrderData = {
|
||||||
@@ -52,8 +55,6 @@ export type CreateOrderData = {
|
|||||||
PayData: string
|
PayData: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 管理端订单查询(.NET /api/course/v1/order/*) ====================
|
|
||||||
|
|
||||||
export type OrderQueryParams = {
|
export type OrderQueryParams = {
|
||||||
PageIndex?: number
|
PageIndex?: number
|
||||||
PageSize?: number
|
PageSize?: number
|
||||||
@@ -68,6 +69,20 @@ export type OrderQueryParams = {
|
|||||||
PackageNames?: string[]
|
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 = {
|
export type OrderFilters = {
|
||||||
orderType: string
|
orderType: string
|
||||||
productId: string
|
productId: string
|
||||||
@@ -118,60 +133,4 @@ export function totalConnections(item: OrderItem): number {
|
|||||||
return item.ConnectCount * item.AccountCount
|
return item.ConnectCount * item.AccountCount
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 表格展示辅助 ====================
|
export { formatDate, formatMoney } from "@/lib/utils"
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
export type RosOrderItem = {
|
export type RosOrderItem = {
|
||||||
id: number
|
id: string
|
||||||
|
user_id: string
|
||||||
username: string
|
username: string
|
||||||
|
product_id: string
|
||||||
product_name: string
|
product_name: string
|
||||||
real_price: number
|
order_price: string
|
||||||
|
real_price: string
|
||||||
|
channel: string | null
|
||||||
order_no: string
|
order_no: string
|
||||||
|
ali_wx_no: string | null
|
||||||
user: string
|
user: string
|
||||||
phone: string
|
phone: string
|
||||||
address: string
|
address: string
|
||||||
|
status: string
|
||||||
|
is_send: string
|
||||||
create_time: string
|
create_time: string
|
||||||
|
update_time: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// 用户(.NET /api/baseinfo/v1/user/*,对应 UserEntity)
|
|
||||||
export type UserItem = {
|
export type UserItem = {
|
||||||
Id: number
|
Id: number
|
||||||
LoginCode: string
|
LoginCode: string
|
||||||
@@ -14,3 +13,50 @@ export type UserQueryParams = {
|
|||||||
PageSize?: number
|
PageSize?: number
|
||||||
KeyWord?: string
|
KeyWord?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PhpUserModel = {
|
||||||
|
apikey: string
|
||||||
|
TenantId: number
|
||||||
|
agent_id: number
|
||||||
|
jgday: number
|
||||||
|
LoginCode: string
|
||||||
|
Password: string
|
||||||
|
Name: string
|
||||||
|
Phone: string
|
||||||
|
Enabled: number
|
||||||
|
LastLoginDate: string
|
||||||
|
Sex: number
|
||||||
|
CreateType: number
|
||||||
|
ProductAccountCount: number
|
||||||
|
ExpiredProductAccountCount: number
|
||||||
|
RestAmount: number
|
||||||
|
ju_money: number
|
||||||
|
ConsumeAmount: number
|
||||||
|
Wx?: string
|
||||||
|
QQ?: string
|
||||||
|
Email?: string
|
||||||
|
TestCountLimit: number
|
||||||
|
UseTestCount: number
|
||||||
|
ManagerId: number
|
||||||
|
ManagerName: string
|
||||||
|
id_code: string
|
||||||
|
is_verify: number
|
||||||
|
is_agent: number
|
||||||
|
parent_id: number
|
||||||
|
CreateTime: string
|
||||||
|
UpdateTime: string
|
||||||
|
DeleteTag: number
|
||||||
|
Id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateUserInfoParams = {
|
||||||
|
QQ: string
|
||||||
|
Wx: string
|
||||||
|
Email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateUserPwdParams = {
|
||||||
|
OldPwd: string
|
||||||
|
NewPwd: string
|
||||||
|
ConfirmPwd: string
|
||||||
|
}
|
||||||
|
|||||||
43
src/lib/recharge.ts
Normal file
43
src/lib/recharge.ts
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -54,3 +54,18 @@ export function estimateTotal(
|
|||||||
if (pagerTotal && pagerTotal > 0) return pagerTotal
|
if (pagerTotal && pagerTotal > 0) return pagerTotal
|
||||||
return itemCount > 0 ? (page - 1) * pageSize + itemCount : 0
|
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}`
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import HelpPage from "@/home/help"
|
|||||||
import IpLinePage from "@/home/ipLine"
|
import IpLinePage from "@/home/ipLine"
|
||||||
import LayoutPage from "@/home/layout"
|
import LayoutPage from "@/home/layout"
|
||||||
import LoginPage from "@/home/login"
|
import LoginPage from "@/home/login"
|
||||||
|
import PayPage from "@/home/pay"
|
||||||
import ProductPage from "@/home/product"
|
import ProductPage from "@/home/product"
|
||||||
import BuyPage from "@/home/product/buy"
|
import BuyPage from "@/home/product/buy"
|
||||||
import BuyStaticPage from "@/home/product/buyStatic"
|
import BuyStaticPage from "@/home/product/buyStatic"
|
||||||
@@ -48,6 +49,14 @@ export const router = createBrowserRouter([
|
|||||||
path: "/login",
|
path: "/login",
|
||||||
element: <LoginPage />,
|
element: <LoginPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/pay",
|
||||||
|
element: (
|
||||||
|
<AuthGuard>
|
||||||
|
<PayPage />
|
||||||
|
</AuthGuard>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
element: (
|
element: (
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ export default defineConfig({
|
|||||||
proxy: {
|
proxy: {
|
||||||
"/api": "http://192.168.3.6:5000",
|
"/api": "http://192.168.3.6:5000",
|
||||||
"/article": "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",
|
"/user": "http://192.168.3.6:5000",
|
||||||
"/product": {
|
"/product": {
|
||||||
target: "http://192.168.3.6:5000",
|
target: "http://192.168.3.6:5000",
|
||||||
@@ -45,6 +50,16 @@ export default defineConfig({
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
|
"/order": {
|
||||||
|
target: "https://php-api.juip.com",
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
"/ros": {
|
||||||
|
target: "https://php-api.juip.com",
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user