fix: 报表接口对接
This commit is contained in:
@@ -1,34 +1,29 @@
|
||||
import { defineComponent, ref, nextTick } from 'vue';
|
||||
import { Select, Table } from 'ant-design-vue';
|
||||
import { DatePicker, Table } from 'ant-design-vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { useReportsModel } from './model/useReportsModel';
|
||||
import { useEffect, useContainerSize } from '@/hooks';
|
||||
import styles from './index.module.less';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
/**
|
||||
* 账务报表
|
||||
*
|
||||
* 页面结构:
|
||||
* 1. 顶部标题 + 时间范围选择
|
||||
* 2. 三张统计卡(总收入 / 总提现 / 平台余额)
|
||||
* 3. 月度收支趋势(echarts 柱状 + 折线组合图)
|
||||
* 4. 月度收支明细(表格)
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'FinanceReports',
|
||||
setup() {
|
||||
const {
|
||||
range,
|
||||
handleRangeChange,
|
||||
dateRange,
|
||||
handleDateRangeChange,
|
||||
summary,
|
||||
chartData,
|
||||
detailColumns,
|
||||
detailData,
|
||||
formatMoney,
|
||||
RANGE_OPTIONS,
|
||||
} = useReportsModel();
|
||||
|
||||
// ===== 组合图(柱状 + 折线,echarts) =====
|
||||
// ===== echarts 组合图 =====
|
||||
const chartRef = ref<HTMLElement>();
|
||||
let chartInstance: any = null;
|
||||
|
||||
@@ -109,11 +104,7 @@ export default defineComponent({
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
lineStyle: { width: 3, color: '#52c41a' },
|
||||
itemStyle: {
|
||||
color: '#52c41a',
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff',
|
||||
},
|
||||
itemStyle: { color: '#52c41a', borderWidth: 2, borderColor: '#fff' },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
@@ -136,17 +127,13 @@ export default defineComponent({
|
||||
|
||||
const renderChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
if (!chartInstance) {
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
}
|
||||
if (!chartInstance) chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(getChartOption());
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
chartInstance?.resize();
|
||||
};
|
||||
const handleResize = () => chartInstance?.resize();
|
||||
|
||||
// ECharts 挂载/销毁 + 窗口 resize 监听
|
||||
// ECharts 挂载/销毁
|
||||
useEffect(() => {
|
||||
const el = chartRef.value;
|
||||
if (!el) return;
|
||||
@@ -170,24 +157,9 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
return () => {
|
||||
/** 根据增长率构建箭头 + 颜色 */
|
||||
const changeDesc = (rateStr: string | undefined, fallbackRate: number | null) => {
|
||||
if (rateStr === undefined && fallbackRate !== null) rateStr = String(fallbackRate);
|
||||
if (rateStr == null || rateStr === '' || isNaN(parseFloat(rateStr)))
|
||||
return { desc: '= 总收入 - 提现金额', descColor: 'rgba(0, 0, 0, 0.55)' };
|
||||
const rate = parseFloat(rateStr);
|
||||
const arrow = rate > 0 ? '↑' : rate < 0 ? '↓' : '';
|
||||
const sign = rate > 0 ? '+' : '';
|
||||
const absRate = Math.abs(rate).toFixed(2);
|
||||
return {
|
||||
desc: rate === 0 ? `→ 与上月持平` : `${arrow} 较上月 ${sign}${absRate}%`,
|
||||
descColor: rate > 0 ? '#52c41a' : rate < 0 ? '#ff4d4f' : 'rgba(0, 0, 0, 0.55)',
|
||||
};
|
||||
};
|
||||
|
||||
const s = summary.value;
|
||||
|
||||
/** 顶部 3 张统计卡配置 */
|
||||
/** 顶部 3 张统计卡 */
|
||||
const statCards = [
|
||||
{
|
||||
key: 'totalIncome',
|
||||
@@ -195,7 +167,6 @@ export default defineComponent({
|
||||
value: s.totalIncome,
|
||||
bgColor: '#f6ffed',
|
||||
accentColor: '#52c41a',
|
||||
...changeDesc(s.totalIncomeTendency, null),
|
||||
},
|
||||
{
|
||||
key: 'totalWithdraw',
|
||||
@@ -203,7 +174,6 @@ export default defineComponent({
|
||||
value: s.totalWithdraw,
|
||||
bgColor: '#fffbe6',
|
||||
accentColor: '#faad14',
|
||||
...changeDesc(s.totalWithdrawTendency, null),
|
||||
},
|
||||
{
|
||||
key: 'totalBalance',
|
||||
@@ -211,13 +181,12 @@ export default defineComponent({
|
||||
value: s.totalBalance,
|
||||
bgColor: '#e6f4ff',
|
||||
accentColor: '#1677ff',
|
||||
...changeDesc(undefined, null),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div class={styles.reportsMain}>
|
||||
{/* ===== 顶部标题 + 时间范围 ===== */}
|
||||
{/* ===== 顶部标题 + 时间段选择 ===== */}
|
||||
<div class={styles.reportHeader}>
|
||||
<div class={styles.headerLeft}>
|
||||
<div class={styles.reportTitle}>财务报表</div>
|
||||
@@ -225,11 +194,12 @@ export default defineComponent({
|
||||
平台资金流水概览,包括收入、退款、提现、用户余额等核心财务指标
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
value={range.value}
|
||||
onUpdate:value={handleRangeChange}
|
||||
options={[...RANGE_OPTIONS]}
|
||||
style={{ width: '120px' }}
|
||||
<RangePicker
|
||||
picker="month"
|
||||
format="YYYY-MM"
|
||||
value={dateRange.value}
|
||||
onUpdate:value={handleDateRangeChange}
|
||||
style={{ width: '240px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -241,18 +211,18 @@ export default defineComponent({
|
||||
<div class={styles.statValue} style={{ color: item.accentColor }}>
|
||||
¥{formatMoney(item.value)}
|
||||
</div>
|
||||
<div class={styles.statDesc} style={{ color: item.descColor }}>
|
||||
{item.desc}
|
||||
<div class={styles.statDesc} style="visibility: hidden;">
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ===== 组合图卡(echarts) ===== */}
|
||||
{/* ===== 组合图卡 ===== */}
|
||||
<div class={styles.chartCard}>
|
||||
<div class={styles.cardTitle}>
|
||||
<span class={styles.cardTitleIcon}>📊</span>
|
||||
月度收支趋势(近 {chartData.value.length} 个月)
|
||||
月度收支趋势({chartData.value.length} 个月)
|
||||
</div>
|
||||
<div
|
||||
ref={(el: any) => {
|
||||
@@ -273,10 +243,7 @@ export default defineComponent({
|
||||
size="middle"
|
||||
bordered={false}
|
||||
rowKey="key"
|
||||
scroll={{
|
||||
y: detailTableHeight.value,
|
||||
x: 'max-content',
|
||||
}}
|
||||
scroll={{ y: detailTableHeight.value, x: 'max-content' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,7 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import type { SelectValue } from 'ant-design-vue/es/select';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { useEffect } from '@/hooks/useEffect';
|
||||
import { getFinanceReport, type FinanceReportVO } from './services';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export type RangeValue = (typeof RANGE_OPTIONS)[number]['value'];
|
||||
|
||||
// ============================================================
|
||||
// 常量
|
||||
// ============================================================
|
||||
|
||||
/** 时间范围选项 */
|
||||
export const RANGE_OPTIONS = [
|
||||
{ value: 'month' as const, label: '本月' },
|
||||
{ value: '3m' as const, label: '近 3 个月' },
|
||||
{ value: '6m' as const, label: '近 6 个月' },
|
||||
{ value: 'year' as const, label: '本年' },
|
||||
];
|
||||
|
||||
/** 范围对应的月数 */
|
||||
const RANGE_MONTHS: Record<RangeValue, number> = {
|
||||
month: 1,
|
||||
'3m': 3,
|
||||
'6m': 6,
|
||||
year: 12,
|
||||
};
|
||||
|
||||
function isRangeValue(val: SelectValue): val is RangeValue {
|
||||
return typeof val === 'string' && RANGE_OPTIONS.some((item) => item.value === val);
|
||||
}
|
||||
import dayjs, { type Dayjs } from 'dayjs';
|
||||
|
||||
// ============================================================
|
||||
// 工具
|
||||
@@ -59,34 +31,26 @@ function toNumber(value: string): number {
|
||||
* 账务报表数据模型
|
||||
*/
|
||||
export function useReportsModel() {
|
||||
// ===== 时间范围 =====
|
||||
const range = ref<RangeValue>('month');
|
||||
// ===== 时间范围(年月区间选择器) =====
|
||||
const dateRange = ref<[Dayjs, Dayjs]>([dayjs().startOf('month'), dayjs().startOf('month')]);
|
||||
|
||||
// ===== 构建 API 参数(根据 range 推算查询起始月份) =====
|
||||
// ===== 构建 API 参数 =====
|
||||
const buildQueryParams = () => {
|
||||
const months = RANGE_MONTHS[range.value];
|
||||
const date = dayjs()
|
||||
.subtract(months - 1, 'month')
|
||||
.format('YYYY-MM');
|
||||
return { date };
|
||||
const [start, end] = dateRange.value;
|
||||
return {
|
||||
beginDate: start.format('YYYY-MM'),
|
||||
endDate: end.format('YYYY-MM'),
|
||||
};
|
||||
};
|
||||
|
||||
// ===== 请求(range 变化时自动重新请求) =====
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
run: fetchReport,
|
||||
} = useRequest<FinanceReportVO>(() => getFinanceReport(buildQueryParams()), {
|
||||
refreshDeps: [range],
|
||||
// ===== 请求(dateRange 变化时自动重新请求) =====
|
||||
const { data, loading } = useRequest<FinanceReportVO>(
|
||||
() => getFinanceReport(buildQueryParams()),
|
||||
{
|
||||
refreshDeps: [dateRange],
|
||||
formatResult: (res) => (res.code == 200 ? res.data : null),
|
||||
});
|
||||
|
||||
// ===== 根据范围过滤后的月列表 =====
|
||||
const filteredMonthList = computed(() => {
|
||||
const list = data.value?.monthList || [];
|
||||
const count = RANGE_MONTHS[range.value];
|
||||
return list.slice(-count);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// ===== 汇总 =====
|
||||
const summary = computed(() => ({
|
||||
@@ -98,15 +62,16 @@ export function useReportsModel() {
|
||||
}));
|
||||
|
||||
// ===== 图表数据 =====
|
||||
const chartData = computed(() =>
|
||||
filteredMonthList.value.map((item) => ({
|
||||
const chartData = computed(() => {
|
||||
const list = data.value?.monthList || [];
|
||||
return list.map((item) => ({
|
||||
month: dayjs(item.month).format('MM月'),
|
||||
order: toNumber(item.orderAmount),
|
||||
refund: toNumber(item.refundAmount),
|
||||
netIncome: toNumber(item.netIncome),
|
||||
withdraw: toNumber(item.withdrawAmount),
|
||||
})),
|
||||
);
|
||||
}));
|
||||
});
|
||||
|
||||
// ===== 明细表列 =====
|
||||
const detailColumns = [
|
||||
@@ -153,25 +118,24 @@ export function useReportsModel() {
|
||||
];
|
||||
|
||||
// ===== 明细表数据 =====
|
||||
const detailData = computed(() =>
|
||||
filteredMonthList.value.map((item) => ({
|
||||
const detailData = computed(() => {
|
||||
const list = data.value?.monthList || [];
|
||||
return list.map((item) => ({
|
||||
key: item.month,
|
||||
month: item.month,
|
||||
order: toNumber(item.orderAmount),
|
||||
refund: toNumber(item.refundAmount),
|
||||
netIncome: toNumber(item.netIncome),
|
||||
withdraw: toNumber(item.withdrawAmount),
|
||||
})),
|
||||
);
|
||||
}));
|
||||
});
|
||||
|
||||
// ===== 切换时间范围 =====
|
||||
const handleRangeChange = (val: SelectValue) => {
|
||||
if (!isRangeValue(val)) return;
|
||||
range.value = val;
|
||||
// ===== 时间范围变更 =====
|
||||
const handleDateRangeChange = (val: any) => {
|
||||
if (!val || val.length < 2) return;
|
||||
dateRange.value = [dayjs(val[0]), dayjs(val[1])];
|
||||
};
|
||||
|
||||
// ===== 范围变化时刷新图表(通过 page 中的 useEffect 监听 range) =====
|
||||
|
||||
/** 千分位格式金额(页面渲染用) */
|
||||
function formatMoney(val: string | number): string {
|
||||
if (typeof val === 'string') return formatMoneyStr(val);
|
||||
@@ -182,14 +146,13 @@ export function useReportsModel() {
|
||||
}
|
||||
|
||||
return {
|
||||
range,
|
||||
dateRange,
|
||||
loading,
|
||||
handleRangeChange,
|
||||
handleDateRangeChange,
|
||||
summary,
|
||||
chartData,
|
||||
detailColumns,
|
||||
detailData,
|
||||
formatMoney,
|
||||
RANGE_OPTIONS: RANGE_OPTIONS as any,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineComponent, reactive, ref, watch } from 'vue';
|
||||
import { defineComponent, reactive, ref } from 'vue';
|
||||
import { useEffect } from '@/hooks';
|
||||
import { Modal, Table, Button, Select, Form, Space, DatePicker, Pagination } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
@@ -92,11 +93,9 @@ export default defineComponent({
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
// ===== 弹窗打开 → 首次加载 =====
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
// ===== 弹窗打开 → 重置并加载 =====
|
||||
useEffect(() => {
|
||||
if (props.visible) {
|
||||
filterForm.timeRange = null;
|
||||
filterForm.type = '';
|
||||
filterForm.status = '';
|
||||
@@ -105,8 +104,7 @@ export default defineComponent({
|
||||
dataSource.value = [];
|
||||
handleSearch();
|
||||
}
|
||||
},
|
||||
);
|
||||
}, [() => props.visible]);
|
||||
|
||||
// ===== 表格列(dataIndex 对齐 API) =====
|
||||
const columns = [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineComponent, ref, reactive, watch } from 'vue';
|
||||
import { defineComponent, ref, reactive } from 'vue';
|
||||
import { useEffect } from '@/hooks';
|
||||
import { Modal, Form, Input, Tree, Button, Spin, message } from 'ant-design-vue';
|
||||
import type { TreeProps } from 'ant-design-vue';
|
||||
import { getPermissionTree, type PermissionTreeNode } from '../model/services';
|
||||
@@ -77,19 +78,13 @@ export default defineComponent({
|
||||
await loadPermissionTree(record.roleId);
|
||||
};
|
||||
|
||||
// 监听 visible 变化,弹窗打开时重新初始化
|
||||
// immediate: true 是因为父组件用 v-if 控制显隐,
|
||||
// 组件挂载时 visible 就已经是 true,不会触发 watch 变更回调
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
await initFormFromRecord(props.record);
|
||||
// 弹窗打开时重新初始化(useEffect 内置 immediate 行为)
|
||||
useEffect(() => {
|
||||
if (props.visible) {
|
||||
initFormFromRecord(props.record);
|
||||
setTimeout(() => formRef.value?.clearValidate(), 0);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
}, [() => props.visible]);
|
||||
|
||||
/** Tree 勾选回调(受控) */
|
||||
const handleCheck: TreeProps['onCheck'] = (checkedKeys, info) => {
|
||||
|
||||
Reference in New Issue
Block a user