= [
+ {
+ title: '订单',
+ key: 'order',
+ width: 230,
+ fixed: 'left',
+ render: (_, row) => (
+
+ {row.order_no}
+
+ {orderStatusMap[row.order_status]?.label ?? row.order_status}
+
+ {row.finance_status === 'settled' ? '已结算' : '待结算'}
+
+
+
+ ),
+ },
+ {
+ title: '租客',
+ key: 'renter',
+ width: 160,
+ render: (_, row) => (
+
+ {formatText(row.renter_nickname)}
+ {formatText(row.renter_phone)}
+
+ ),
+ },
+ {
+ title: '号主',
+ key: 'owner',
+ width: 160,
+ render: (_, row) => (
+
+ {formatText(row.owner_nickname)}
+ {formatText(row.owner_phone)}
+
+ ),
+ },
+ {
+ title: '实付 / 退款',
+ key: 'amount',
+ width: 160,
+ align: 'right',
+ render: (_, row) => (
+
+ {formatMoney(row.paid_amount_cent)}
+
+ 退款 {formatMoney(row.refunded_amount_cent)}
+
+
+ ),
+ },
+ {
+ title: '号主结算',
+ key: 'owner_settle',
+ width: 150,
+ align: 'right',
+ render: (_, row) => (
+
+ {formatMoney(row.owner_effective_settlement_amount_cent)}
+
+ 钱包 {formatMoney(row.owner_wallet_income_amount_cent)}
+
+
+ ),
+ },
+ {
+ title: '平台净额',
+ dataIndex: 'platform_net_amount_cent',
+ width: 130,
+ align: 'right',
+ render: value => formatMoney(value),
+ },
+ {
+ title: '结算模式',
+ key: 'mode',
+ width: 150,
+ render: (_, row) => (
+
+ {formatText(row.settlement_mode)}
+ {formatText(row.offline_settlement_status)}
+
+ ),
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'created_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+ {
+ title: '结算时间',
+ dataIndex: 'settled_at',
+ width: 170,
+ render: value => (value ? formatDateTime(value) : '—'),
+ },
+]
+
+export function AdminFinanceDetailsPage() {
+ const [filters, setFilters] = useState(emptyFilters)
+ const [applied, setApplied] = useState(emptyFilters)
+ const [settlementStatus, setSettlementStatus] = useState()
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+ const permissions = useAdminSessionStore(state => state.admin?.permissions)
+ const canView = Boolean(permissions?.includes('*') || permissions?.includes('wallet:view'))
+
+ const query = useMemo(
+ () => ({
+ order_no: applied.order_no.trim() || undefined,
+ date_type: applied.date_type,
+ settlement_status: settlementStatus,
+ start_date: applied.range?.[0]?.format('YYYY-MM-DD'),
+ end_date: applied.range?.[1]?.format('YYYY-MM-DD'),
+ page,
+ page_size: pageSize,
+ }),
+ [applied, settlementStatus, page, pageSize]
+ )
+
+ const detailsQuery = useQuery({
+ queryKey: ['admin-finance-details', query],
+ queryFn: () => fetchFinanceDetails(query),
+ enabled: canView,
+ placeholderData: keepPreviousData,
+ })
+
+ if (!canView) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/features/admin/AdminPaymentsPage.tsx b/frontend/src/features/admin/AdminPaymentsPage.tsx
new file mode 100644
index 0000000..c6ae0d8
--- /dev/null
+++ b/frontend/src/features/admin/AdminPaymentsPage.tsx
@@ -0,0 +1,269 @@
+import { useMemo, useState } from 'react'
+import { keepPreviousData, useQuery } from '@tanstack/react-query'
+import { Alert, Button, Input, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
+import { ReloadOutlined } from '@ant-design/icons'
+
+import { fetchAdminPayments } from '@/features/admin/api'
+import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
+import { DataTable } from '@/features/admin/components/DataTable'
+import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
+import type { AdminPaymentQuery, AdminPaymentRow } from '@/features/admin/types'
+import { useAdminSessionStore } from '@/stores/adminSession'
+import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
+
+type Filters = Pick
+
+const emptyFilters: Filters = { order_no: '', biz_type: undefined, status: undefined, provider: undefined }
+
+const statusOptions = [
+ { value: 'pending', label: '待支付' },
+ { value: 'paid', label: '已支付' },
+ { value: 'failed', label: '支付失败' },
+ { value: 'closed', label: '已关闭' },
+]
+
+const bizTypeOptions = [
+ { value: 'order_pay', label: '订单支付' },
+ { value: 'main_recharge', label: '充值' },
+]
+
+const providerOptions = [
+ { value: 'leshua', label: '拉卡拉' },
+ { value: 'shuncheng', label: '顺诚' },
+ { value: 'mock', label: '模拟支付' },
+]
+
+const statusMeta: Record = {
+ pending: { label: '待支付', color: 'gold' },
+ paid: { label: '已支付', color: 'green' },
+ failed: { label: '支付失败', color: 'red' },
+ closed: { label: '已关闭', color: 'default' },
+}
+
+function statusTag(value?: string | null) {
+ if (!value) return 未知
+ const meta = statusMeta[value] ?? { label: value, color: 'default' }
+ return (
+
+ {meta.label}
+
+ )
+}
+
+function errorText(error: unknown) {
+ const response = (error as { response?: { data?: { message?: string } } })?.response
+ if (response?.data?.message) return response.data.message
+ return error instanceof Error ? error.message : '请求失败,请稍后重试'
+}
+
+const columns: TableColumnsType = [
+ {
+ title: '支付单号',
+ key: 'payment',
+ width: 240,
+ fixed: 'left',
+ render: (_, row) => (
+
+ {row.payment_no}
+ {formatText(row.order_no)}
+
+ ),
+ },
+ {
+ title: '用户',
+ key: 'user',
+ width: 170,
+ render: (_, row) => (
+
+ {formatText(row.user_phone)}
+ UID {row.user_id}
+
+ ),
+ },
+ {
+ title: '渠道 / 支付方式',
+ key: 'channel',
+ width: 170,
+ render: (_, row) => (
+
+ {formatText(row.provider)}
+ {formatText(row.pay_way)}
+
+ ),
+ },
+ {
+ title: '金额',
+ dataIndex: 'amount_cent',
+ width: 130,
+ align: 'right',
+ render: value => {formatMoney(value)},
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ width: 110,
+ render: value => statusTag(value),
+ },
+ {
+ title: '支付时间',
+ dataIndex: 'paid_at',
+ width: 170,
+ render: value => (value ? formatDateTime(value) : '—'),
+ },
+ {
+ title: '失败原因',
+ key: 'error',
+ render: (_, row) =>
+ row.error_message ? (
+
+ {row.error_code ? `${row.error_code} · ` : ''}
+ {row.error_message}
+
+ ) : (
+ —
+ ),
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'created_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+]
+
+export function AdminPaymentsPage() {
+ const [filters, setFilters] = useState(emptyFilters)
+ const [applied, setApplied] = useState(emptyFilters)
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+ const permissions = useAdminSessionStore(state => state.admin?.permissions)
+ const canView = Boolean(permissions?.includes('*') || permissions?.includes('wallet:view'))
+
+ const query = useMemo(
+ () => ({
+ order_no: applied.order_no?.trim() || undefined,
+ biz_type: applied.biz_type,
+ status: applied.status,
+ provider: applied.provider,
+ page,
+ page_size: pageSize,
+ }),
+ [applied, page, pageSize]
+ )
+
+ const paymentsQuery = useQuery({
+ queryKey: ['admin-payments', query],
+ queryFn: () => fetchAdminPayments(query),
+ enabled: canView,
+ placeholderData: keepPreviousData,
+ })
+
+ if (!canView) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/features/admin/AdminWalletLedgerPage.tsx b/frontend/src/features/admin/AdminWalletLedgerPage.tsx
new file mode 100644
index 0000000..ec9eb99
--- /dev/null
+++ b/frontend/src/features/admin/AdminWalletLedgerPage.tsx
@@ -0,0 +1,221 @@
+import { useState } from 'react'
+import { keepPreviousData, useQuery } from '@tanstack/react-query'
+import { Alert, Button, InputNumber, Select, Space, Tag, type TableColumnsType } from 'antd'
+import { ReloadOutlined } from '@ant-design/icons'
+
+import { fetchAdminWalletLedger } from '@/features/admin/api'
+import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
+import { DataTable } from '@/features/admin/components/DataTable'
+import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
+import type { AdminWalletLedgerQuery, AdminWalletLedgerRow } from '@/features/admin/types'
+import { useAdminSessionStore } from '@/stores/adminSession'
+import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
+
+const bizTypeOptions = [
+ { value: 'order', label: '订单结算' },
+ { value: 'order_pay', label: '订单支付' },
+ { value: 'withdraw_apply', label: '提现申请' },
+ { value: 'withdraw_complete', label: '提现到账' },
+ { value: 'admin_pickup', label: '线下提号' },
+ { value: 'invite_rebate', label: '邀请返利' },
+]
+
+function errorText(error: unknown) {
+ const response = (error as { response?: { data?: { message?: string } } })?.response
+ if (response?.data?.message) return response.data.message
+ return error instanceof Error ? error.message : '请求失败,请稍后重试'
+}
+
+const columns: TableColumnsType = [
+ {
+ title: '流水号',
+ key: 'ledger',
+ width: 240,
+ fixed: 'left',
+ render: (_, row) => (
+
+ {row.ledger_no}
+ {formatText(row.order_no)}
+
+ ),
+ },
+ {
+ title: '用户',
+ key: 'user',
+ width: 180,
+ render: (_, row) => (
+
+ {row.user_nickname || formatText(row.user_phone)}
+ UID {row.user_id}
+
+ ),
+ },
+ {
+ title: '业务',
+ dataIndex: 'biz_type',
+ width: 140,
+ render: value => {formatText(value)},
+ },
+ {
+ title: '方向',
+ dataIndex: 'direction',
+ width: 90,
+ render: value => (
+
+ {value === 'income' ? '入账' : '出账'}
+
+ ),
+ },
+ {
+ title: '金额',
+ dataIndex: 'amount_cent',
+ width: 130,
+ align: 'right',
+ render: (value: number, row) => (
+
+ {row.direction === 'income' ? '+' : '-'}
+ {formatMoney(Math.abs(value))}
+
+ ),
+ },
+ {
+ title: '变化后余额',
+ dataIndex: 'balance_after_cent',
+ width: 140,
+ align: 'right',
+ render: value => formatMoney(value),
+ },
+ {
+ title: '备注',
+ dataIndex: 'remark',
+ ellipsis: true,
+ render: value => formatText(value),
+ },
+ {
+ title: '时间',
+ dataIndex: 'created_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+]
+
+export function AdminWalletLedgerPage() {
+ const [filters, setFilters] = useState<{ user_id?: number; order_id?: number; biz_type?: string }>({})
+ const [applied, setApplied] = useState(filters)
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+ const permissions = useAdminSessionStore(state => state.admin?.permissions)
+ const canView = Boolean(permissions?.includes('*') || permissions?.includes('wallet:view'))
+
+ const query: AdminWalletLedgerQuery = {
+ user_id: applied.user_id,
+ order_id: applied.order_id,
+ biz_type: applied.biz_type,
+ page,
+ page_size: pageSize,
+ }
+
+ const ledgerQuery = useQuery({
+ queryKey: ['admin-wallet-ledger', query],
+ queryFn: () => fetchAdminWalletLedger(query),
+ enabled: canView,
+ placeholderData: keepPreviousData,
+ })
+
+ if (!canView) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+ 共 {ledgerQuery.data?.total ?? 0} 条
+
+ }
+ extra={
+ }
+ loading={ledgerQuery.isFetching}
+ onClick={() => void ledgerQuery.refetch()}
+ >
+ 刷新
+
+ }
+ />
+
+ {
+ setPage(1)
+ setApplied(filters)
+ }}
+ onReset={() => {
+ setFilters({})
+ setApplied({})
+ setPage(1)
+ }}
+ >
+
+ setFilters(current => ({ ...current, user_id: value ?? undefined }))}
+ />
+
+
+ setFilters(current => ({ ...current, order_id: value ?? undefined }))}
+ />
+
+
+
+
+
+ {ledgerQuery.isError ? (
+
+ ) : null}
+
+
+ columns={columns}
+ dataSource={ledgerQuery.data?.items ?? []}
+ rowKey="id"
+ loading={ledgerQuery.isFetching}
+ total={ledgerQuery.data?.total ?? 0}
+ page={ledgerQuery.data?.page ?? page}
+ pageSize={ledgerQuery.data?.page_size ?? pageSize}
+ onPageChange={(nextPage, nextSize) => {
+ setPage(nextPage)
+ setPageSize(nextSize)
+ }}
+ emptyDescription="还没有钱包流水"
+ />
+
+ )
+}
diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts
index 79c7047..f608ad2 100644
--- a/frontend/src/features/admin/api.ts
+++ b/frontend/src/features/admin/api.ts
@@ -98,6 +98,10 @@ import type {
FinanceDetailPage,
FinanceDisbursementPage,
FinanceDisbursementQuery,
+ AdminPaymentPage,
+ AdminPaymentQuery,
+ AdminWalletLedgerPage,
+ AdminWalletLedgerQuery,
ManualDisbursementPayload,
OperatingExpensePage,
OperatingExpensePayload,
@@ -1365,3 +1369,30 @@ export async function voidOperatingExpense(id: number | string, reason: string)
)
return data.data
}
+
+export async function fetchAdminPayments(query: AdminPaymentQuery = {}) {
+ const { data } = await adminClient.get>>(
+ '/admin/payments',
+ { params: compactParams(query) }
+ )
+ return {
+ items: data.data.items ?? [],
+ total: data.data.total ?? 0,
+ page: data.data.page ?? query.page ?? 1,
+ page_size: data.data.page_size ?? query.page_size ?? 20,
+ }
+}
+
+export async function fetchAdminWalletLedger(query: AdminWalletLedgerQuery = {}) {
+ const { data } = await adminClient.get>>(
+ '/admin/wallet/ledger',
+ { params: compactParams(query) }
+ )
+ return {
+ items: data.data.items ?? [],
+ total: data.data.total ?? 0,
+ page: data.data.page ?? query.page ?? 1,
+ page_size: data.data.page_size ?? query.page_size ?? 20,
+ }
+}
+
diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts
index 4eeaa6d..d62ce7f 100644
--- a/frontend/src/features/admin/types.ts
+++ b/frontend/src/features/admin/types.ts
@@ -534,6 +534,81 @@ export interface FinanceDetailPage {
page_size: number
}
+export interface AdminPaymentRow {
+ id: number
+ payment_no: string
+ order_id: number
+ order_no: string
+ user_id: number
+ payment_config_id: number
+ user_phone: string
+ provider: string
+ merchant_id: string
+ third_order_id: string
+ provider_order_id: string
+ pay_way: string
+ amount_cent: number
+ biz_type: string
+ status: string
+ error_code: string
+ error_message: string
+ paid_at?: string | null
+ notified_at?: string | null
+ created_at: string
+ updated_at: string
+}
+
+export interface AdminPaymentPage {
+ items: AdminPaymentRow[]
+ total: number
+ page: number
+ page_size: number
+}
+
+export interface AdminPaymentQuery {
+ user_id?: number
+ order_id?: number
+ order_no?: string
+ biz_type?: string
+ status?: string
+ provider?: string
+ page?: number
+ page_size?: number
+}
+
+export interface AdminWalletLedgerRow {
+ id: number
+ ledger_no: string
+ user_id: number
+ user_phone: string
+ user_nickname: string
+ order_id?: number | null
+ order_no: string
+ direction: string
+ amount_cent: number
+ balance_after_cent: number
+ balance_type: string
+ biz_type: string
+ biz_no: string
+ remark: string
+ created_at: string
+}
+
+export interface AdminWalletLedgerPage {
+ items: AdminWalletLedgerRow[]
+ total: number
+ page: number
+ page_size: number
+}
+
+export interface AdminWalletLedgerQuery {
+ user_id?: number
+ order_id?: number
+ biz_type?: string
+ page?: number
+ page_size?: number
+}
+
export interface AdminAnnouncementQuery {
category?: string
status?: string
diff --git a/frontend/src/features/mobile/MobileWithdrawalPage.tsx b/frontend/src/features/mobile/MobileWithdrawalPage.tsx
new file mode 100644
index 0000000..0a44813
--- /dev/null
+++ b/frontend/src/features/mobile/MobileWithdrawalPage.tsx
@@ -0,0 +1,231 @@
+import { useEffect, useMemo, useState } from 'react'
+import { useQuery, useQueryClient } from '@tanstack/react-query'
+import {
+ Button,
+ Card,
+ Dialog,
+ ErrorBlock,
+ Form,
+ Input,
+ List,
+ NavBar,
+ Selector,
+ Skeleton,
+ Tag,
+ Toast,
+} from 'antd-mobile'
+import { useNavigate } from 'react-router-dom'
+
+import {
+ cancelWithdrawal,
+ createWithdrawal,
+ fetchPaymentAccounts,
+ fetchWalletBalance,
+ fetchWithdrawals,
+} from '@/features/wallet/api'
+import { paymentAccountTypeLabel, withdrawalStatusMeta } from '@/features/wallet/labels'
+import { useSessionStore } from '@/stores/session'
+import { formatDateTime, formatMoney } from '@/shared/utils/format'
+
+interface WithdrawFormValues {
+ amount: string
+}
+
+function errorText(error: unknown) {
+ const response = (error as { response?: { data?: { message?: string } } })?.response
+ if (response?.data?.message) return response.data.message
+ return error instanceof Error ? error.message : '请求失败,请稍后重试'
+}
+
+export function MobileWithdrawalPage() {
+ const navigate = useNavigate()
+ const queryClient = useQueryClient()
+ const [form] = Form.useForm()
+ const [accountId, setAccountId] = useState()
+ const [submitting, setSubmitting] = useState(false)
+ const user = useSessionStore(state => state.user)
+
+ const balanceQuery = useQuery({
+ queryKey: ['wallet-balance'],
+ queryFn: fetchWalletBalance,
+ enabled: Boolean(user),
+ })
+ const accountsQuery = useQuery({
+ queryKey: ['payment-accounts', 1],
+ queryFn: () => fetchPaymentAccounts(1, 50),
+ enabled: Boolean(user),
+ })
+ const withdrawalsQuery = useQuery({
+ queryKey: ['withdrawals', 1, 20],
+ queryFn: () => fetchWithdrawals(1, 20),
+ enabled: Boolean(user),
+ })
+
+ const available = balanceQuery.data?.available_balance_cent ?? 0
+ const accounts = useMemo(() => accountsQuery.data?.items ?? [], [accountsQuery.data])
+ const withdrawals = withdrawalsQuery.data?.items ?? []
+
+ useEffect(() => {
+ if (accountId) return
+ const preferred = accounts.find(item => item.is_default) ?? accounts[0]
+ if (preferred) setAccountId(preferred.id)
+ }, [accounts, accountId])
+
+ const submit = async () => {
+ const values = await form.validateFields()
+ if (!accountId) {
+ Toast.show({ content: '请先选择收款账户' })
+ return
+ }
+ const amountCent = Math.round(Number(values.amount) * 100)
+ if (!Number.isFinite(amountCent) || amountCent <= 0) {
+ Toast.show({ content: '请输入正确的提现金额' })
+ return
+ }
+ if (amountCent > available) {
+ Toast.show({ content: '提现金额不能超过可用余额' })
+ return
+ }
+ setSubmitting(true)
+ try {
+ await createWithdrawal({ payment_account_id: accountId, amount_cent: amountCent })
+ Toast.show({ icon: 'success', content: '提现申请已提交' })
+ form.setFieldValue('amount', '')
+ await Promise.all([
+ queryClient.invalidateQueries({ queryKey: ['withdrawals'] }),
+ queryClient.invalidateQueries({ queryKey: ['wallet-balance'] }),
+ queryClient.invalidateQueries({ queryKey: ['wallet-ledger'] }),
+ ])
+ } catch (error) {
+ Toast.show({ content: errorText(error) })
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ const cancel = async (id: number, withdrawNo: string) => {
+ const confirmed = await Dialog.confirm({
+ content: `确认取消提现 ${withdrawNo}?取消后金额退回可用余额。`,
+ confirmText: '取消提现',
+ cancelText: '返回',
+ })
+ if (!confirmed) return
+ try {
+ await cancelWithdrawal(id)
+ Toast.show({ icon: 'success', content: '提现已取消' })
+ await Promise.all([
+ queryClient.invalidateQueries({ queryKey: ['withdrawals'] }),
+ queryClient.invalidateQueries({ queryKey: ['wallet-balance'] }),
+ ])
+ } catch (error) {
+ Toast.show({ content: errorText(error) })
+ }
+ }
+
+ return (
+
+
navigate('/m/profile')}>提现
+
+
+ 可用余额
+ {balanceQuery.isPending ? (
+
+ ) : (
+ {formatMoney(available)}
+ )}
+
+
+
+ {accounts.length === 0 && !accountsQuery.isPending ? (
+
+ ) : (
+ <>
+
+ {accountsQuery.isPending ? (
+
+ ) : (
+ setAccountId(values[0] ? Number(values[0]) : undefined)}
+ options={accounts.map(account => ({
+ label: `${paymentAccountTypeLabel(account.account_type)} · ${account.account_name}`,
+ description: account.account_no,
+ value: String(account.id),
+ }))}
+ />
+ )}
+
+
+
+
+
+
+
+
+ 可提现 {formatMoney(available)},当前手续费 0%,到账金额以审核结果为准。
+
+
+
+ >
+ )}
+
+
+ {withdrawalsQuery.isPending ? (
+
+ ) : withdrawals.length === 0 ? (
+
+ ) : (
+
+ {withdrawals.map(item => {
+ const meta = withdrawalStatusMeta(item.status)
+ return (
+
+ {meta.label}
+
+ }
+ clickable={item.status === 'pending'}
+ onClick={() => {
+ if (item.status === 'pending') void cancel(item.id, item.withdraw_no)
+ }}
+ >
+ {formatMoney(item.amount_cent)}
+
+ {item.withdraw_no} · 到账 {formatMoney(item.actual_amount_cent)}
+
+
+ )
+ })}
+
+ )}
+
+
+ )
+}
diff --git a/frontend/src/features/seller/SellerEarningsPage.tsx b/frontend/src/features/seller/SellerEarningsPage.tsx
new file mode 100644
index 0000000..52f9a7c
--- /dev/null
+++ b/frontend/src/features/seller/SellerEarningsPage.tsx
@@ -0,0 +1,161 @@
+import { useState } from 'react'
+import { keepPreviousData, useQuery } from '@tanstack/react-query'
+import { Alert, Button, Space, Table, Tag, Typography, type TableColumnsType } from 'antd'
+import { ReloadOutlined } from '@ant-design/icons'
+import { useNavigate } from 'react-router-dom'
+
+import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
+import { fetchWalletBalance, fetchWalletLedger } from '@/features/wallet/api'
+import { ledgerBalanceTypeLabel, walletBizTypeLabel } from '@/features/wallet/labels'
+import type { WalletLedger } from '@/features/wallet/types'
+import { useSessionStore } from '@/stores/session'
+import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
+
+const columns: TableColumnsType = [
+ {
+ title: '流水号',
+ dataIndex: 'ledger_no',
+ width: 220,
+ render: value => {formatText(value)},
+ },
+ {
+ title: '业务',
+ dataIndex: 'biz_type',
+ width: 150,
+ render: value => {walletBizTypeLabel(value)},
+ },
+ {
+ title: '方向',
+ dataIndex: 'direction',
+ width: 90,
+ render: value => (
+
+ {value === 'income' ? '入账' : '出账'}
+
+ ),
+ },
+ {
+ title: '金额',
+ dataIndex: 'amount_cent',
+ width: 140,
+ align: 'right',
+ render: (value: number, row) => (
+
+ {row.direction === 'income' ? '+' : '-'}
+ {formatMoney(Math.abs(value))}
+
+ ),
+ },
+ {
+ title: '余额类型',
+ dataIndex: 'balance_type',
+ width: 120,
+ render: value => (
+ {ledgerBalanceTypeLabel(value)}
+ ),
+ },
+ {
+ title: '变化后余额',
+ dataIndex: 'balance_after_cent',
+ width: 150,
+ align: 'right',
+ render: value => formatMoney(value),
+ },
+ {
+ title: '备注',
+ dataIndex: 'remark',
+ ellipsis: true,
+ render: value => formatText(value),
+ },
+ {
+ title: '时间',
+ dataIndex: 'created_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+]
+
+export function SellerEarningsPage() {
+ const navigate = useNavigate()
+ const user = useSessionStore(state => state.user)
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+
+ const balanceQuery = useQuery({
+ queryKey: ['wallet-balance'],
+ queryFn: fetchWalletBalance,
+ enabled: Boolean(user),
+ })
+ const ledgerQuery = useQuery({
+ queryKey: ['wallet-ledger', page, pageSize],
+ queryFn: () => fetchWalletLedger(page, pageSize),
+ enabled: Boolean(user),
+ placeholderData: keepPreviousData,
+ })
+
+ return (
+
+
+ 可用 {formatMoney(balanceQuery.data?.available_balance_cent ?? 0)}
+ 冻结 {formatMoney(balanceQuery.data?.frozen_balance_cent ?? 0)}
+
+ }
+ extra={
+
+
+
+ }
+ loading={ledgerQuery.isFetching}
+ onClick={() => {
+ void ledgerQuery.refetch()
+ void balanceQuery.refetch()
+ }}
+ >
+ 刷新
+
+
+ }
+ />
+
+ {ledgerQuery.isError ? (
+
+ ) : null}
+
+
+ rowKey="id"
+ size="middle"
+ columns={columns}
+ dataSource={ledgerQuery.data?.items ?? []}
+ loading={ledgerQuery.isFetching}
+ scroll={{ x: 'max-content' }}
+ locale={{ emptyText: '还没有收益流水' }}
+ pagination={{
+ current: ledgerQuery.data?.page ?? page,
+ pageSize: ledgerQuery.data?.page_size ?? pageSize,
+ total: ledgerQuery.data?.total ?? 0,
+ showSizeChanger: true,
+ pageSizeOptions: ['10', '20', '50'],
+ showTotal: (count, range) => `第 ${range[0]}-${range[1]} 条 / 共 ${count} 条`,
+ onChange: (nextPage, nextSize) => {
+ setPage(nextPage)
+ setPageSize(nextSize)
+ },
+ }}
+ />
+
+ )
+}
diff --git a/frontend/src/features/seller/SellerHandoffsPage.tsx b/frontend/src/features/seller/SellerHandoffsPage.tsx
new file mode 100644
index 0000000..42f048f
--- /dev/null
+++ b/frontend/src/features/seller/SellerHandoffsPage.tsx
@@ -0,0 +1,195 @@
+import { useState } from 'react'
+import { keepPreviousData, useQuery } from '@tanstack/react-query'
+import { Alert, Button, Card, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
+import { ReloadOutlined, RightOutlined } from '@ant-design/icons'
+import { useNavigate } from 'react-router-dom'
+
+import { DataTable } from '@/features/admin/components/DataTable'
+import { fetchSellerHandoffs } from '@/features/seller/api'
+import { handoffStatusMeta, orderStatusMeta } from '@/features/orders/labels'
+import type { UserOrderListItem } from '@/features/orders/types'
+import { useSessionStore } from '@/stores/session'
+import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
+
+const statusOptions = [
+ { value: '', label: '全部' },
+ { value: 'pending_handoff', label: '待交接' },
+ { value: 'pending_checkout_accept', label: '待确认结账' },
+ { value: 'abnormal', label: '异常订单' },
+]
+
+export function SellerHandoffsPage() {
+ const navigate = useNavigate()
+ const user = useSessionStore(state => state.user)
+ const [status, setStatus] = useState('')
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+
+ const handoffsQuery = useQuery({
+ queryKey: ['seller-handoffs', status, page, pageSize],
+ queryFn: () => fetchSellerHandoffs(page, pageSize, status),
+ enabled: Boolean(user),
+ placeholderData: keepPreviousData,
+ })
+
+ const metrics = handoffsQuery.data?.metrics
+
+ const columns: TableColumnsType = [
+ {
+ title: '订单',
+ key: 'order',
+ width: 260,
+ render: (_, row) => (
+
+
+ {row.title}
+ {row.listing_no}
+
+ ),
+ },
+ {
+ title: '状态',
+ key: 'status',
+ width: 200,
+ render: (_, row) => (
+
+ {orderStatusMeta(row.status).label}
+
+ {handoffStatusMeta(row.handoff_status).label}
+
+
+ ),
+ },
+ {
+ title: '商品信息',
+ key: 'listing',
+ render: (_, row) => (
+
+ {formatText(row.server_region)}
+ {formatText(row.login_platform)}
+
+ ),
+ },
+ {
+ title: '金额',
+ key: 'amount',
+ width: 150,
+ align: 'right',
+ render: (_, row) => (
+
+ {formatMoney(row.display_amount_cent)}
+ 押金 {formatMoney(row.deposit_amount_cent)}
+
+ ),
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'created_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+ {
+ title: '操作',
+ key: 'actions',
+ width: 100,
+ fixed: 'right',
+ render: (_, row) => (
+ }
+ onClick={() => navigate(`/orders/${row.id}`)}
+ >
+ 处理
+
+ ),
+ },
+ ]
+
+ return (
+
+
+
+ JIEYOU · 卖家中心
+
+ 号主待办
+
+
+ 处理待交接、待确认结账与异常订单;点击订单进入详情完成后续操作。
+
+
+
+
+
+
+
+
+
+
+ 待交接
+ {metrics?.pending_handoff ?? 0}
+ 需要尽快上号
+
+
+ 待确认结账
+ {metrics?.pending_checkout ?? 0}
+ 核对消耗后确认
+
+
+ 异常订单
+ {metrics?.abnormal ?? 0}
+ 需与客服协同处理
+
+
+
+ {handoffsQuery.isError ? (
+
+ ) : null}
+
+
+
+ columns={columns}
+ dataSource={handoffsQuery.data?.items ?? []}
+ rowKey="id"
+ loading={handoffsQuery.isFetching}
+ total={handoffsQuery.data?.total ?? 0}
+ page={handoffsQuery.data?.page ?? page}
+ pageSize={handoffsQuery.data?.page_size ?? pageSize}
+ onPageChange={(nextPage, nextSize) => {
+ setPage(nextPage)
+ setPageSize(nextSize)
+ }}
+ emptyDescription="当前没有需要处理的订单"
+ />
+
+
+ )
+}
diff --git a/frontend/src/features/seller/SellerPickupsPage.tsx b/frontend/src/features/seller/SellerPickupsPage.tsx
new file mode 100644
index 0000000..c636fd4
--- /dev/null
+++ b/frontend/src/features/seller/SellerPickupsPage.tsx
@@ -0,0 +1,196 @@
+import { useState } from 'react'
+import { keepPreviousData, useQuery } from '@tanstack/react-query'
+import { Alert, Button, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
+import { ReloadOutlined } from '@ant-design/icons'
+
+import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
+import { DataTable } from '@/features/admin/components/DataTable'
+import { fetchSellerPickups } from '@/features/seller/api'
+import type { SellerPickup } from '@/features/seller/api'
+import { useSessionStore } from '@/stores/session'
+import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
+
+const statusMeta: Record = {
+ picking_up: { label: '提号中', color: 'gold' },
+ completed: { label: '已完成', color: 'green' },
+ cancelled: { label: '已取消', color: 'default' },
+}
+
+const settlementMeta: Record = {
+ none: { label: '未结算', color: 'default' },
+ pending: { label: '待结算', color: 'gold' },
+ settled: { label: '已结算', color: 'green' },
+}
+
+const statusOptions = [
+ { value: '', label: '全部状态' },
+ { value: 'picking_up', label: '提号中' },
+ { value: 'completed', label: '已完成' },
+ { value: 'cancelled', label: '已取消' },
+]
+
+const columns: TableColumnsType = [
+ {
+ title: '提号编号',
+ key: 'pickup',
+ width: 260,
+ fixed: 'left',
+ render: (_, row) => (
+
+ {row.pickup_no}
+ {formatText(row.listing_no)}
+
+ ),
+ },
+ {
+ title: '账号',
+ key: 'account',
+ render: (_, row) => (
+
+ {formatText(row.account_title)}
+
+ {formatText(row.server_region)} · {formatText(row.login_platform)}
+
+
+ ),
+ },
+ {
+ title: '操作人 / 平台',
+ key: 'operator',
+ width: 170,
+ render: (_, row) => (
+
+ {formatText(row.operator)}
+ {formatText(row.platform)}
+
+ ),
+ },
+ {
+ title: '订单金额',
+ dataIndex: 'order_amount_cent',
+ width: 130,
+ align: 'right',
+ render: value => formatMoney(value),
+ },
+ {
+ title: '应结算',
+ key: 'settle',
+ width: 150,
+ align: 'right',
+ render: (_, row) => (
+
+ {formatMoney(row.effective_settle_amount_cent)}
+ 原 {formatMoney(row.settle_amount_cent)}
+
+ ),
+ },
+ {
+ title: '状态',
+ key: 'status',
+ width: 190,
+ render: (_, row) => {
+ const status = statusMeta[row.status] ?? { label: row.status || '未知', color: 'default' }
+ const settlement =
+ settlementMeta[row.offline_settlement_status] ?? {
+ label: row.offline_settlement_status || '—',
+ color: 'default',
+ }
+ return (
+
+
+ {status.label}
+
+
+ {settlement.label}
+
+
+ )
+ },
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'created_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+ {
+ title: '完成时间',
+ dataIndex: 'completed_at',
+ width: 170,
+ render: value => (value ? formatDateTime(value) : '—'),
+ },
+]
+
+export function SellerPickupsPage() {
+ const user = useSessionStore(state => state.user)
+ const [status, setStatus] = useState('')
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(20)
+
+ const pickupsQuery = useQuery({
+ queryKey: ['seller-pickups', status, page, pageSize],
+ queryFn: () => fetchSellerPickups(page, pageSize, status),
+ enabled: Boolean(user),
+ placeholderData: keepPreviousData,
+ })
+
+ return (
+
+
+ 共 {pickupsQuery.data?.total ?? 0} 条
+
+ }
+ extra={
+
+
+ }
+ />
+
+ {pickupsQuery.isError ? (
+
+ ) : null}
+
+
+ columns={columns}
+ dataSource={pickupsQuery.data?.items ?? []}
+ rowKey="id"
+ loading={pickupsQuery.isFetching}
+ total={pickupsQuery.data?.total ?? 0}
+ page={pickupsQuery.data?.page ?? page}
+ pageSize={pickupsQuery.data?.page_size ?? pageSize}
+ onPageChange={(nextPage, nextSize) => {
+ setPage(nextPage)
+ setPageSize(nextSize)
+ }}
+ emptyDescription="还没有线下提号记录"
+ />
+
+ )
+}
diff --git a/frontend/src/features/seller/api.ts b/frontend/src/features/seller/api.ts
index 76f4974..e1440ff 100644
--- a/frontend/src/features/seller/api.ts
+++ b/frontend/src/features/seller/api.ts
@@ -59,3 +59,65 @@ export function publishResultText(listing: Listing, editing: boolean) {
if (published) return editing ? '修改成功,已上架' : '发布成功,已上架'
return editing ? '修改成功,等待后台审核' : '发布成功,等待后台审核'
}
+
+export interface SellerHandoffMetrics {
+ pending_handoff: number
+ pending_checkout: number
+ abnormal: number
+}
+
+export interface SellerHandoffPage {
+ items: T[]
+ total: number
+ page: number
+ page_size: number
+ metrics: SellerHandoffMetrics
+}
+
+export interface SellerPickup {
+ id: number
+ pickup_no: string
+ listing_no: string
+ account_title: string
+ server_region: string
+ login_platform: string
+ operator: string
+ platform: string
+ status: string
+ offline_settlement_status: string
+ order_amount_cent: number
+ settle_amount_cent: number
+ effective_settle_amount_cent: number
+ profit_amount_cent: number
+ created_at: string
+ completed_at?: string | null
+ cancelled_at?: string | null
+}
+
+export interface SellerPickupPage {
+ items: SellerPickup[]
+ total: number
+ page: number
+ page_size: number
+}
+
+export async function fetchSellerHandoffs(page = 1, pageSize = 20, status = '') {
+ return unwrapData>(
+ apiClient.get>>('/orders/handoffs', {
+ params: { page, page_size: pageSize, status: status || undefined },
+ })
+ )
+}
+
+export async function fetchSellerPickups(page = 1, pageSize = 20, status = '') {
+ return unwrapData>(
+ apiClient.get>>('/seller/pickups', {
+ params: { page, page_size: pageSize, status: status || undefined },
+ })
+ ).then(data => ({
+ items: data.items ?? [],
+ total: data.total ?? 0,
+ page: data.page ?? page,
+ page_size: data.page_size ?? pageSize,
+ }))
+}
diff --git a/frontend/src/features/wallet/PaymentAccountsPage.tsx b/frontend/src/features/wallet/PaymentAccountsPage.tsx
new file mode 100644
index 0000000..62d024e
--- /dev/null
+++ b/frontend/src/features/wallet/PaymentAccountsPage.tsx
@@ -0,0 +1,375 @@
+import { useState } from 'react'
+import { useQuery, useQueryClient } from '@tanstack/react-query'
+import {
+ App,
+ Button,
+ Card,
+ Empty,
+ Form,
+ Input,
+ Modal,
+ Select,
+ Space,
+ Tag,
+ Typography,
+ Upload,
+ type UploadFile,
+} from 'antd'
+import {
+ DeleteOutlined,
+ EditOutlined,
+ PlusOutlined,
+ ReloadOutlined,
+ StarFilled,
+ StarOutlined,
+} from '@ant-design/icons'
+import { useNavigate } from 'react-router-dom'
+
+import { uploadFile } from '@/shared/api/files'
+import {
+ createPaymentAccount,
+ deletePaymentAccount,
+ fetchPaymentAccounts,
+ setDefaultPaymentAccount,
+ updatePaymentAccount,
+} from '@/features/wallet/api'
+import { paymentAccountTypeLabel } from '@/features/wallet/labels'
+import type {
+ CreatePaymentAccountPayload,
+ PaymentAccount,
+ PaymentAccountType,
+} from '@/features/wallet/types'
+import { useSessionStore } from '@/stores/session'
+import { formatDateTime, formatText } from '@/shared/utils/format'
+
+interface AccountFormValues {
+ account_type: PaymentAccountType
+ account_name: string
+ account_no: string
+ bank_name?: string
+ bank_branch?: string
+}
+
+function errorText(error: unknown) {
+ const response = (error as { response?: { data?: { message?: string } } })?.response
+ if (response?.data?.message) return response.data.message
+ return error instanceof Error ? error.message : '请求失败,请稍后重试'
+}
+
+export function PaymentAccountsPage() {
+ const navigate = useNavigate()
+ const { message, modal } = App.useApp()
+ const queryClient = useQueryClient()
+ const [form] = Form.useForm()
+ const [editorOpen, setEditorOpen] = useState(false)
+ const [editing, setEditing] = useState()
+ const [certificates, setCertificates] = useState([])
+ const [uploading, setUploading] = useState(false)
+ const [submitting, setSubmitting] = useState(false)
+ const user = useSessionStore(state => state.user)
+ const accountType = Form.useWatch('account_type', form)
+
+ const accountsQuery = useQuery({
+ queryKey: ['payment-accounts', 1],
+ queryFn: () => fetchPaymentAccounts(1, 50),
+ enabled: Boolean(user),
+ })
+
+ const accounts = accountsQuery.data?.items ?? []
+
+ const invalidate = () => queryClient.invalidateQueries({ queryKey: ['payment-accounts'] })
+
+ const openCreate = () => {
+ setEditing(undefined)
+ setCertificates([])
+ form.setFieldsValue({
+ account_type: 'alipay',
+ account_name: '',
+ account_no: '',
+ bank_name: '',
+ bank_branch: '',
+ })
+ setEditorOpen(true)
+ }
+
+ const openEdit = (account: PaymentAccount) => {
+ setEditing(account)
+ setCertificates(account.certificate_urls ?? [])
+ form.setFieldsValue({
+ account_type: account.account_type,
+ account_name: account.account_name,
+ account_no: account.account_no,
+ bank_name: account.bank_name,
+ bank_branch: account.bank_branch,
+ })
+ setEditorOpen(true)
+ }
+
+ const save = async () => {
+ const values = await form.validateFields()
+ setSubmitting(true)
+ try {
+ if (editing) {
+ await updatePaymentAccount(editing.id, {
+ bank_branch: values.bank_branch?.trim(),
+ certificate_urls: certificates,
+ })
+ message.success('收款账户已更新')
+ } else {
+ const payload: CreatePaymentAccountPayload = {
+ account_type: values.account_type,
+ account_name: values.account_name.trim(),
+ account_no: values.account_no.trim(),
+ bank_name: values.bank_name?.trim(),
+ bank_branch: values.bank_branch?.trim(),
+ certificate_urls: certificates,
+ }
+ await createPaymentAccount(payload)
+ message.success('收款账户已添加')
+ }
+ setEditorOpen(false)
+ await invalidate()
+ } catch (error) {
+ message.error(errorText(error))
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ const remove = (account: PaymentAccount) => {
+ modal.confirm({
+ title: `确认删除收款账户「${account.account_name}」?`,
+ content: '删除后该账户不再出现在提现选择列表中,已提交的提现不受影响。',
+ okText: '删除',
+ okButtonProps: { danger: true },
+ cancelText: '取消',
+ onOk: async () => {
+ try {
+ await deletePaymentAccount(account.id)
+ message.success('收款账户已删除')
+ await invalidate()
+ } catch (error) {
+ message.error(errorText(error))
+ }
+ },
+ })
+ }
+
+ const setDefault = async (account: PaymentAccount) => {
+ try {
+ await setDefaultPaymentAccount(account.id)
+ message.success('已设为默认收款账户')
+ await invalidate()
+ } catch (error) {
+ message.error(errorText(error))
+ }
+ }
+
+ const handleUpload = async (file: File) => {
+ setUploading(true)
+ try {
+ const uploaded = await uploadFile(file, 'payment-cert')
+ setCertificates(current => [...current, uploaded.url])
+ message.success('收款凭证已上传')
+ } catch (error) {
+ message.error(errorText(error))
+ } finally {
+ setUploading(false)
+ }
+ return false
+ }
+
+ const fileList: UploadFile[] = certificates.map((url, index) => ({
+ uid: `${index}`,
+ name: `收款凭证 ${index + 1}`,
+ status: 'done',
+ url,
+ thumbUrl: url,
+ }))
+
+ return (
+
+
+
+ JIEYOU · 资金
+
+ 收款账户
+
+
+ 提现前需维护至少一个收款账户;默认账户会在提现表单中自动选中。
+
+
+
+ 可用账户
+ {accounts.length}
+
+ } onClick={openCreate}>
+ 添加账户
+
+
+
+
+
+
+
}
+ loading={accountsQuery.isFetching}
+ onClick={() => void accountsQuery.refetch()}
+ >
+ 刷新
+
+ }
+ >
+ {accounts.length === 0 ? (
+
+ ) : (
+
+ {accounts.map(account => (
+
+
+
+
+ {paymentAccountTypeLabel(account.account_type)}
+
+ {account.is_default ? (
+
+ 默认
+
+ ) : null}
+
+ {account.account_no}
+
+
{account.account_name}
+ {account.bank_name ? (
+
+ {account.bank_name}
+ {account.bank_branch ? ` · ${account.bank_branch}` : ''}
+
+ ) : null}
+
+ 添加于 {formatDateTime(account.created_at)}
+
+ {account.certificate_urls?.length ? (
+
+ 凭证 {account.certificate_urls.length} 张
+
+ ) : null}
+
+ : }
+ disabled={account.is_default}
+ onClick={() => void setDefault(account)}
+ >
+ 设为默认
+
+ }
+ onClick={() => openEdit(account)}
+ >
+ 编辑
+
+ }
+ onClick={() => remove(account)}
+ >
+ 删除
+
+
+
+ ))}
+
+ )}
+
+
+
void save()}
+ onCancel={() => (submitting ? undefined : setEditorOpen(false))}
+ maskClosable={!submitting}
+ >
+
+
+
+
+
+
+
+
+
+ {accountType === 'bank' ? (
+ <>
+
+
+
+
+
+
+ >
+ ) : null}
+
+ {
+ void handleUpload(file as File)
+ return false
+ }}
+ onRemove={file => {
+ const index = Number(file.uid)
+ setCertificates(current => current.filter((_, i) => i !== index))
+ }}
+ >
+ {certificates.length >= 5 || uploading ? null : (
+
+ )}
+
+
+
+
+
+
+ 当前共 {accounts.length} 个账户,其中默认账户 {accounts.filter(item => item.is_default).length} 个
+ {formatText('')}
+
+
+ )
+}
diff --git a/frontend/src/features/wallet/WalletPage.tsx b/frontend/src/features/wallet/WalletPage.tsx
new file mode 100644
index 0000000..b274037
--- /dev/null
+++ b/frontend/src/features/wallet/WalletPage.tsx
@@ -0,0 +1,215 @@
+import { useState } from 'react'
+import { keepPreviousData, useQuery } from '@tanstack/react-query'
+import { Alert, Button, Card, Space, Table, Tag, Typography, type TableColumnsType } from 'antd'
+import { ArrowDownOutlined, ArrowUpOutlined, ReloadOutlined } from '@ant-design/icons'
+import { useNavigate } from 'react-router-dom'
+
+import { fetchWalletBalance, fetchWalletLedger } from '@/features/wallet/api'
+import {
+ ledgerBalanceTypeLabel,
+ walletBizTypeLabel,
+ walletStatusLabel,
+} from '@/features/wallet/labels'
+import type { WalletLedger } from '@/features/wallet/types'
+import { useSessionStore } from '@/stores/session'
+import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
+
+const PAGE_SIZE = 20
+
+function directionTag(value?: string | null) {
+ if (value === 'income') {
+ return (
+ } bordered={false}>
+ 入账
+
+ )
+ }
+ return (
+ } bordered={false}>
+ 出账
+
+ )
+}
+
+const columns: TableColumnsType = [
+ {
+ title: '流水号',
+ dataIndex: 'ledger_no',
+ width: 210,
+ render: value => {formatText(value)},
+ },
+ {
+ title: '业务',
+ dataIndex: 'biz_type',
+ width: 140,
+ render: value => {walletBizTypeLabel(value)},
+ },
+ {
+ title: '方向',
+ dataIndex: 'direction',
+ width: 100,
+ render: value => directionTag(value),
+ },
+ {
+ title: '金额',
+ dataIndex: 'amount_cent',
+ width: 140,
+ align: 'right',
+ render: (value: number, row) => (
+
+ {row.direction === 'income' ? '+' : '-'}
+ {formatMoney(Math.abs(value))}
+
+ ),
+ },
+ {
+ title: '余额类型',
+ dataIndex: 'balance_type',
+ width: 120,
+ render: value => (
+ {ledgerBalanceTypeLabel(value)}
+ ),
+ },
+ {
+ title: '变化后余额',
+ dataIndex: 'balance_after_cent',
+ width: 150,
+ align: 'right',
+ render: value => formatMoney(value),
+ },
+ {
+ title: '备注',
+ dataIndex: 'remark',
+ ellipsis: true,
+ render: value => formatText(value),
+ },
+ {
+ title: '时间',
+ dataIndex: 'created_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+]
+
+export function WalletPage() {
+ const navigate = useNavigate()
+ const user = useSessionStore(state => state.user)
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(PAGE_SIZE)
+
+ const balanceQuery = useQuery({
+ queryKey: ['wallet-balance'],
+ queryFn: fetchWalletBalance,
+ enabled: Boolean(user),
+ })
+ const ledgerQuery = useQuery({
+ queryKey: ['wallet-ledger', page, pageSize],
+ queryFn: () => fetchWalletLedger(page, pageSize),
+ enabled: Boolean(user),
+ placeholderData: keepPreviousData,
+ })
+
+ const account = balanceQuery.data
+ const ledger = ledgerQuery.data
+
+ return (
+
+
+
+ JIEYOU · 资金
+
+ 资金账户
+
+
+ 查看卖家结算收入、冻结押金与每一笔资金变化;提现需先维护收款账户。
+
+
+
+ 当前可用
+ {formatMoney(account?.available_balance_cent ?? 0)}
+
+
+
+
+
+
+
+
+
+ 可用余额
+ {formatMoney(account?.available_balance_cent ?? 0)}
+ 可随时申请提现
+
+
+ 冻结余额
+ {formatMoney(account?.frozen_balance_cent ?? 0)}
+ 提现处理中或订单押金暂扣
+
+
+ 账户状态
+ {walletStatusLabel(account?.status)}
+ 异常时请联系客服核实
+
+
+
+ {balanceQuery.isError ? (
+
+ ) : null}
+
+
+
+ 资金流水
+
+
+ 共 {ledger?.total ?? 0} 条记录,最近变动优先展示。
+
+
+ }
+ extra={
+ }
+ loading={ledgerQuery.isFetching}
+ onClick={() => {
+ void ledgerQuery.refetch()
+ void balanceQuery.refetch()
+ }}
+ >
+ 刷新
+
+ }
+ >
+
+ rowKey="id"
+ size="middle"
+ columns={columns}
+ dataSource={ledger?.items ?? []}
+ loading={ledgerQuery.isFetching}
+ scroll={{ x: 'max-content' }}
+ locale={{ emptyText: '暂无资金流水' }}
+ pagination={{
+ current: ledger?.page ?? page,
+ pageSize: ledger?.page_size ?? pageSize,
+ total: ledger?.total ?? 0,
+ showSizeChanger: true,
+ pageSizeOptions: ['10', '20', '50'],
+ showTotal: (count, range) => `第 ${range[0]}-${range[1]} 条 / 共 ${count} 条`,
+ onChange: (nextPage, nextSize) => {
+ setPage(nextPage)
+ setPageSize(nextSize)
+ },
+ }}
+ />
+
+
+ )
+}
diff --git a/frontend/src/features/wallet/WithdrawalPage.tsx b/frontend/src/features/wallet/WithdrawalPage.tsx
new file mode 100644
index 0000000..e77cb97
--- /dev/null
+++ b/frontend/src/features/wallet/WithdrawalPage.tsx
@@ -0,0 +1,347 @@
+import { useEffect, useMemo, useState } from 'react'
+import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query'
+import {
+ Alert,
+ App,
+ Button,
+ Card,
+ Form,
+ InputNumber,
+ Select,
+ Space,
+ Table,
+ Tag,
+ Typography,
+ type TableColumnsType,
+} from 'antd'
+import { ReloadOutlined } from '@ant-design/icons'
+import { useNavigate } from 'react-router-dom'
+
+import {
+ cancelWithdrawal,
+ createWithdrawal,
+ fetchPaymentAccounts,
+ fetchWalletBalance,
+ fetchWithdrawals,
+} from '@/features/wallet/api'
+import {
+ paymentAccountTypeLabel,
+ withdrawalStatusMeta,
+} from '@/features/wallet/labels'
+import type { PaymentAccount, WithdrawalRecord } from '@/features/wallet/types'
+import { useSessionStore } from '@/stores/session'
+import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
+
+const PAGE_SIZE = 20
+
+interface WithdrawFormValues {
+ payment_account_id: number
+ amount_yuan: number
+}
+
+function errorText(error: unknown) {
+ const response = (error as { response?: { data?: { message?: string } } })?.response
+ if (response?.data?.message) return response.data.message
+ return error instanceof Error ? error.message : '请求失败,请稍后重试'
+}
+
+const columns: TableColumnsType = [
+ {
+ title: '提现单号',
+ dataIndex: 'withdraw_no',
+ width: 200,
+ render: value => {formatText(value)},
+ },
+ {
+ title: '提现金额',
+ dataIndex: 'amount_cent',
+ width: 130,
+ align: 'right',
+ render: value => {formatMoney(value)},
+ },
+ {
+ title: '手续费 / 到账',
+ key: 'fee',
+ width: 170,
+ align: 'right',
+ render: (_, row) => (
+
+ {formatMoney(row.actual_amount_cent)}
+ 手续费 {formatMoney(row.fee_cent)}
+
+ ),
+ },
+ {
+ title: '收款方式',
+ key: 'account',
+ width: 190,
+ render: (_, row) => (
+
+ {paymentAccountTypeLabel(row.account_type)}
+
+ {formatText(row.account_name)} · {formatText(row.account_no)}
+
+
+ ),
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ width: 110,
+ render: value => {
+ const meta = withdrawalStatusMeta(value)
+ return (
+
+ {meta.label}
+
+ )
+ },
+ },
+ {
+ title: '备注',
+ dataIndex: 'review_remark',
+ ellipsis: true,
+ render: value => formatText(value),
+ },
+ {
+ title: '申请时间',
+ dataIndex: 'created_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+]
+
+export function WithdrawalPage() {
+ const navigate = useNavigate()
+ const { message, modal } = App.useApp()
+ const queryClient = useQueryClient()
+ const [form] = Form.useForm()
+ const [page, setPage] = useState(1)
+ const [pageSize, setPageSize] = useState(PAGE_SIZE)
+ const [submitting, setSubmitting] = useState(false)
+ const user = useSessionStore(state => state.user)
+ const amountYuan = Form.useWatch('amount_yuan', form)
+
+ const balanceQuery = useQuery({
+ queryKey: ['wallet-balance'],
+ queryFn: fetchWalletBalance,
+ enabled: Boolean(user),
+ })
+ const accountsQuery = useQuery({
+ queryKey: ['payment-accounts', 1],
+ queryFn: () => fetchPaymentAccounts(1, 50),
+ enabled: Boolean(user),
+ })
+ const withdrawalsQuery = useQuery({
+ queryKey: ['withdrawals', page, pageSize],
+ queryFn: () => fetchWithdrawals(page, pageSize),
+ enabled: Boolean(user),
+ placeholderData: keepPreviousData,
+ })
+
+ const available = balanceQuery.data?.available_balance_cent ?? 0
+ const accounts: PaymentAccount[] = accountsQuery.data?.items ?? []
+ const withdrawals = withdrawalsQuery.data
+
+ const accountOptions = useMemo(
+ () =>
+ accounts.map(account => ({
+ value: account.id,
+ label: `${paymentAccountTypeLabel(account.account_type)} · ${account.account_name} (${account.account_no})`,
+ })),
+ [accounts]
+ )
+
+ useEffect(() => {
+ if (form.getFieldValue('payment_account_id')) return
+ const preferred = accounts.find(item => item.is_default) ?? accounts[0]
+ if (preferred) form.setFieldValue('payment_account_id', preferred.id)
+ }, [accounts, form])
+
+ const submit = async () => {
+ const values = await form.validateFields()
+ const amountCent = Math.round(Number(values.amount_yuan) * 100)
+ if (amountCent <= 0) {
+ message.warning('提现金额必须大于 0')
+ return
+ }
+ if (amountCent > available) {
+ message.warning('提现金额不能超过可用余额')
+ return
+ }
+ setSubmitting(true)
+ try {
+ await createWithdrawal({
+ payment_account_id: values.payment_account_id,
+ amount_cent: amountCent,
+ })
+ message.success('提现申请已提交,等待审核')
+ form.setFieldValue('amount_yuan', undefined)
+ await Promise.all([
+ queryClient.invalidateQueries({ queryKey: ['withdrawals'] }),
+ queryClient.invalidateQueries({ queryKey: ['wallet-balance'] }),
+ queryClient.invalidateQueries({ queryKey: ['wallet-ledger'] }),
+ ])
+ } catch (error) {
+ message.error(errorText(error))
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ const cancel = (record: WithdrawalRecord) => {
+ modal.confirm({
+ title: `确认取消提现「${record.withdraw_no}」?`,
+ content: '取消后冻结金额会退回可用余额,需重新提交提现申请。',
+ okText: '取消提现',
+ okButtonProps: { danger: true },
+ cancelText: '返回',
+ onOk: async () => {
+ try {
+ await cancelWithdrawal(record.id)
+ message.success('提现已取消')
+ await Promise.all([
+ queryClient.invalidateQueries({ queryKey: ['withdrawals'] }),
+ queryClient.invalidateQueries({ queryKey: ['wallet-balance'] }),
+ ])
+ } catch (error) {
+ message.error(errorText(error))
+ }
+ },
+ })
+ }
+
+ const rows: WithdrawalRecord[] = withdrawals?.items ?? []
+
+ return (
+
+
+
+ JIEYOU · 资金
+
+ 申请提现
+
+
+ 提现申请提交后进入人工审核,审核通过后由财务打款到所选收款账户。
+
+
+
+ 当前可用
+ {formatMoney(available)}
+
+
+
+
+
+
+
+ {accounts.length === 0 ? (
+
navigate('/wallet/payment-accounts')}>
+ 去添加
+
+ }
+ />
+ ) : null}
+
+
+
+
+
+
+
+
+
+ 预计到账 {formatMoney(Math.round(Number(amountYuan ?? 0) * 100))}
+
+
+
+
+
+ }
+ loading={withdrawalsQuery.isFetching}
+ onClick={() => void withdrawalsQuery.refetch()}
+ >
+ 刷新
+
+ }
+ >
+
+ rowKey="id"
+ size="middle"
+ columns={[
+ ...columns,
+ {
+ title: '操作',
+ key: 'actions',
+ width: 110,
+ fixed: 'right',
+ render: (_, row) =>
+ row.status === 'pending' ? (
+
+ ) : (
+ —
+ ),
+ },
+ ]}
+ dataSource={rows}
+ loading={withdrawalsQuery.isFetching}
+ scroll={{ x: 'max-content' }}
+ locale={{ emptyText: '还没有提现记录' }}
+ pagination={{
+ current: withdrawals?.page ?? page,
+ pageSize: withdrawals?.page_size ?? pageSize,
+ total: withdrawals?.total ?? 0,
+ showSizeChanger: true,
+ pageSizeOptions: ['10', '20', '50'],
+ showTotal: (count, range) => `第 ${range[0]}-${range[1]} 条 / 共 ${count} 条`,
+ onChange: (nextPage, nextSize) => {
+ setPage(nextPage)
+ setPageSize(nextSize)
+ },
+ }}
+ />
+
+
+ )
+}
diff --git a/frontend/src/features/wallet/api.ts b/frontend/src/features/wallet/api.ts
new file mode 100644
index 0000000..3555255
--- /dev/null
+++ b/frontend/src/features/wallet/api.ts
@@ -0,0 +1,95 @@
+import { apiClient, unwrapData } from '@/shared/api/client'
+import type { ApiResponse } from '@/shared/api/types'
+import type {
+ CreatePaymentAccountPayload,
+ CreateWithdrawalPayload,
+ PaymentAccount,
+ PaymentAccountPage,
+ UpdatePaymentAccountPayload,
+ WalletAccount,
+ WalletLedger,
+ WalletLedgerPage,
+ WithdrawalPage,
+ WithdrawalRecord,
+} from '@/features/wallet/types'
+
+export const WALLET_PAGE_SIZE = 20
+
+export function fetchWalletBalance() {
+ return unwrapData(apiClient.get>('/wallet/balance'))
+}
+
+export function fetchWalletLedger(page = 1, pageSize = WALLET_PAGE_SIZE) {
+ return unwrapData>(
+ apiClient.get>>('/wallet/ledger', {
+ params: { page, page_size: pageSize },
+ })
+ ).then(data => ({
+ items: (data.items ?? []) as WalletLedger[],
+ total: data.total ?? 0,
+ page: data.page ?? page,
+ page_size: data.page_size ?? pageSize,
+ }))
+}
+
+export function fetchPaymentAccounts(page = 1, pageSize = WALLET_PAGE_SIZE) {
+ return unwrapData>(
+ apiClient.get>>('/payment-accounts', {
+ params: { page, page_size: pageSize },
+ })
+ ).then(data => ({
+ items: (data.items ?? []) as PaymentAccount[],
+ total: data.total ?? 0,
+ page: data.page ?? page,
+ page_size: data.page_size ?? pageSize,
+ }))
+}
+
+export function createPaymentAccount(payload: CreatePaymentAccountPayload) {
+ return unwrapData(
+ apiClient.post>('/payment-accounts', payload)
+ )
+}
+
+export function updatePaymentAccount(id: number, payload: UpdatePaymentAccountPayload) {
+ return unwrapData(
+ apiClient.put>(`/payment-accounts/${id}`, payload)
+ )
+}
+
+export function deletePaymentAccount(id: number) {
+ return unwrapData<{ deleted: boolean }>(
+ apiClient.delete>(`/payment-accounts/${id}`)
+ )
+}
+
+export function setDefaultPaymentAccount(id: number) {
+ return unwrapData<{ updated: boolean }>(
+ apiClient.post>(`/payment-accounts/${id}/set-default`)
+ )
+}
+
+export function fetchWithdrawals(page = 1, pageSize = WALLET_PAGE_SIZE) {
+ return unwrapData>(
+ apiClient.get>>('/withdrawals', {
+ params: { page, page_size: pageSize },
+ })
+ ).then(data => ({
+ items: (data.items ?? []) as WithdrawalRecord[],
+ total: data.total ?? 0,
+ page: data.page ?? page,
+ page_size: data.page_size ?? pageSize,
+ }))
+}
+
+export function createWithdrawal(payload: CreateWithdrawalPayload) {
+ return unwrapData(
+ apiClient.post>('/withdrawals', payload)
+ )
+}
+
+export function cancelWithdrawal(id: number) {
+ return unwrapData<{ cancelled: boolean }>(
+ apiClient.post>(`/withdrawals/${id}/cancel`)
+ )
+}
diff --git a/frontend/src/features/wallet/labels.ts b/frontend/src/features/wallet/labels.ts
new file mode 100644
index 0000000..a1e2e5a
--- /dev/null
+++ b/frontend/src/features/wallet/labels.ts
@@ -0,0 +1,75 @@
+import type { PaymentAccountType } from '@/features/wallet/types'
+
+const bizTypeLabels: Record = {
+ order: '订单结算',
+ order_pay: '订单支付',
+ cancel_refund: '取消退款',
+ checkout_refund: '结账退款',
+ admin_refund: '后台退款',
+ arbitration_owner_income: '仲裁号主收入',
+ arbitration_release_frozen: '仲裁解冻',
+ dispute: '申诉',
+ withdraw_apply: '提现申请',
+ withdraw_freeze: '提现冻结',
+ withdraw_complete: '提现到账',
+ withdraw_reject: '提现驳回',
+ withdraw_cancel: '提现取消',
+ invite_rebate: '邀请返利',
+ main_recharge: '充值',
+ admin_pickup: '线下提号',
+ admin_pickup_settle: '线下提号结算',
+ admin_pickup_adjustment: '线下提号调整',
+ distribution_settlement: '分销结算',
+ distribution_reversal: '分销冲正',
+ site_income: '站点收入',
+ site_withdraw: '站点提现',
+ mohong_order: '摸红订单',
+}
+
+export function walletBizTypeLabel(value?: string | null) {
+ if (!value) return '其他'
+ return bizTypeLabels[value] ?? value
+}
+
+export function ledgerDirectionLabel(value?: string | null) {
+ if (value === 'income') return '入账'
+ if (value === 'expense') return '出账'
+ return value || '—'
+}
+
+export function ledgerBalanceTypeLabel(value?: string | null) {
+ if (value === 'available') return '可用余额'
+ if (value === 'frozen') return '冻结余额'
+ return value || '—'
+}
+
+const accountTypeLabels: Record = {
+ alipay: '支付宝',
+ wechat: '微信',
+ bank: '银行卡',
+}
+
+export function paymentAccountTypeLabel(value?: PaymentAccountType | string | null) {
+ if (!value) return '—'
+ return accountTypeLabels[value] ?? value
+}
+
+const withdrawalStatusLabels: Record = {
+ pending: { label: '待审核', color: 'gold' },
+ processing: { label: '打款中', color: 'blue' },
+ completed: { label: '已到账', color: 'green' },
+ rejected: { label: '已驳回', color: 'red' },
+ cancelled: { label: '已取消', color: 'default' },
+}
+
+export function withdrawalStatusMeta(value?: string | null) {
+ if (!value) return { label: '未知', color: 'default' }
+ return withdrawalStatusLabels[value] ?? { label: value, color: 'default' }
+}
+
+export function walletStatusLabel(value?: string | null) {
+ if (value === 'active') return '正常'
+ if (value === 'frozen') return '已冻结'
+ if (value === 'closed') return '已关闭'
+ return value || '—'
+}
diff --git a/frontend/src/features/wallet/types.ts b/frontend/src/features/wallet/types.ts
new file mode 100644
index 0000000..8bbaed7
--- /dev/null
+++ b/frontend/src/features/wallet/types.ts
@@ -0,0 +1,115 @@
+export type WalletStatus = 'active' | 'frozen' | 'closed' | string
+
+export type LedgerDirection = 'income' | 'expense' | string
+
+export type LedgerBalanceType = 'available' | 'frozen' | string
+
+export interface WalletAccount {
+ user_id: number
+ distribution_site_id?: number
+ available_balance_cent: number
+ frozen_balance_cent: number
+ status: WalletStatus
+}
+
+export interface WalletLedger {
+ id: number
+ ledger_no: string
+ user_id: number
+ distribution_site_id?: number
+ order_id?: number | null
+ direction: LedgerDirection
+ amount_cent: number
+ balance_after_cent: number
+ balance_type: LedgerBalanceType
+ biz_type: string
+ biz_no: string
+ remark: string
+ created_at: string
+}
+
+export interface WalletLedgerPage {
+ items: WalletLedger[]
+ total: number
+ page: number
+ page_size: number
+}
+
+export type PaymentAccountType = 'alipay' | 'wechat' | 'bank'
+
+export interface PaymentAccount {
+ id: number
+ user_id: number
+ account_type: PaymentAccountType
+ account_name: string
+ account_no: string
+ bank_name: string
+ bank_branch: string
+ certificate_urls: string[]
+ is_default: boolean
+ status: string
+ created_at: string
+ updated_at: string
+}
+
+export interface PaymentAccountPage {
+ items: PaymentAccount[]
+ total: number
+ page: number
+ page_size: number
+}
+
+export interface CreatePaymentAccountPayload {
+ account_type: PaymentAccountType
+ account_name: string
+ account_no: string
+ bank_name?: string
+ bank_branch?: string
+ certificate_urls?: string[]
+}
+
+export interface UpdatePaymentAccountPayload {
+ bank_branch?: string
+ certificate_urls?: string[]
+ is_default?: boolean
+}
+
+export type WithdrawalStatus =
+ | 'pending'
+ | 'processing'
+ | 'completed'
+ | 'rejected'
+ | 'cancelled'
+ | string
+
+export interface WithdrawalRecord {
+ id: number
+ withdraw_no: string
+ user_id: number
+ distribution_site_id?: number
+ amount_cent: number
+ fee_cent: number
+ actual_amount_cent: number
+ account_type: PaymentAccountType | string
+ account_name: string
+ account_no: string
+ bank_name: string
+ status: WithdrawalStatus
+ review_remark: string
+ created_at: string
+ updated_at: string
+ reviewed_at?: string | null
+ paid_at?: string | null
+}
+
+export interface WithdrawalPage {
+ items: WithdrawalRecord[]
+ total: number
+ page: number
+ page_size: number
+}
+
+export interface CreateWithdrawalPayload {
+ payment_account_id: number
+ amount_cent: number
+}
diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx
index 47f77ad..18c4e31 100644
--- a/frontend/src/layouts/AdminLayout.tsx
+++ b/frontend/src/layouts/AdminLayout.tsx
@@ -85,7 +85,10 @@ const allNavGroups: NavGroup[] = [
icon: ,
children: [
{ key: '/finance', label: '财务看板', permission: 'wallet:view' },
+ { key: '/finance/details', label: '资金明细', permission: 'wallet:view' },
{ key: '/finance/disbursements', label: '资金出款', permission: 'wallet:view' },
+ { key: '/payments', label: '支付流水', permission: 'wallet:view' },
+ { key: '/wallet-ledger', label: '钱包流水', permission: 'wallet:view' },
{
key: '/finance/operating-expenses',
label: '运营开支',
@@ -164,6 +167,9 @@ const sectionMeta: Record = {
'/listings/review': { title: '商品审核', description: '审核待上架商品与账号截图。' },
'/users': { title: '用户管理', description: '查看用户、实名状态与交易记录。' },
'/finance': { title: '财务看板', description: '查看收入、退款、结算与资金趋势。' },
+ '/finance/details': { title: '资金明细', description: '按订单核对实付、退款、号主结算与平台净额。' },
+ '/payments': { title: '支付流水', description: '核对渠道支付单、回调状态与失败原因。' },
+ '/wallet-ledger': { title: '钱包流水', description: '按用户、订单与业务类型核对余额变动。' },
'/finance/disbursements': { title: '资金出款', description: '查看代管出款、提现与线下出款明细。' },
'/finance/operating-expenses': { title: '运营开支', description: '登记与核对日常运营支出。' },
'/withdrawals': { title: '提现审核', description: '审核提现申请与打款确认。' },
diff --git a/frontend/src/layouts/StorefrontLayout.tsx b/frontend/src/layouts/StorefrontLayout.tsx
index 1f2700a..b6ac846 100644
--- a/frontend/src/layouts/StorefrontLayout.tsx
+++ b/frontend/src/layouts/StorefrontLayout.tsx
@@ -52,6 +52,26 @@ export function StorefrontLayout() {
label: '我的消息',
onClick: () => navigate('/messages'),
},
+ {
+ key: 'wallet',
+ label: '我的钱包',
+ onClick: () => navigate('/wallet'),
+ },
+ {
+ key: 'seller-earnings',
+ label: '卖家收益',
+ onClick: () => navigate('/seller/earnings'),
+ },
+ {
+ key: 'seller-handoffs',
+ label: '号主待办',
+ onClick: () => navigate('/seller/handoffs'),
+ },
+ {
+ key: 'seller-pickups',
+ label: '线下提号',
+ onClick: () => navigate('/seller/pickups'),
+ },
{
key: 'seller',
label: '卖家中心',
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index 734a74f..7e3cef3 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -4,6 +4,7 @@
@import './storefront-sections.css';
@import './storefront-listings.css';
@import './storefront-content.css';
+@import './wallet.css';
@import './auth.css';
@import './admin-misc.css';
@import './admin-login.css';
diff --git a/frontend/src/styles/wallet.css b/frontend/src/styles/wallet.css
new file mode 100644
index 0000000..4e2e638
--- /dev/null
+++ b/frontend/src/styles/wallet.css
@@ -0,0 +1,216 @@
+/* 资金域:钱包、收款账户、提现(PC 与移动端共用部分) */
+.wallet-page {
+ width: min(1180px, 100%);
+ margin: 0 auto;
+ padding: 32px 24px 64px;
+ display: grid;
+ gap: 20px;
+}
+
+.wallet-hero {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 24px;
+ padding: 28px 32px;
+ border: 1px solid var(--line);
+ border-radius: 14px;
+ background: linear-gradient(135deg, var(--paper-warm), #f3ece0);
+}
+
+.wallet-hero--compact {
+ align-items: center;
+ padding: 22px 26px;
+}
+
+.wallet-eyebrow {
+ font-size: 12px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: var(--ink-muted);
+}
+
+.wallet-title {
+ margin: 8px 0 6px !important;
+ font-size: 30px !important;
+}
+
+.wallet-desc {
+ margin-bottom: 0 !important;
+ max-width: 560px;
+ color: var(--ink-secondary);
+}
+
+.wallet-hero__action {
+ display: grid;
+ gap: 8px;
+ justify-items: end;
+ text-align: right;
+}
+
+.wallet-hero__action > span {
+ font-size: 12px;
+ letter-spacing: 0.08em;
+ color: var(--ink-muted);
+}
+
+.wallet-hero__action > strong {
+ font-family: 'Noto Serif SC', 'Songti SC', serif;
+ font-size: 32px;
+ color: var(--brand-ink);
+ line-height: 1.1;
+}
+
+.wallet-metrics {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 14px;
+}
+
+.wallet-metric {
+ display: grid;
+ gap: 4px;
+ padding: 18px 20px;
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ background: var(--paper-warm);
+}
+
+.wallet-metric > span {
+ font-size: 12px;
+ color: var(--ink-muted);
+}
+
+.wallet-metric > strong {
+ font-size: 22px;
+ color: var(--brand-ink);
+}
+
+.wallet-metric > small {
+ color: var(--ink-secondary);
+ font-size: 12px;
+}
+
+.wallet-ledger-card {
+ border-radius: 12px;
+}
+
+.wallet-ledger-card .ant-card-head-title {
+ white-space: normal;
+}
+
+.wallet-mono {
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 12px;
+ color: var(--ink-secondary);
+}
+
+.wallet-stack {
+ display: grid;
+ gap: 2px;
+}
+
+.wallet-muted {
+ font-size: 12px;
+ color: var(--ink-muted);
+}
+
+.wallet-amount-up {
+ color: var(--success);
+}
+
+.wallet-amount-down {
+ color: var(--danger);
+}
+
+.pay-account-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 14px;
+}
+
+.pay-account-card {
+ display: grid;
+ gap: 6px;
+ padding: 16px 18px;
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ background: var(--paper-warm);
+}
+
+.pay-account-card__head {
+ display: grid;
+ gap: 6px;
+}
+
+.pay-account-card > strong {
+ font-size: 16px;
+}
+
+.pay-account-card__bank {
+ font-size: 13px;
+ color: var(--ink-secondary);
+}
+
+.pay-account-card__time,
+.pay-account-card__cert {
+ font-size: 12px;
+ color: var(--ink-muted);
+}
+
+/* 移动端提现 */
+.mobile-wallet {
+ display: grid;
+ gap: 12px;
+ padding-bottom: 24px;
+}
+
+.mobile-wallet__balance .adm-card-body {
+ display: grid;
+ gap: 8px;
+ justify-items: start;
+}
+
+.mobile-wallet__label {
+ font-size: 12px;
+ color: var(--ink-muted);
+}
+
+.mobile-wallet__amount {
+ font-family: 'Noto Serif SC', 'Songti SC', serif;
+ font-size: 30px;
+ color: var(--brand-ink);
+}
+
+.mobile-wallet__hint {
+ margin-top: 8px;
+ font-size: 12px;
+ color: var(--ink-muted);
+ line-height: 1.6;
+}
+
+.mobile-wallet__sub {
+ font-size: 12px;
+ color: var(--ink-muted);
+}
+
+@media (max-width: 768px) {
+ .wallet-page {
+ padding: 20px 14px 48px;
+ }
+
+ .wallet-hero {
+ align-items: flex-start;
+ flex-direction: column;
+ padding: 20px;
+ }
+
+ .wallet-hero__action {
+ justify-items: start;
+ text-align: left;
+ }
+
+ .wallet-title {
+ font-size: 24px !important;
+ }
+}