diff --git a/package.json b/package.json index b30813c..96538f3 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "ant-design-vue": "^4.0.0", "china-area-data": "^5.0.1", "dayjs": "^1.11.21", + "echarts": "^5.6.0", "express": "^5.2.1", "html2canvas": "^1.4.1", "qs": "^6.15.0", diff --git a/src/pages/finance/payments/index.module.less b/src/pages/finance/payments/index.module.less new file mode 100644 index 0000000..38ddafa --- /dev/null +++ b/src/pages/finance/payments/index.module.less @@ -0,0 +1,46 @@ +// ===== 汇总(按图片:左对齐两段加粗数字) ===== +.summary { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0 32px; + flex-shrink: 0; + padding: 14px 15px; + background: #fff; + border-radius: 8px; + font-size: 14px; + color: rgba(0, 0, 0, 0.85); +} + +.summaryItem { + display: inline-flex; + align-items: baseline; +} + +.summaryLabel { + color: rgba(0, 0, 0, 0.65); +} + +.summaryValue { + font-weight: 600; + color: rgba(0, 0, 0, 0.85); + letter-spacing: 0.3px; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +// ===== 表格 body 容器:占满 flex 剩余空间 ===== +.tableBody { + flex: 1; + min-height: 0; + overflow: hidden; + + :global { + .ant-spin-nested-loading, + .ant-spin-container, + .ant-table, + .ant-table-container { + height: 100%; + } + } +} diff --git a/src/pages/finance/payments/index.tsx b/src/pages/finance/payments/index.tsx index d61e398..bb354fc 100644 --- a/src/pages/finance/payments/index.tsx +++ b/src/pages/finance/payments/index.tsx @@ -1,4 +1,10 @@ import { defineComponent } from 'vue'; +import { Button, Input, Table, Form, Space, Pagination, Select, DatePicker } from 'ant-design-vue'; +import { usePaymentsModel } from './model/usePaymentsModel'; +import { useContainerSize } from '@/hooks'; +import styles from './index.module.less'; + +const { RangePicker } = DatePicker; /** * 支付流水 @@ -6,9 +12,107 @@ import { defineComponent } from 'vue'; export default defineComponent({ name: 'FinancePayments', setup() { + const { + filterForm, + loading, + dataSource, + columns, + summary, + pagination, + handleSearch, + handleReset, + handlePageChange, + PAYMENT_TYPE_OPTIONS, + } = usePaymentsModel(); + + const { containerRef, height } = useContainerSize({ headerOffset: 55 }); + return () => (
-
支付流水 - 开发中
+ {/* ===== 筛选区 ===== */} +
+
+ + (filterForm.timeRange = val)} + /> + + + + + + + + + handleRangeChange(val)} + options={[...RANGE_OPTIONS]} + style={{ width: 120 }} + /> +
+ + {/* ===== 统计卡 ===== */} +
+ {statCards.map((item) => ( +
+
{item.label}
+
+ ¥{formatMoney(item.value)} +
+
+ {item.desc} +
+
+ ))} +
+ + {/* ===== 柱状图卡(echarts) ===== */} +
+
+ 📊 + 月度收支趋势(近 6 个月) +
+
{ + if (el) chartRef.value = el as HTMLElement; + }} + class={styles.chartEcharts} + /> +
+ + {/* ===== 月度收支明细 ===== */} +
+
月度收支明细
+ + + + ); + }; }, }); diff --git a/src/pages/finance/reports/model/useReportsModel.ts b/src/pages/finance/reports/model/useReportsModel.ts new file mode 100644 index 0000000..a3a232a --- /dev/null +++ b/src/pages/finance/reports/model/useReportsModel.ts @@ -0,0 +1,163 @@ +import { ref } from 'vue'; +import { useState } from '@/hooks'; + +// ============================================================ +// 常量 +// ============================================================ + +/** 时间范围选项 */ +export const RANGE_OPTIONS = [ + { value: 'month', label: '本月' }, + { value: '3m', label: '近 3 个月' }, + { value: '6m', label: '近 6 个月' }, + { value: 'year', label: '本年' }, +] as const; + +// ============================================================ +// 假数据(数值自洽:总收入 - 总提现 = 平台余额) +// ============================================================ + +/** 6 个月柱状图数据(月份标签 + 订单金额 + 净收入) */ +const CHART_DATA = [ + { month: '01月', order: 22500, netIncome: 19000, withdraw: 300 }, + { month: '02月', order: 24500, netIncome: 21500, withdraw: 1850 }, + { month: '03月', order: 24800, netIncome: 23200, withdraw: 1400 }, + { month: '04月', order: 26100, netIncome: 24900, withdraw: 1500 }, + { month: '05月', order: 28900, netIncome: 27400, withdraw: 1800 }, + { month: '06月', order: 32500, netIncome: 31860, withdraw: 2100 }, +]; + +/** 月度收支明细(最近 4 个月) */ +const DETAIL_DATA = [ + { + key: '2026-06', + month: '2026-06', + order: 32500, + refund: 640, + netIncome: 31860, + withdraw: 2100, + }, + { + key: '2026-05', + month: '2026-05', + order: 28900, + refund: 1500, + netIncome: 27400, + withdraw: 1800, + }, + { + key: '2026-04', + month: '2026-04', + order: 26100, + refund: 1200, + netIncome: 24900, + withdraw: 1500, + }, + { + key: '2026-03', + month: '2026-03', + order: 24800, + refund: 1600, + netIncome: 23200, + withdraw: 1400, + }, +]; + +/** 顶部 3 张统计卡数据 */ +const SUMMARY = { + totalIncome: 147860, // 总收入(净收入合计) + totalWithdraw: 8950, // 总提现 + totalBalance: 138910, // 平台余额 = 总收入 - 总提现 +}; + +// ============================================================ +// 工具 +// ============================================================ + +/** 千分位 + 保留 2 位小数 */ +function formatMoney(n: number): string { + return (n || 0).toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); +} + +// ============================================================ +// Model +// ============================================================ + +/** + * 账务报表数据模型 + */ +export function useReportsModel() { + // ===== 时间范围 ===== + const [range, setRange] = useState('month'); + + /** 切换时间范围(演示用:打日志,不改数据) */ + const handleRangeChange = (val: string) => { + setRange(val); + // TODO: 接入 API 时按 range 重新拉取汇总 / 图表 / 明细 + }; + + // ===== 汇总 ===== + const summary = ref({ ...SUMMARY }); + + // ===== 图表 ===== + const chartData = ref([...CHART_DATA]); + + // ===== 明细表 ===== + const detailColumns = [ + { + title: '月份', + dataIndex: 'month', + key: 'month', + width: 140, + align: 'left' as const, + }, + { + title: '订单金额', + dataIndex: 'order', + key: 'order', + align: 'right' as const, + customRender: ({ text }: { text: number }) => `¥${formatMoney(text)}`, + }, + { + title: '退款金额', + dataIndex: 'refund', + key: 'refund', + align: 'right' as const, + customRender: ({ text }: { text: number }) => `¥${formatMoney(text)}`, + customCell: (_record: any) => ({ style: { color: '#ff4d4f' } }), + }, + { + title: '净收入', + dataIndex: 'netIncome', + key: 'netIncome', + align: 'right' as const, + customRender: ({ text }: { text: number }) => `¥${formatMoney(text)}`, + customCell: (_record: any) => ({ + style: { color: '#52c41a', fontWeight: 600 }, + }), + }, + { + title: '提现金额', + dataIndex: 'withdraw', + key: 'withdraw', + align: 'right' as const, + customRender: ({ text }: { text: number }) => `¥${formatMoney(text)}`, + customCell: (_record: any) => ({ style: { color: '#fa8c16' } }), + }, + ]; + const detailData = ref([...DETAIL_DATA]); + + return { + range, + handleRangeChange, + summary, + chartData, + detailColumns, + detailData, + formatMoney, + RANGE_OPTIONS: RANGE_OPTIONS as any, + }; +} diff --git a/src/pages/finance/wallet/components/WalletDetailModal.module.less b/src/pages/finance/wallet/components/WalletDetailModal.module.less new file mode 100644 index 0000000..2abdd8f --- /dev/null +++ b/src/pages/finance/wallet/components/WalletDetailModal.module.less @@ -0,0 +1,62 @@ +// ===== 自定义标题栏 ===== +.modalHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 0 16px; + margin-bottom: 16px; + border-bottom: 1px solid #f0f0f0; +} + +.modalTitle { + font-size: 16px; + font-weight: 600; + color: rgba(0, 0, 0, 0.85); +} + +.closeBtn { + width: 28px; + height: 28px; + border: none; + background: transparent; + font-size: 22px; + line-height: 1; + color: rgba(0, 0, 0, 0.45); + cursor: pointer; + border-radius: 4px; + transition: all 0.2s; + + &:hover { + background: rgba(0, 0, 0, 0.04); + color: rgba(0, 0, 0, 0.85); + } +} + +// ===== 内容区 ===== +.modalBody { + padding-top: 0; +} + +// ===== 筛选区 ===== +.filterBar { + flex-shrink: 0; + padding: 16px 20px; + margin-bottom: 16px; + background: #fafafa; + border: 1px solid #f0f0f0; + border-radius: 6px; +} + +// ===== 表格区 ===== +.tableWrap { + flex: 1; + min-height: 0; +} + +// ===== Modal 外层包裹:去除 antd 默认内边距 ===== +/* .walletModalWrap { + :global(.ant-modal-body) { + padding-top: 16px; + padding-bottom: 20px; + } +} */ diff --git a/src/pages/finance/wallet/components/WalletDetailModal.tsx b/src/pages/finance/wallet/components/WalletDetailModal.tsx new file mode 100644 index 0000000..de45682 --- /dev/null +++ b/src/pages/finance/wallet/components/WalletDetailModal.tsx @@ -0,0 +1,276 @@ +import { defineComponent, ref, reactive, computed, watch } from 'vue'; +import { Modal, Table, Button, Select, Form, Space, DatePicker } from 'ant-design-vue'; +import { + useWalletModel, + TRANSACTION_TYPE_OPTIONS, + TRANSACTION_STATUS_OPTIONS, +} from '../model/useWalletModel'; +import styles from './WalletDetailModal.module.less'; + +const { RangePicker } = DatePicker; + +interface WalletDetailModalProps { + visible: boolean; + record: any; + onClose: () => void; +} + +/** + * 钱包交易明细弹窗 + */ +export default defineComponent({ + name: 'WalletDetailModal', + props: { + visible: { type: Boolean, default: false }, + record: { type: Object, default: () => ({}) }, + onClose: { type: Function, required: true }, + }, + setup(props: WalletDetailModalProps) { + const { loadTransactions, renderTransactionStatus } = useWalletModel(); + + // ===== 弹窗内筛选条件 ===== + const detailFilter = reactive({ + timeRange: null as [string, string] | null, + type: '', + status: '', + }); + + // ===== 加载明细 ===== + const loading = ref(false); + const allTransactions = ref([]); + + const fetchTransactions = async () => { + loading.value = true; + try { + allTransactions.value = await loadTransactions(props.record); + } finally { + loading.value = false; + } + }; + + /** + * 弹窗内筛选后的数据(用 computed 自动响应全量数据和筛选条件的变化) + * 真实场景应传给后端做查询;此处做前端过滤演示 + */ + const filteredTransactions = computed(() => { + return allTransactions.value.filter((item) => { + if (detailFilter.type && item.type !== detailFilter.type) return false; + if (detailFilter.status && item.status !== detailFilter.status) return false; + if (detailFilter.timeRange && detailFilter.timeRange.length === 2) { + const [start, end] = detailFilter.timeRange; + if (item.time < start || item.time > end) return false; + } + return true; + }); + }); + + const handleSearch = () => { + // computed 会自动响应;这里保留方法以便将来扩展(如后端查询) + }; + + const handleReset = () => { + detailFilter.timeRange = null; + detailFilter.type = ''; + detailFilter.status = ''; + }; + + // ===== 分页 ===== + const pagination = reactive({ + current: 1, + pageSize: 10, + total: 0, + showSizeChanger: true, + showTotal: (total: number) => `共 ${total} 条`, + }); + + /** 筛选条件变化时重置到第 1 页,并同步总数 */ + watch(filteredTransactions, (list) => { + pagination.total = list.length; + pagination.current = 1; + }); + + const handleTableChange = (pag: any) => { + pagination.current = pag.current; + pagination.pageSize = pag.pageSize; + }; + + /** 监听 visible 变化:打开时加载明细 */ + watch( + () => props.visible, + (val) => { + if (val) { + detailFilter.timeRange = null; + detailFilter.type = ''; + detailFilter.status = ''; + allTransactions.value = []; + fetchTransactions(); + } + }, + ); + + // ===== 表格列配置 ===== + const detailColumns = [ + { + title: '类型', + dataIndex: 'type', + key: 'type', + width: 120, + }, + { + title: '金额', + dataIndex: 'amount', + key: 'amount', + width: 110, + align: 'center' as const, + customRender: ({ text }: { text: number }) => { + const num = text || 0; + const isPlus = num > 0; + const isMinus = num < 0; + const color = isPlus ? '#52c41a' : isMinus ? '#ff4d4f' : 'rgba(0,0,0,0.85)'; + const sign = isPlus ? '+' : ''; + return ( + + {sign} + {num.toFixed(2)} + + ); + }, + }, + { + title: '余额', + dataIndex: 'balance', + key: 'balance', + width: 110, + align: 'center' as const, + customRender: ({ text }: { text: number }) => `¥${(text || 0).toFixed(2)}`, + }, + { + title: '可提现金额', + dataIndex: 'withdrawable', + key: 'withdrawable', + width: 130, + align: 'center' as const, + customRender: ({ text }: { text: number }) => `¥${(text || 0).toFixed(2)}`, + }, + { + title: '冻结金额', + dataIndex: 'frozen', + key: 'frozen', + width: 110, + align: 'center' as const, + customRender: ({ text }: { text: number }) => `¥${(text || 0).toFixed(2)}`, + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 90, + align: 'center' as const, + customRender: ({ text }: { text: string }) => renderTransactionStatus(text), + }, + { + title: '冻结时间', + dataIndex: 'frozenTime', + key: 'frozenTime', + width: 130, + align: 'center' as const, + }, + { + title: '解冻时间', + dataIndex: 'unfreezeTime', + key: 'unfreezeTime', + width: 130, + align: 'center' as const, + }, + { + title: '关联单号', + dataIndex: 'relatedNo', + key: 'relatedNo', + width: 130, + align: 'center' as const, + }, + { + title: '时间', + dataIndex: 'time', + key: 'time', + width: 130, + align: 'center' as const, + }, + ]; + + return () => ( + +
+ {/* ===== 弹窗内筛选区 ===== */} +
+ + + (detailFilter.timeRange = val)} + /> + + + (detailFilter.status = val || '')} + /> + + + + + + + + +
+ + {/* ===== 表格区 ===== */} +
+
+ + + + ); + }, +}); diff --git a/src/pages/finance/wallet/index.module.less b/src/pages/finance/wallet/index.module.less new file mode 100644 index 0000000..3dae585 --- /dev/null +++ b/src/pages/finance/wallet/index.module.less @@ -0,0 +1,107 @@ +// ===== 表格 body 容器:占满 flex 剩余空间,并确保 antd 嵌套 div 逐层传递 height: 100% ===== +.tableBody { + flex: 1; + min-height: 0; + overflow: hidden; + + :global { + .ant-spin-nested-loading, + .ant-spin-container, + .ant-table, + .ant-table-container { + height: 100%; + } + } +} + +// ===== 顶部 3 张统计卡 ===== +.statCards { + display: flex; + gap: 16px; + flex-shrink: 0; + margin-bottom: 16px; +} + +.statCard { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 16px; + padding: 22px 24px; + background: #fff; + border: 1px solid #f0f0f0; + border-radius: 10px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02); + transition: all 0.25s ease; + cursor: default; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); + } +} + +.statCardLeft { + flex-shrink: 0; +} + +.statIconWrap { + width: 52px; + height: 52px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + font-size: 26px; + transition: transform 0.25s ease; + + .statCard:hover & { + transform: scale(1.05); + } +} + +.statCardRight { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.statLabel { + font-size: 13px; + color: rgba(0, 0, 0, 0.55); + letter-spacing: 0.3px; +} + +.statValue { + font-size: 26px; + font-weight: 600; + line-height: 1.2; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + letter-spacing: 0.3px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +// ===== 下方白卡片:包裹筛选区 + 表格区 ===== +.tableSection { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + padding: 16px 20px; + background: #fff; + border: 1px solid #f0f0f0; + border-radius: 10px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02); + overflow: hidden; + + // 表格区紧贴筛选区下方,控制间距 + :global(.page-table) { + margin-top: 16px; + } +} diff --git a/src/pages/finance/wallet/index.tsx b/src/pages/finance/wallet/index.tsx index 7f2a94e..a2437fa 100644 --- a/src/pages/finance/wallet/index.tsx +++ b/src/pages/finance/wallet/index.tsx @@ -1,4 +1,43 @@ import { defineComponent } from 'vue'; +import { Button, Input, Table, Form, Space, Pagination } from 'ant-design-vue'; +import { WalletOutlined, LockOutlined, RiseOutlined } from '@ant-design/icons-vue'; +import { useWalletModel } from './model/useWalletModel'; +import { useContainerSize } from '@/hooks'; +import WalletDetailModal from './components/WalletDetailModal'; +import styles from './index.module.less'; + +interface StatCardItem { + key: string; + label: string; + value: number; + icon: any; + color: string; + bgColor: string; +} + +/** + * bodyCell 渲染函数 + */ +function renderBodyCell({ + column, + record, + onViewDetail, +}: { + column: any; + text: any; + record: any; + onViewDetail: (record: any) => void; +}) { + // 操作列 + if (column.key === 'action') { + return ( + + ); + } + return; +} /** * 用户钱包 @@ -6,10 +45,198 @@ import { defineComponent } from 'vue'; export default defineComponent({ name: 'FinanceWallet', setup() { - return () => ( -
-
用户钱包 - 开发中
-
- ); + const { + filterForm, + loading, + dataSource, + columns, + summary, + pagination, + detailVisible, + currentWallet, + handleSearch, + handleReset, + handlePageChange, + handleViewDetail, + handleCloseDetail, + } = useWalletModel(); + + const { containerRef, height } = useContainerSize({ headerOffset: 55 }); + + /** 最终表格列:模型列 + 操作 */ + const tableColumns = [ + ...columns, + { + title: '操作', + key: 'action', + width: 100, + fixed: 'right' as const, + align: 'center' as const, + }, + ]; + + return () => { + /** 顶部统计卡配置 */ + const statCards: StatCardItem[] = [ + { + key: 'totalBalance', + label: '总余额', + value: summary.value.totalBalance, + icon: WalletOutlined, + color: '#1677ff', + bgColor: 'rgba(22, 119, 255, 0.08)', + }, + { + key: 'frozenAmount', + label: '冻结金额', + value: summary.value.frozenAmount, + icon: LockOutlined, + color: '#fa8c16', + bgColor: 'rgba(250, 140, 22, 0.08)', + }, + { + key: 'totalWithdraw', + label: '累计提现', + value: summary.value.totalWithdraw, + icon: RiseOutlined, + color: '#52c41a', + bgColor: 'rgba(82, 196, 26, 0.08)', + }, + ]; + + return ( +
+ {/* ===== 顶部统计卡 ===== */} +
+ {statCards.map((item) => ( +
+
+
+ +
+
+
+
{item.label}
+
+ ¥ + {item.value.toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} +
+
+
+ ))} +
+ + {/* ===== 筛选区 ===== */} +
+
+ + + + + + + + + (filterForm.minBalance = val ?? '')} + onPressEnter={handleSearch} + allowClear + /> + + (filterForm.maxBalance = val ?? '')} + onPressEnter={handleSearch} + allowClear + /> + + + + + + + + + +
+ + {/* ===== 下方区域:筛选 + 表格(独立白卡片) ===== */} + + {/* ===== 表格区 ===== */} +
+
+
+ {{ + bodyCell: (args: any) => + renderBodyCell({ + ...args, + onViewDetail: handleViewDetail, + }), + }} +
+
+ + {/* 独立分页,右下方 */} +
+ `共 ${total} 条`} + onChange={handlePageChange} + onShowSizeChange={handlePageChange} + /> +
+
+ + {/* ===== 交易明细弹窗 ===== */} + +
+ ); + }; }, }); diff --git a/src/pages/finance/wallet/model/useWalletModel.ts b/src/pages/finance/wallet/model/useWalletModel.ts new file mode 100644 index 0000000..1da1173 --- /dev/null +++ b/src/pages/finance/wallet/model/useWalletModel.ts @@ -0,0 +1,349 @@ +import { computed, reactive, toRef, Ref, h } from 'vue'; +import { message, Tag } from 'ant-design-vue'; +import { useState, useDebounce, useThrottleFn } from '@/hooks'; + +// ============================================================ +// 常量 +// ============================================================ + +/** 明细类型选项(弹窗内筛选) */ +export const TRANSACTION_TYPE_OPTIONS = [ + { value: '', label: '全部' }, + { value: '报名收入', label: '报名收入' }, + { value: '提现', label: '提现' }, + { value: '取消报名退款', label: '取消报名退款' }, +] as const; + +/** 明细状态选项(弹窗内筛选) */ +export const TRANSACTION_STATUS_OPTIONS = [ + { value: '', label: '全部' }, + { value: '正常', label: '正常' }, + { value: '冻结中', label: '冻结中' }, + { value: '已解冻', label: '已解冻' }, +] as const; + +/** 明细状态映射(用于表格 Tag 渲染) */ +const TRANSACTION_STATUS_MAP: Record = { + 正常: { label: '正常', color: 'blue' }, + 冻结中: { label: '冻结中', color: 'orange' }, + 已解冻: { label: '已解冻', color: 'green' }, +}; + +// ============================================================ +// 顶部统计卡假数据 +// ============================================================ + +const MOCK_SUMMARY = { + totalBalance: 86520.0, + frozenAmount: 3200.0, + totalWithdraw: 63080.0, +}; + +// ============================================================ +// 假数据(钱包列表) +// ============================================================ + +const MOCK_DATA = [ + { + key: '1', + userId: 'U20260701001', + nickName: '张三', + phone: '12345678997', + balance: 0, + frozen: 0, + totalWithdraw: 0, + }, + { + key: '2', + userId: '', + nickName: '李四', + phone: '12345678998', + balance: 350, + frozen: 200, + totalWithdraw: 1450, + }, + { + key: '3', + userId: '', + nickName: '王五', + phone: '12345678999', + balance: 1280, + frozen: 0, + totalWithdraw: 800, + }, + { + key: '4', + userId: 'U20260701004', + nickName: '赵六', + phone: '13800138000', + balance: 560, + frozen: 100, + totalWithdraw: 200, + }, + { + key: '5', + userId: '', + nickName: '钱七', + phone: '13900139001', + balance: 4200, + frozen: 1500, + totalWithdraw: 5600, + }, + { + key: '6', + userId: '', + nickName: '孙八', + phone: '12345678997', + balance: 880, + frozen: 0, + totalWithdraw: 300, + }, +]; + +// ============================================================ +// 交易明细假数据(所有用户共用,演示用) +// ============================================================ + +const MOCK_TRANSACTIONS: any[] = [ + { + key: 't-1', + type: '报名收入', + amount: 100, + balance: 600, + withdrawable: 500, + frozen: 100, + status: '已解冻', + frozenTime: '2026-06-10 08:00', + unfreezeTime: '2026-06-12 08:00', + relatedNo: 'xxxxxxxx', + time: '2026-06-10 08:00', + }, + { + key: 't-2', + type: '提现', + amount: -500, + balance: 1100, + withdrawable: 500, + frozen: 600, + status: '冻结中', + frozenTime: '2026-06-10 06:00', + unfreezeTime: '-', + relatedNo: 'xxxxxxxx', + time: '2026-06-10 06:00', + }, + { + key: 't-3', + type: '取消报名退款', + amount: -100, + balance: 500, + withdrawable: 500, + frozen: 0, + status: '正常', + frozenTime: '-', + unfreezeTime: '-', + relatedNo: 'xxxxxxxx', + time: '2026-06-10 05:00', + }, + { + key: 't-4', + type: '报名收入', + amount: 350, + balance: 850, + withdrawable: 750, + frozen: 100, + status: '正常', + frozenTime: '-', + unfreezeTime: '-', + relatedNo: 'xxxxxxxx', + time: '2026-06-09 17:30', + }, + { + key: 't-5', + type: '提现', + amount: -250, + balance: 600, + withdrawable: 500, + frozen: 100, + status: '已解冻', + frozenTime: '2026-06-08 10:00', + unfreezeTime: '2026-06-09 08:00', + relatedNo: 'xxxxxxxx', + time: '2026-06-08 10:00', + }, +]; + +// ============================================================ +// Model +// ============================================================ + +/** + * 用户钱包页数据模型 + */ +export function useWalletModel() { + // ===== 筛选条件 ===== + const filterForm = reactive({ + searchUserName: '', + searchPhone: '', + minBalance: '' as string | number, + maxBalance: '' as string | number, + }); + + // 可搜索字段防抖 + const { debouncedValue: debouncedUserName } = useDebounce( + toRef(filterForm, 'searchUserName') as Ref, + { delay: 300 }, + ); + const { debouncedValue: debouncedPhone } = useDebounce( + toRef(filterForm, 'searchPhone') as Ref, + { delay: 300 }, + ); + + // ===== 顶部统计 ===== + const [summary, setSummary] = useState(MOCK_SUMMARY); + + // ===== 表格状态 ===== + const [loading, setLoading] = useState(false); + const [dataSource, setDataSource] = useState(MOCK_DATA); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 10, + total: MOCK_DATA.length, + }); + + // ===== 详情弹窗状态 ===== + const [detailVisible, setDetailVisible] = useState(false); + const [currentWallet, setCurrentWallet] = useState({}); + const [detailLoading, setDetailLoading] = useState(false); + + // ===== 表格列配置 ===== + const columns = [ + { title: '用户ID', dataIndex: 'userId', key: 'userId', width: 160 }, + { title: '用户昵称', dataIndex: 'nickName', key: 'nickName', width: 120 }, + { title: '手机号', dataIndex: 'phone', key: 'phone', width: 140 }, + { + title: '余额', + dataIndex: 'balance', + key: 'balance', + width: 120, + customRender: ({ text }: { text: number }) => `${(text || 0).toFixed(2)}`, + }, + { + title: '冻结金额', + dataIndex: 'frozen', + key: 'frozen', + width: 120, + customRender: ({ text }: { text: number }) => `${(text || 0).toFixed(2)}`, + }, + { + title: '累计提现', + dataIndex: 'totalWithdraw', + key: 'totalWithdraw', + width: 120, + customRender: ({ text }: { text: number }) => `${(text || 0).toFixed(2)}`, + }, + ]; + + // ===== 计算属性 ===== + const hasFilter = computed(() => { + return ( + debouncedUserName.value.trim() !== '' || + debouncedPhone.value.trim() !== '' || + filterForm.minBalance !== '' || + filterForm.maxBalance !== '' + ); + }); + + // ===== 方法 ===== + + /** 查询(节流 500ms) */ + const handleSearch = useThrottleFn(async () => { + setLoading(true); + try { + console.log('搜索条件:', { + nickName: debouncedUserName.value, + phone: debouncedPhone.value, + minBalance: filterForm.minBalance, + maxBalance: filterForm.maxBalance, + }); + // TODO: 替换为真实 API 调用 + setDataSource(MOCK_DATA); + setPagination({ ...pagination.value, total: MOCK_DATA.length }); + setSummary(MOCK_SUMMARY); + message.success('查询成功'); + } catch (error: any) { + message.error(error.msg || '查询失败'); + } finally { + setLoading(false); + } + }, 500); + + /** 重置(节流 500ms) */ + const handleReset = useThrottleFn(() => { + filterForm.searchUserName = ''; + filterForm.searchPhone = ''; + filterForm.minBalance = ''; + filterForm.maxBalance = ''; + setPagination({ current: 1, pageSize: 10, total: MOCK_DATA.length }); + setDataSource(MOCK_DATA); + setSummary(MOCK_SUMMARY); + }, 500); + + const handlePageChange = (page: number, pageSize: number) => { + setPagination({ ...pagination.value, current: page, pageSize }); + handleSearch(); + }; + + /** 打开明细弹窗 */ + const handleViewDetail = (record: any) => { + setCurrentWallet(record); + setDetailVisible(true); + }; + + /** 关闭明细弹窗 */ + const handleCloseDetail = () => { + setDetailVisible(false); + }; + + /** + * 加载某用户的交易明细 + * 真实场景应调用 API;演示阶段所有用户共用同一份假数据 + */ + const loadTransactions = async (_record: any): Promise => { + setDetailLoading(true); + try { + // 模拟接口延迟 + await new Promise((resolve) => setTimeout(resolve, 200)); + return [...MOCK_TRANSACTIONS]; + } finally { + setDetailLoading(false); + } + }; + + /** 渲染交易状态 Tag */ + const renderTransactionStatus = (status: string) => { + const info = TRANSACTION_STATUS_MAP[status] || { label: status || '-', color: 'default' }; + return h(Tag, { color: info.color }, () => info.label); + }; + + return { + filterForm, + loading, + dataSource, + columns, + summary, + pagination, + hasFilter, + detailVisible, + currentWallet, + detailLoading, + handleSearch, + handleReset, + handlePageChange, + handleViewDetail, + handleCloseDetail, + loadTransactions, + renderTransactionStatus, + TRANSACTION_TYPE_OPTIONS: TRANSACTION_TYPE_OPTIONS as any, + TRANSACTION_STATUS_OPTIONS: TRANSACTION_STATUS_OPTIONS as any, + }; +} diff --git a/src/pages/finance/withdraw/components/WithdrawAuditModal.module.less b/src/pages/finance/withdraw/components/WithdrawAuditModal.module.less new file mode 100644 index 0000000..584a28e --- /dev/null +++ b/src/pages/finance/withdraw/components/WithdrawAuditModal.module.less @@ -0,0 +1,195 @@ +// ===== Modal 外层包裹:去掉 antd 默认内边距 ===== +.modalWrap { + :global(.ant-modal-header) { + display: none; + } +} + +// ===== 自定义标题栏 ===== +.modalHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 14px; + margin-bottom: 8px; + border-bottom: 1px solid #f0f0f0; +} + +.modalTitle { + font-size: 16px; + font-weight: 600; + color: rgba(0, 0, 0, 0.85); +} + +.closeBtn { + width: 28px; + height: 28px; + border: none; + background: transparent; + font-size: 22px; + line-height: 1; + color: rgba(0, 0, 0, 0.45); + cursor: pointer; + border-radius: 4px; + transition: all 0.2s; + + &:hover { + background: rgba(0, 0, 0, 0.04); + color: rgba(0, 0, 0, 0.85); + } +} + +// ===== 通用区块 ===== +.section { + padding: 14px 0; + border-bottom: 1px dashed #f0f0f0; + + &:last-child { + border-bottom: none; + padding-bottom: 0; + } +} + +.sectionTitle { + font-size: 14px; + font-weight: 600; + color: rgba(0, 0, 0, 0.85); + margin-bottom: 14px; + position: relative; + padding-left: 10px; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 14px; + background: #1677ff; + border-radius: 2px; + } +} + +// ===== 描述项栅格(3 列) ===== +.descGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + row-gap: 14px; + column-gap: 24px; +} + +.descItem { + display: flex; + align-items: baseline; + min-width: 0; +} + +.descLabel { + flex-shrink: 0; + color: rgba(0, 0, 0, 0.55); + font-size: 13px; + white-space: nowrap; +} + +.descValue { + flex: 1; + min-width: 0; + color: rgba(0, 0, 0, 0.85); + font-size: 13px; + word-break: break-all; + overflow: hidden; + text-overflow: ellipsis; +} + +.descValueBold { + font-weight: 600; + color: rgba(0, 0, 0, 0.85); +} + +// ===== 审核表单 ===== +.formRow { + display: flex; + align-items: flex-start; + margin-bottom: 18px; + + &:last-child { + margin-bottom: 0; + } +} + +.formLabel { + width: 88px; + flex-shrink: 0; + color: rgba(0, 0, 0, 0.55); + font-size: 13px; + padding-top: 6px; +} + +.formLabelTop { + padding-top: 6px; +} + +.footer { + display: flex; + justify-content: flex-end; + margin-top: 24px; +} + +// ===== 上传 ===== +.uploader { + flex: 1; + + :global { + .ant-upload-list-picture-card-container { + width: 96px; + height: 96px; + } + .ant-upload-select { + width: 96px; + height: 96px; + margin: 0; + } + } +} + +.uploadTrigger { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: rgba(0, 0, 0, 0.45); + font-size: 22px; + background: #fafafa; + border-radius: 4px; + transition: all 0.2s; + + &:hover { + color: #1677ff; + background: #f0f7ff; + } +} + +.uploadText { + font-size: 12px; + margin-top: 4px; + color: rgba(0, 0, 0, 0.45); +} + +// ===== 审核信息(只读,逐行展示) ===== +.infoColumn { + display: flex; + flex-direction: column; + row-gap: 14px; +} + +.auditImg { + border-radius: 4px; + object-fit: cover; + margin-right: 8px; +} + +.noImage { + color: rgba(0, 0, 0, 0.45); +} diff --git a/src/pages/finance/withdraw/components/WithdrawAuditModal.tsx b/src/pages/finance/withdraw/components/WithdrawAuditModal.tsx new file mode 100644 index 0000000..812f405 --- /dev/null +++ b/src/pages/finance/withdraw/components/WithdrawAuditModal.tsx @@ -0,0 +1,245 @@ +import { defineComponent, reactive, ref, watch } from 'vue'; +import { Modal, Radio, Input, Button, Upload, Image, message } from 'ant-design-vue'; +import { PlusOutlined } from '@ant-design/icons-vue'; +import { useWithdrawModel } from '../model/useWithdrawModel'; +import styles from './WithdrawAuditModal.module.less'; + +const { TextArea } = Input; + +interface WithdrawAuditModalProps { + visible: boolean; + record: any; + onClose: () => void; +} + +/** + * 提现申请详情 / 审核弹窗 + * + * 结构: + * - 基本信息 + * - 银行卡信息 + * - 支付信息 + * - 审核操作 + */ +export default defineComponent({ + name: 'WithdrawAuditModal', + props: { + visible: { type: Boolean, default: false }, + record: { type: Object, default: () => ({}) }, + onClose: { type: Function, required: true }, + }, + setup(props: WithdrawAuditModalProps) { + const { submitAudit } = useWithdrawModel(); + + // ===== 表单状态 ===== + const auditForm = reactive({ + pass: true, + remark: '', + }); + const imageList = ref([]); + const submitting = ref(false); + + /** 监听 visible:每次打开都重置表单 */ + watch( + () => props.visible, + (val) => { + if (val) { + auditForm.pass = true; + auditForm.remark = ''; + imageList.value = []; + submitting.value = false; + } + }, + ); + + /** 上传前校验(演示用:限制 5 张 & 5MB) */ + const beforeUpload = (file: any) => { + const isLt5M = file.size / 1024 / 1024 < 5; + if (!isLt5M) { + message.error('图片大小不能超过 5MB'); + return false; + } + imageList.value = [...imageList.value, file]; + return false; // 阻止自动上传,由"提交审核"按钮统一处理 + }; + + const handleRemove = (file: any) => { + imageList.value = imageList.value.filter((f) => f.uid !== file.uid); + }; + + const handleSubmit = async () => { + if (!auditForm.remark.trim()) { + message.warning('请输入审核内容'); + return; + } + submitting.value = true; + try { + // 真实场景应上传图片并调用审核 API + const urls = imageList.value.map((f) => f.name || ''); + submitAudit(auditForm.pass, auditForm.remark.trim(), urls); + } finally { + submitting.value = false; + } + }; + + /** + * 通用描述项渲染 + */ + const renderItem = (label: string, value: any, isBold = false) => ( +
+ {label} + {value || '-'} +
+ ); + + return () => { + const record = props.record || {}; + const bank = record.bank || {}; + const pay = record.pay || {}; + const isPending = record.auditStatus === '待审核'; + + return ( + + {/* 自定义标题栏 */} +
+ 提现申请详情 + +
+ + {/* ===== 基本信息 ===== */} +
+
基本信息
+
+ {renderItem('申请人昵称:', record.nickName)} + {renderItem('申请人手机号:', record.phone)} + {renderItem('真实姓名:', record.realName)} + {renderItem('申请时间:', record.applyTime)} + {renderItem('打款类型:', record.withdrawType)} +
+ {renderItem('提现金额:', `¥${(record.withdrawAmount || 0).toFixed(2)}`, true)} + {renderItem('费率:', `${((record.feeRate || 0) * 100).toFixed(2)}%`)} + {renderItem('手续费:', `¥${(record.feeAmount || 0).toFixed(2)}`)} + {renderItem('到账金额:', `¥${(record.transferAmount || 0).toFixed(2)}`, true)} + {renderItem('审核状态:', record.auditStatus, true)} + {renderItem('列账状态:', record.transferStatus || '-')} +
+
+ + {/* ===== 银行卡信息 ===== */} +
+
银行卡信息
+
+ {renderItem('银行卡类型:', bank.cardType)} + {renderItem('持卡人:', bank.holder)} + {renderItem('银行卡号:', bank.cardNo)} + {renderItem('开户行:', bank.bankName)} + {renderItem('开户支行:', bank.branchName)} +
+
+ + {/* ===== 支付信息 ===== */} +
+
支付信息
+
+ {renderItem('到账支付时间:', pay.transferTime || '-')} + {renderItem('商户订单号:', pay.merchantNo)} +
+
+ + {/* ===== 审核操作 / 审核信息 ===== */} +
+ {isPending ? ( + <> +
审核操作
+
+ 审核状态 + (auditForm.pass = val)} + > + 通过 + 未通过 + +
+ +
+ 审核内容 +