feat: 财务管理页面编写

This commit is contained in:
cst
2026-07-27 14:56:44 +08:00
parent 0e0e1e300a
commit 9c2dc49d8d
3 changed files with 352 additions and 1 deletions
@@ -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%;
}
}
}
+105 -1
View File
@@ -1,4 +1,10 @@
import { defineComponent } from 'vue'; 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({ export default defineComponent({
name: 'FinancePayments', name: 'FinancePayments',
setup() { setup() {
const {
filterForm,
loading,
dataSource,
columns,
summary,
pagination,
handleSearch,
handleReset,
handlePageChange,
PAYMENT_TYPE_OPTIONS,
} = usePaymentsModel();
const { containerRef, height } = useContainerSize({ headerOffset: 55 });
return () => ( return () => (
<div class="page-container"> <div class="page-container">
<div style={{ padding: '24px', fontSize: '16px', color: '#999' }}> - </div> {/* ===== 筛选区 ===== */}
<div class="page-filter">
<Form style={{ rowGap: '10px' }} layout="inline" model={filterForm}>
<Form.Item label="时间" name="timeRange">
<RangePicker
value={filterForm.timeRange as any}
style={{ width: '240px' }}
allowClear
onUpdate:value={(val: any) => (filterForm.timeRange = val)}
/>
</Form.Item>
<Form.Item label="用户昵称" name="searchUserName">
<Input
placeholder="请输入"
style={{ width: '180px' }}
allowClear
onPressEnter={handleSearch}
/>
</Form.Item>
<Form.Item label="手机号" name="searchPhone">
<Input
placeholder="请输入"
style={{ width: '180px' }}
allowClear
onPressEnter={handleSearch}
/>
</Form.Item>
<Form.Item label="类型" name="type">
<Select
value={filterForm.type}
options={PAYMENT_TYPE_OPTIONS as any}
style={{ width: '140px' }}
allowClear
onUpdate:value={(val: any) => (filterForm.type = val || '')}
/>
</Form.Item>
<Form.Item>
<Space>
<Button onClick={handleReset}></Button>
<Button type="primary" onClick={handleSearch} loading={loading.value}>
</Button>
</Space>
</Form.Item>
</Form>
</div>
{/* ===== 表格区 ===== */}
<div class="page-table">
{/* ===== 汇总区(按图片样式:左对齐的两段加粗数字) ===== */}
<div class={styles.summary}>
<span class={styles.summaryItem}>
<span class={styles.summaryLabel}></span>
<span class={styles.summaryValue}>{summary.value.totalPay.toFixed(2)}</span>
</span>
<span class={styles.summaryItem}>
<span class={styles.summaryLabel}>退</span>
<span class={styles.summaryValue}>{summary.value.totalRefund.toFixed(2)}</span>
</span>
</div>
<div ref={containerRef} class={styles.tableBody}>
<Table
columns={columns}
dataSource={dataSource.value}
loading={loading.value}
scroll={{ x: 'max-content', y: height.value }}
pagination={false}
/>
</div>
{/* 独立分页,右下方 */}
<div class="page-pagination">
<Pagination
current={pagination.value.current}
pageSize={pagination.value.pageSize}
total={pagination.value.total}
showSizeChanger
showTotal={(total: number) => `${total}`}
onChange={handlePageChange}
onShowSizeChange={handlePageChange}
/>
</div>
</div>
</div> </div>
); );
}, },
@@ -0,0 +1,201 @@
import { reactive, toRef, Ref } from 'vue';
import { message } from 'ant-design-vue';
import { useState, useDebounce, useThrottleFn } from '@/hooks';
// ============================================================
// 常量
// ============================================================
/** 类型选项(筛选) */
export const PAYMENT_TYPE_OPTIONS = [
{ value: '', label: '全部' },
{ value: '报名', label: '报名' },
{ value: '取消报名', label: '取消报名' },
{ value: '提现', label: '提现' },
] as const;
// ============================================================
// 汇总假数据
// ============================================================
const MOCK_SUMMARY = {
totalPay: 113255.0,
totalRefund: 3255.0,
};
// ============================================================
// 支付流水假数据(5 条)
// ============================================================
const MOCK_DATA = [
{
key: '1',
flowNo: 'xxxxxx',
orderNo: 'xxxxxxxx',
type: '报名',
nickName: '张三',
phone: '12345678997',
amount: 99.0,
thirdFlowNo: 'xxxxxxxxxxxxxxxxxxxx',
tradeTime: '2026-05-26 08:00:00',
},
{
key: '2',
flowNo: '',
orderNo: '',
type: '取消报名',
nickName: '李四',
phone: '12345678998',
amount: 100.0,
thirdFlowNo: '',
tradeTime: '2026-05-26 07:00:00',
},
{
key: '3',
flowNo: '',
orderNo: '',
type: '提现',
nickName: '王五',
phone: '12345678998',
amount: 1000.0,
thirdFlowNo: '',
tradeTime: '2026-05-26 06:00:00',
},
{
key: '4',
flowNo: '',
orderNo: '',
type: '报名',
nickName: '赵六',
phone: '13800138000',
amount: 199.0,
thirdFlowNo: '',
tradeTime: '2026-05-25 19:30:00',
},
{
key: '5',
flowNo: '',
orderNo: '',
type: '取消报名',
nickName: '钱七',
phone: '13900139001',
amount: 50.0,
thirdFlowNo: '',
tradeTime: '2026-05-25 15:20:00',
},
];
// ============================================================
// Model
// ============================================================
/**
* 支付流水页数据模型
*/
export function usePaymentsModel() {
// ===== 筛选条件 =====
const filterForm = reactive({
timeRange: null as [string, string] | null,
searchUserName: '',
searchPhone: '',
type: '',
});
// 可搜索字段防抖
const { debouncedValue: debouncedUserName } = useDebounce(
toRef(filterForm, 'searchUserName') as Ref<string>,
{ delay: 300 },
);
const { debouncedValue: debouncedPhone } = useDebounce(
toRef(filterForm, 'searchPhone') as Ref<string>,
{ delay: 300 },
);
// ===== 汇总 =====
const [summary, setSummary] = useState(MOCK_SUMMARY);
// ===== 表格状态 =====
const [loading, setLoading] = useState<boolean>(false);
const [dataSource, setDataSource] = useState<any[]>(MOCK_DATA);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: MOCK_DATA.length,
});
// ===== 表格列配置 =====
const columns = [
{ title: '流水号', dataIndex: 'flowNo', key: 'flowNo', width: 140 },
{ title: '订单编号', dataIndex: 'orderNo', key: 'orderNo', width: 140 },
{ title: '类型', dataIndex: 'type', key: 'type', width: 110 },
{ title: '用户昵称', dataIndex: 'nickName', key: 'nickName', width: 120 },
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 140 },
{
title: '金额',
dataIndex: 'amount',
key: 'amount',
width: 120,
customRender: ({ text }: { text: number }) => `${(text || 0).toFixed(2)}`,
},
{ title: '第三方流水单号', dataIndex: 'thirdFlowNo', key: 'thirdFlowNo', width: 220 },
{
title: '交易时间',
dataIndex: 'tradeTime',
key: 'tradeTime',
width: 170,
},
];
// ===== 方法 =====
/** 查询(节流 500ms */
const handleSearch = useThrottleFn(async () => {
setLoading(true);
try {
console.log('搜索条件:', {
timeRange: filterForm.timeRange,
nickName: debouncedUserName.value,
phone: debouncedPhone.value,
type: filterForm.type,
});
// 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.timeRange = null;
filterForm.searchUserName = '';
filterForm.searchPhone = '';
filterForm.type = '';
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();
};
return {
filterForm,
loading,
dataSource,
columns,
summary,
pagination,
handleSearch,
handleReset,
handlePageChange,
PAYMENT_TYPE_OPTIONS: PAYMENT_TYPE_OPTIONS as any,
};
}