fix: 更新提现审核相关功能 更新账务报表页面功能
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 财务报表 API
|
||||||
|
* 对应 OpenAPI: GET /admin/finance/report/list
|
||||||
|
*/
|
||||||
|
import { get } from '@/utils/request';
|
||||||
|
import type { ReportQueryParams, FinanceReportVO, ApiResult } from './types';
|
||||||
|
|
||||||
|
export type { ReportQueryParams, FinanceReportVO, ReportMonthItem, ApiResult } from './types';
|
||||||
|
|
||||||
|
/** GET /admin/finance/report/list — 财务报表 */
|
||||||
|
export function getFinanceReport(params: ReportQueryParams): Promise<ApiResult<FinanceReportVO>> {
|
||||||
|
return get('/admin/finance/report/list', params as Record<string, any>);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 财务报表 — 类型定义
|
||||||
|
* 对应 OpenAPI: GET /admin/finance/report/list
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 月度明细项(对应 InnerMonthVO) */
|
||||||
|
export interface ReportMonthItem {
|
||||||
|
/** 月份 yyyy-MM */
|
||||||
|
month: string;
|
||||||
|
/** 订单金额,0.00 格式 */
|
||||||
|
orderAmount: string;
|
||||||
|
/** 退款金额,0.00 格式 */
|
||||||
|
refundAmount: string;
|
||||||
|
/** 净收入 = orderAmount - refundAmount,0.00 格式 */
|
||||||
|
netIncome: string;
|
||||||
|
/** 提现金额,0.00 格式 */
|
||||||
|
withdrawAmount: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 财务报表汇总(对应 TournamentFinanceReportVO) */
|
||||||
|
export interface FinanceReportVO {
|
||||||
|
/** 总收入(扣除退款后),0.00 格式 */
|
||||||
|
totalIncome: string;
|
||||||
|
/** 总收入较上月增长率 */
|
||||||
|
totalIncomeTendency: string;
|
||||||
|
/** 总提现金额,0.00 格式 */
|
||||||
|
totalWithdrawIncome: string;
|
||||||
|
/** 总提现金额较上月增长率 */
|
||||||
|
totalWithdrawIncomeTendency: string;
|
||||||
|
/** 平台余额 = totalIncome - totalWithdrawIncome,0.00 格式 */
|
||||||
|
totalBalanceAmount: string;
|
||||||
|
/** 月度收支明细 */
|
||||||
|
monthList: ReportMonthItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 财务报表查询参数 */
|
||||||
|
export interface ReportQueryParams {
|
||||||
|
/** 查询日期 yyyy-MM */
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResult<T> {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ export type { WithdrawVO, WithdrawInfoVO, WithdrawQueryParams, PageData, ApiResu
|
|||||||
|
|
||||||
const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
/** GET /finance/withdraw/page — 提现列表分页 */
|
/** GET /admin/finance/withdraw/page — 提现列表分页 */
|
||||||
export async function getWithdrawList(
|
export async function getWithdrawList(
|
||||||
params: WithdrawQueryParams,
|
params: WithdrawQueryParams,
|
||||||
): Promise<ApiResult<PageData<WithdrawVO>>> {
|
): Promise<ApiResult<PageData<WithdrawVO>>> {
|
||||||
@@ -26,16 +26,16 @@ export async function getWithdrawList(
|
|||||||
data: buildMockWithdrawPage(page, limit, params),
|
data: buildMockWithdrawPage(page, limit, params),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return get('/finance/withdraw/page', params as Record<string, any>);
|
return get('/admin/finance/withdraw/page', params as Record<string, any>);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET /finance/withdraw/info?id=xxx — 提现详情 */
|
/** GET /admin/finance/withdraw/info?id=xxx — 提现详情 */
|
||||||
export async function getWithdrawInfo(id: number): Promise<ApiResult<WithdrawInfoVO>> {
|
export async function getWithdrawInfo(id: number): Promise<ApiResult<WithdrawInfoVO>> {
|
||||||
if (USE_MOCK) {
|
if (USE_MOCK) {
|
||||||
await delay(MOCK_DELAY);
|
await delay(MOCK_DELAY);
|
||||||
return { code: 200, msg: 'success', data: buildMockWithdrawInfo(id) };
|
return { code: 200, msg: 'success', data: buildMockWithdrawInfo(id) };
|
||||||
}
|
}
|
||||||
return get('/finance/withdraw/info', { id: String(id) });
|
return get('/admin/finance/withdraw/info', { id: String(id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 提现审核请求参数 */
|
/** 提现审核请求参数 */
|
||||||
@@ -50,7 +50,7 @@ export interface WithdrawAuditParams {
|
|||||||
checkImg: string;
|
checkImg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** POST /finance/withdraw/audit — 审核提现 */
|
/** POST /admin/finance/withdraw/audit — 审核提现 */
|
||||||
export async function postWithdrawAudit(params: WithdrawAuditParams): Promise<ApiResult<null>> {
|
export async function postWithdrawAudit(params: WithdrawAuditParams): Promise<ApiResult<null>> {
|
||||||
return post('/finance/withdraw/audit', params);
|
return post('/admin/finance/withdraw/audit', params);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,7 +377,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const file = base64ToFile(dataUrl, `banner_${Date.now()}.jpg`);
|
const file = base64ToFile(dataUrl, `banner_${Date.now()}.jpg`);
|
||||||
const result = await uploadFile(file, OssUploadType.Banner as any);
|
const result = await uploadFile(file, OssUploadType.EventCover);
|
||||||
|
|
||||||
URL.revokeObjectURL(localUrl);
|
URL.revokeObjectURL(localUrl);
|
||||||
formData.imageUrl = result.url;
|
formData.imageUrl = result.url;
|
||||||
|
|||||||
@@ -123,4 +123,10 @@
|
|||||||
color: rgba(0, 0, 0, 0.85);
|
color: rgba(0, 0, 0, 0.85);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detailTableWrap {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { defineComponent, ref, nextTick } from 'vue';
|
|||||||
import { Select, Table } from 'ant-design-vue';
|
import { Select, Table } from 'ant-design-vue';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
import { useReportsModel } from './model/useReportsModel';
|
import { useReportsModel } from './model/useReportsModel';
|
||||||
import { useEffect } from '@/hooks';
|
import { useEffect, useContainerSize } from '@/hooks';
|
||||||
import styles from './index.module.less';
|
import styles from './index.module.less';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,7 +11,7 @@ import styles from './index.module.less';
|
|||||||
* 页面结构:
|
* 页面结构:
|
||||||
* 1. 顶部标题 + 时间范围选择
|
* 1. 顶部标题 + 时间范围选择
|
||||||
* 2. 三张统计卡(总收入 / 总提现 / 平台余额)
|
* 2. 三张统计卡(总收入 / 总提现 / 平台余额)
|
||||||
* 3. 月度收支趋势(近 6 个月,echarts 柱状 + 折线组合图)
|
* 3. 月度收支趋势(echarts 柱状 + 折线组合图)
|
||||||
* 4. 月度收支明细(表格)
|
* 4. 月度收支明细(表格)
|
||||||
*/
|
*/
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -33,22 +33,23 @@ export default defineComponent({
|
|||||||
let chartInstance: any = null;
|
let chartInstance: any = null;
|
||||||
|
|
||||||
const getChartOption = () => {
|
const getChartOption = () => {
|
||||||
const months = chartData.value.map((d) => d.month);
|
const data = chartData.value;
|
||||||
const order = chartData.value.map((d) => d.order);
|
const months = data.map((d) => d.month);
|
||||||
const netIncome = chartData.value.map((d) => d.netIncome);
|
const orderAmount = data.map((d) => d.order);
|
||||||
|
const refundAmount = data.map((d) => d.refund);
|
||||||
|
const withdrawAmount = data.map((d) => d.withdraw);
|
||||||
|
const netIncome = data.map((d) => d.netIncome);
|
||||||
return {
|
return {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: {
|
axisPointer: { type: 'shadow' },
|
||||||
type: 'cross',
|
|
||||||
crossStyle: { color: 'rgba(0, 0, 0, 0.15)' },
|
|
||||||
label: { backgroundColor: '#6a7985' },
|
|
||||||
},
|
|
||||||
valueFormatter: (v: number) => `¥${formatMoney(v)}`,
|
valueFormatter: (v: number) => `¥${formatMoney(v)}`,
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
data: [
|
data: [
|
||||||
{ name: '订单金额', icon: 'roundRect' },
|
{ name: '订单金额', icon: 'roundRect' },
|
||||||
|
{ name: '退款金额', icon: 'roundRect' },
|
||||||
|
{ name: '提现金额', icon: 'roundRect' },
|
||||||
{ name: '净收入', icon: 'circle' },
|
{ name: '净收入', icon: 'circle' },
|
||||||
],
|
],
|
||||||
bottom: 8,
|
bottom: 8,
|
||||||
@@ -63,7 +64,6 @@ export default defineComponent({
|
|||||||
axisTick: { show: false },
|
axisTick: { show: false },
|
||||||
axisLine: { lineStyle: { color: '#e8e8e8' } },
|
axisLine: { lineStyle: { color: '#e8e8e8' } },
|
||||||
axisLabel: { color: 'rgba(0, 0, 0, 0.55)' },
|
axisLabel: { color: 'rgba(0, 0, 0, 0.55)' },
|
||||||
axisPointer: { type: 'shadow' },
|
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
@@ -77,23 +77,28 @@ export default defineComponent({
|
|||||||
{
|
{
|
||||||
name: '订单金额',
|
name: '订单金额',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: order,
|
stack: 'amount',
|
||||||
barMaxWidth: 48,
|
data: orderAmount,
|
||||||
barCategoryGap: '35%',
|
barMaxWidth: 40,
|
||||||
itemStyle: {
|
itemStyle: { color: '#4096ff' },
|
||||||
color: {
|
emphasis: { focus: 'series' },
|
||||||
type: 'linear',
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
x2: 0,
|
|
||||||
y2: 1,
|
|
||||||
colorStops: [
|
|
||||||
{ offset: 0, color: '#4096ff' },
|
|
||||||
{ offset: 1, color: '#1677ff' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
borderRadius: [4, 4, 0, 0],
|
{
|
||||||
|
name: '退款金额',
|
||||||
|
type: 'bar',
|
||||||
|
stack: 'amount',
|
||||||
|
data: refundAmount,
|
||||||
|
barMaxWidth: 40,
|
||||||
|
itemStyle: { color: '#ff7875' },
|
||||||
|
emphasis: { focus: 'series' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '提现金额',
|
||||||
|
type: 'bar',
|
||||||
|
stack: 'amount',
|
||||||
|
data: withdrawAmount,
|
||||||
|
barMaxWidth: 40,
|
||||||
|
itemStyle: { color: '#ffa940' },
|
||||||
emphasis: { focus: 'series' },
|
emphasis: { focus: 'series' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -154,48 +159,59 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
}, [() => chartRef.value]);
|
}, [() => chartRef.value]);
|
||||||
|
|
||||||
// 时间范围切换时刷新图表
|
// 数据更新时刷新图表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
renderChart();
|
renderChart();
|
||||||
}, [range]);
|
}, [chartData]);
|
||||||
|
|
||||||
|
// ===== 明细表容器高度 =====
|
||||||
|
const { containerRef: detailContainerRef, height: detailTableHeight } = useContainerSize({
|
||||||
|
headerOffset: 56,
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
/** 根据增长率构建箭头 + 颜色(正值 ↑ 绿,负值 ↓ 红,null 时固定文本) */
|
/** 根据增长率构建箭头 + 颜色 */
|
||||||
const changeDesc = (rate: number | null) => {
|
const changeDesc = (rateStr: string | undefined, fallbackRate: number | null) => {
|
||||||
if (rate == null) return { desc: '= 总收入 - 提现金额', descColor: 'rgba(0, 0, 0, 0.55)' };
|
if (rateStr === undefined && fallbackRate !== null) rateStr = String(fallbackRate);
|
||||||
const arrow = rate > 0 ? '↑' : '↓';
|
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 sign = rate > 0 ? '+' : '';
|
||||||
|
const absRate = Math.abs(rate).toFixed(2);
|
||||||
return {
|
return {
|
||||||
desc: `${arrow} 较上月 ${sign}${rate}%`,
|
desc: rate === 0 ? `→ 与上月持平` : `${arrow} 较上月 ${sign}${absRate}%`,
|
||||||
descColor: rate > 0 ? '#52c41a' : '#ff4d4f',
|
descColor: rate > 0 ? '#52c41a' : rate < 0 ? '#ff4d4f' : 'rgba(0, 0, 0, 0.55)',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const s = summary.value;
|
||||||
|
|
||||||
/** 顶部 3 张统计卡配置 */
|
/** 顶部 3 张统计卡配置 */
|
||||||
const statCards = [
|
const statCards = [
|
||||||
{
|
{
|
||||||
key: 'totalIncome',
|
key: 'totalIncome',
|
||||||
label: '总收入(扣除退款后)',
|
label: '总收入(扣除退款后)',
|
||||||
value: summary.value.totalIncome,
|
value: s.totalIncome,
|
||||||
bgColor: '#f6ffed',
|
bgColor: '#f6ffed',
|
||||||
accentColor: '#52c41a',
|
accentColor: '#52c41a',
|
||||||
...changeDesc(12.5),
|
...changeDesc(s.totalIncomeTendency, null),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'totalWithdraw',
|
key: 'totalWithdraw',
|
||||||
label: '总提现金额',
|
label: '总提现金额',
|
||||||
value: summary.value.totalWithdraw,
|
value: s.totalWithdraw,
|
||||||
bgColor: '#fffbe6',
|
bgColor: '#fffbe6',
|
||||||
accentColor: '#faad14',
|
accentColor: '#faad14',
|
||||||
...changeDesc(32),
|
...changeDesc(s.totalWithdrawTendency, null),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'totalBalance',
|
key: 'totalBalance',
|
||||||
label: '平台余额(所有用户余额总和)',
|
label: '平台余额(所有用户余额总和)',
|
||||||
value: summary.value.totalBalance,
|
value: s.totalBalance,
|
||||||
bgColor: '#e6f4ff',
|
bgColor: '#e6f4ff',
|
||||||
accentColor: '#1677ff',
|
accentColor: '#1677ff',
|
||||||
...changeDesc(null),
|
...changeDesc(undefined, null),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -236,7 +252,7 @@ export default defineComponent({
|
|||||||
<div class={styles.chartCard}>
|
<div class={styles.chartCard}>
|
||||||
<div class={styles.cardTitle}>
|
<div class={styles.cardTitle}>
|
||||||
<span class={styles.cardTitleIcon}>📊</span>
|
<span class={styles.cardTitleIcon}>📊</span>
|
||||||
月度收支趋势(近 6 个月)
|
月度收支趋势(近 {chartData.value.length} 个月)
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={(el: any) => {
|
ref={(el: any) => {
|
||||||
@@ -249,6 +265,7 @@ export default defineComponent({
|
|||||||
{/* ===== 月度收支明细 ===== */}
|
{/* ===== 月度收支明细 ===== */}
|
||||||
<div class={styles.detailCard}>
|
<div class={styles.detailCard}>
|
||||||
<div class={styles.cardTitle}>月度收支明细</div>
|
<div class={styles.cardTitle}>月度收支明细</div>
|
||||||
|
<div ref={detailContainerRef} class={styles.detailTableWrap}>
|
||||||
<Table
|
<Table
|
||||||
columns={detailColumns}
|
columns={detailColumns}
|
||||||
dataSource={detailData.value}
|
dataSource={detailData.value}
|
||||||
@@ -256,9 +273,14 @@ export default defineComponent({
|
|||||||
size="middle"
|
size="middle"
|
||||||
bordered={false}
|
bordered={false}
|
||||||
rowKey="key"
|
rowKey="key"
|
||||||
|
scroll={{
|
||||||
|
y: detailTableHeight.value,
|
||||||
|
x: 'max-content',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 财务报表 - 接口服务
|
||||||
|
*/
|
||||||
|
import { get } from '@/utils/request';
|
||||||
|
import type {
|
||||||
|
ReportQueryParams,
|
||||||
|
FinanceReportVO,
|
||||||
|
ReportMonthItem,
|
||||||
|
ApiResult,
|
||||||
|
} from '@/api/reports/types';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ReportQueryParams,
|
||||||
|
FinanceReportVO,
|
||||||
|
ReportMonthItem,
|
||||||
|
ApiResult,
|
||||||
|
} from '@/api/reports/types';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// URL 常量
|
||||||
|
// ============================================================
|
||||||
|
const reportList = '/admin/finance/report/list';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// API 函数
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 财务报表 — GET /admin/finance/report/list?date=yyyy-MM */
|
||||||
|
export function getFinanceReport(params: ReportQueryParams): Promise<ApiResult<FinanceReportVO>> {
|
||||||
|
return get(reportList, params as Record<string, any>);
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import type { SelectValue } from 'ant-design-vue/es/select';
|
import type { SelectValue } from 'ant-design-vue/es/select';
|
||||||
import { useState } from '@/hooks';
|
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 type RangeValue = (typeof RANGE_OPTIONS)[number]['value'];
|
||||||
|
|
||||||
@@ -10,85 +13,44 @@ export type RangeValue = (typeof RANGE_OPTIONS)[number]['value'];
|
|||||||
|
|
||||||
/** 时间范围选项 */
|
/** 时间范围选项 */
|
||||||
export const RANGE_OPTIONS = [
|
export const RANGE_OPTIONS = [
|
||||||
{ value: 'month', label: '本月' },
|
{ value: 'month' as const, label: '本月' },
|
||||||
{ value: '3m', label: '近 3 个月' },
|
{ value: '3m' as const, label: '近 3 个月' },
|
||||||
{ value: '6m', label: '近 6 个月' },
|
{ value: '6m' as const, label: '近 6 个月' },
|
||||||
{ value: 'year', label: '本年' },
|
{ value: 'year' as const, label: '本年' },
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
|
/** 范围对应的月数 */
|
||||||
|
const RANGE_MONTHS: Record<RangeValue, number> = {
|
||||||
|
month: 1,
|
||||||
|
'3m': 3,
|
||||||
|
'6m': 6,
|
||||||
|
year: 12,
|
||||||
|
};
|
||||||
|
|
||||||
function isRangeValue(val: SelectValue): val is RangeValue {
|
function isRangeValue(val: SelectValue): val is RangeValue {
|
||||||
return typeof val === 'string' && RANGE_OPTIONS.some((item) => item.value === val);
|
return typeof val === 'string' && RANGE_OPTIONS.some((item) => item.value === val);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 假数据(数值自洽:总收入 - 总提现 = 平台余额)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
/** 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 位小数 */
|
/** 千分位 + 保留 2 位小数 */
|
||||||
function formatMoney(n: number): string {
|
function formatMoneyStr(value: string): string {
|
||||||
return (n || 0).toLocaleString('zh-CN', {
|
const n = parseFloat(value);
|
||||||
|
if (isNaN(n)) return '0.00';
|
||||||
|
return n.toLocaleString('zh-CN', {
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 字符串金额转数字 */
|
||||||
|
function toNumber(value: string): number {
|
||||||
|
const n = parseFloat(value);
|
||||||
|
return isNaN(n) ? 0 : n;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Model
|
// Model
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -98,22 +60,55 @@ function formatMoney(n: number): string {
|
|||||||
*/
|
*/
|
||||||
export function useReportsModel() {
|
export function useReportsModel() {
|
||||||
// ===== 时间范围 =====
|
// ===== 时间范围 =====
|
||||||
const [range, setRange] = useState<RangeValue>('month');
|
const range = ref<RangeValue>('month');
|
||||||
|
|
||||||
/** 切换时间范围(演示用:打日志,不改数据) */
|
// ===== 构建 API 参数(根据 range 推算查询起始月份) =====
|
||||||
const handleRangeChange = (val: SelectValue) => {
|
const buildQueryParams = () => {
|
||||||
if (!isRangeValue(val)) return;
|
const months = RANGE_MONTHS[range.value];
|
||||||
setRange(val);
|
const date = dayjs()
|
||||||
// TODO: 接入 API 时按 range 重新拉取汇总 / 图表 / 明细
|
.subtract(months - 1, 'month')
|
||||||
|
.format('YYYY-MM');
|
||||||
|
return { date };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===== 请求(range 变化时自动重新请求) =====
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
run: fetchReport,
|
||||||
|
} = useRequest<FinanceReportVO>(() => getFinanceReport(buildQueryParams()), {
|
||||||
|
refreshDeps: [range],
|
||||||
|
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 = ref({ ...SUMMARY });
|
const summary = computed(() => ({
|
||||||
|
totalIncome: data.value?.totalIncome || '0.00',
|
||||||
|
totalIncomeTendency: data.value?.totalIncomeTendency || '0',
|
||||||
|
totalWithdraw: data.value?.totalWithdrawIncome || '0.00',
|
||||||
|
totalWithdrawTendency: data.value?.totalWithdrawIncomeTendency || '0',
|
||||||
|
totalBalance: data.value?.totalBalanceAmount || '0.00',
|
||||||
|
}));
|
||||||
|
|
||||||
// ===== 图表 =====
|
// ===== 图表数据 =====
|
||||||
const chartData = ref([...CHART_DATA]);
|
const chartData = computed(() =>
|
||||||
|
filteredMonthList.value.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 = [
|
const detailColumns = [
|
||||||
{
|
{
|
||||||
title: '月份',
|
title: '月份',
|
||||||
@@ -156,10 +151,39 @@ export function useReportsModel() {
|
|||||||
customCell: (_record: any) => ({ style: { color: '#fa8c16' } }),
|
customCell: (_record: any) => ({ style: { color: '#fa8c16' } }),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const detailData = ref([...DETAIL_DATA]);
|
|
||||||
|
// ===== 明细表数据 =====
|
||||||
|
const detailData = computed(() =>
|
||||||
|
filteredMonthList.value.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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 范围变化时刷新图表(通过 page 中的 useEffect 监听 range) =====
|
||||||
|
|
||||||
|
/** 千分位格式金额(页面渲染用) */
|
||||||
|
function formatMoney(val: string | number): string {
|
||||||
|
if (typeof val === 'string') return formatMoneyStr(val);
|
||||||
|
return val.toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
range,
|
range,
|
||||||
|
loading,
|
||||||
handleRangeChange,
|
handleRangeChange,
|
||||||
summary,
|
summary,
|
||||||
chartData,
|
chartData,
|
||||||
|
|||||||
@@ -135,6 +135,11 @@
|
|||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #ff4d4f;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { defineComponent, reactive, ref, watch } from 'vue';
|
import { defineComponent, reactive, ref } from 'vue';
|
||||||
|
import { useEffect } from '@/hooks';
|
||||||
import { Modal, Radio, Input, Button, Upload, Image, Spin, message } from 'ant-design-vue';
|
import { Modal, Radio, Input, Button, Upload, Image, Spin, message } from 'ant-design-vue';
|
||||||
|
import type { UploadFile } from 'ant-design-vue';
|
||||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { uploadFile, OssUploadType } from '@/utils/oss';
|
||||||
import { getWithdrawInfo, postWithdrawAudit, type WithdrawInfoVO } from '../model/services';
|
import { getWithdrawInfo, postWithdrawAudit, type WithdrawInfoVO } from '../model/services';
|
||||||
import { getAuditStatusText, getPayStatusText } from '../model/useWithdrawModel';
|
import { getAuditStatusText, getPayStatusText } from '../model/useWithdrawModel';
|
||||||
import styles from './WithdrawDetailModal.module.less';
|
import styles from './WithdrawDetailModal.module.less';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
const MAX_IMAGE_COUNT = 3;
|
||||||
|
const MAX_IMAGE_SIZE = 5 * 1024 * 1024;
|
||||||
|
|
||||||
interface WithdrawDetailModalProps {
|
interface WithdrawDetailModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -30,7 +35,7 @@ export default defineComponent({
|
|||||||
const detailLoading = ref(false);
|
const detailLoading = ref(false);
|
||||||
|
|
||||||
const auditForm = reactive({ pass: true, remark: '' });
|
const auditForm = reactive({ pass: true, remark: '' });
|
||||||
const imageList = ref<any[]>([]);
|
const imageList = ref<UploadFile[]>([]);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
|
|
||||||
// ===== 拉取详情 =====
|
// ===== 拉取详情 =====
|
||||||
@@ -47,44 +52,91 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
// ===== 弹窗打开时拉取详情 =====
|
||||||
() => props.visible,
|
useEffect(() => {
|
||||||
async (val) => {
|
if (props.visible && props.record?.id) {
|
||||||
if (val && props.record?.id) {
|
|
||||||
auditForm.pass = true;
|
auditForm.pass = true;
|
||||||
auditForm.remark = '';
|
auditForm.remark = '';
|
||||||
imageList.value = [];
|
imageList.value = [];
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
await fetchDetail(props.record.id);
|
fetchDetail(props.record.id);
|
||||||
}
|
}
|
||||||
},
|
}, [() => props.visible]);
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== 上传 =====
|
// ===== 图片上传(OSS) =====
|
||||||
const beforeUpload = (file: any) => {
|
const beforeUpload = async (file: UploadFile) => {
|
||||||
if (file.size / 1024 / 1024 >= 5) {
|
const rawFile = file as unknown as File;
|
||||||
|
|
||||||
|
if (imageList.value.length >= MAX_IMAGE_COUNT) {
|
||||||
|
message.warning(`最多上传 ${MAX_IMAGE_COUNT} 张图片`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!/^image\//.test(rawFile.type)) {
|
||||||
|
message.error('请选择图片文件');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (rawFile.size >= MAX_IMAGE_SIZE) {
|
||||||
message.error('图片大小不能超过 5MB');
|
message.error('图片大小不能超过 5MB');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
imageList.value = [...imageList.value, file];
|
|
||||||
|
const uid = file.uid || `${Date.now()}-${Math.random()}`;
|
||||||
|
const uploadingItem: UploadFile = {
|
||||||
|
uid,
|
||||||
|
name: rawFile.name,
|
||||||
|
status: 'uploading',
|
||||||
|
};
|
||||||
|
imageList.value = [...imageList.value, uploadingItem];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await uploadFile(rawFile, OssUploadType.WithdrawAudit);
|
||||||
|
imageList.value = imageList.value.map((item) =>
|
||||||
|
item.uid === uid
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
status: 'done',
|
||||||
|
url: result.url,
|
||||||
|
thumbUrl: result.url,
|
||||||
|
}
|
||||||
|
: item,
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
imageList.value = imageList.value.filter((item) => item.uid !== uid);
|
||||||
|
message.error(err?.message || '图片上传失败');
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
const handleRemove = (file: any) => {
|
|
||||||
imageList.value = imageList.value.filter((f) => f.uid !== file.uid);
|
const handleRemove = (file: UploadFile) => {
|
||||||
|
imageList.value = imageList.value.filter((item) => item.uid !== file.uid);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== 提交审核 =====
|
// ===== 提交审核 =====
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!auditForm.remark.trim()) {
|
// 驳回:审核内容必填
|
||||||
message.warning('请输入审核内容');
|
if (!auditForm.pass && !auditForm.remark.trim()) {
|
||||||
|
message.warning('驳回时请填写审核内容');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 通过:必须上传图片
|
||||||
|
if (auditForm.pass) {
|
||||||
|
const hasDoneImage = imageList.value.some((item) => item.status === 'done' && item.url);
|
||||||
|
if (!hasDoneImage) {
|
||||||
|
message.warning('审核通过时必须上传审核图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (imageList.value.some((item) => item.status === 'uploading')) {
|
||||||
|
message.warning('图片仍在上传中,请稍候');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
const checkImg = imageList.value
|
const checkImg = imageList.value
|
||||||
.map((f) => f.name || f.url || '')
|
.filter((item) => item.status === 'done' && item.url)
|
||||||
.filter(Boolean)
|
.map((item) => item.url as string)
|
||||||
.join(',');
|
.join(',');
|
||||||
const res = await postWithdrawAudit({
|
const res = await postWithdrawAudit({
|
||||||
id: String(props.record.id),
|
id: String(props.record.id),
|
||||||
@@ -117,7 +169,7 @@ export default defineComponent({
|
|||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
const d = detail.value!;
|
const d = detail.value!;
|
||||||
const bankCardTypeLabel = d.bankCardType === 1 ? '个人' : d.bankCardType || '-';
|
const bankCardTypeLabel = d.bankCardType === 1 ? '个人' : d.bankCardType || '-';
|
||||||
const isPending = d.auditStatus === 0;
|
const isPending = d.auditStatus == 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -177,9 +229,12 @@ export default defineComponent({
|
|||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.formRow}>
|
<div class={styles.formRow}>
|
||||||
<span class={[styles.formLabel, styles.formLabelTop]}>审核内容</span>
|
<span class={[styles.formLabel, styles.formLabelTop]}>
|
||||||
|
{!auditForm.pass && <span class={styles.required}>*</span>}
|
||||||
|
审核内容
|
||||||
|
</span>
|
||||||
<TextArea
|
<TextArea
|
||||||
v-model={auditForm.remark}
|
v-model:value={auditForm.remark}
|
||||||
placeholder="请输入审核内容"
|
placeholder="请输入审核内容"
|
||||||
rows={4}
|
rows={4}
|
||||||
maxlength={500}
|
maxlength={500}
|
||||||
@@ -188,7 +243,10 @@ export default defineComponent({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.formRow}>
|
<div class={styles.formRow}>
|
||||||
<span class={[styles.formLabel, styles.formLabelTop]}>审核图片</span>
|
<span class={[styles.formLabel, styles.formLabelTop]}>
|
||||||
|
{auditForm.pass && <span class={styles.required}>*</span>}
|
||||||
|
审核图片
|
||||||
|
</span>
|
||||||
<Upload
|
<Upload
|
||||||
listType="picture-card"
|
listType="picture-card"
|
||||||
fileList={imageList.value}
|
fileList={imageList.value}
|
||||||
@@ -197,10 +255,10 @@ export default defineComponent({
|
|||||||
accept="image/*"
|
accept="image/*"
|
||||||
class={styles.uploader}
|
class={styles.uploader}
|
||||||
>
|
>
|
||||||
{imageList.value.length >= 3 ? null : (
|
{imageList.value.length >= MAX_IMAGE_COUNT ? null : (
|
||||||
<div class={styles.uploadTrigger}>
|
<div class={styles.uploadTrigger}>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
<div class={styles.uploadText}>粘贴图片到此处</div>
|
<div class={styles.uploadText}>请选择图片</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Upload>
|
</Upload>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export default defineComponent({
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 100,
|
width: 140,
|
||||||
fixed: 'right' as const,
|
fixed: 'right' as const,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -3,7 +3,7 @@ import { fetchOssSign } from '@/api/upload';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* OSS 上传类型
|
* OSS 上传类型
|
||||||
* 1=证件照 2=赛事封面 3=头像 4=富文本 5=Banner
|
* 1=证件照 2=赛事封面 3=头像 4=富文本 5=Banner 6=提现审核
|
||||||
*/
|
*/
|
||||||
export const OssUploadType = {
|
export const OssUploadType = {
|
||||||
IdCard: '1',
|
IdCard: '1',
|
||||||
@@ -11,6 +11,7 @@ export const OssUploadType = {
|
|||||||
Avatar: '3',
|
Avatar: '3',
|
||||||
RichText: '4',
|
RichText: '4',
|
||||||
Banner: '5',
|
Banner: '5',
|
||||||
|
WithdrawAudit: '6',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type OssUploadTypeValue = (typeof OssUploadType)[keyof typeof OssUploadType];
|
export type OssUploadTypeValue = (typeof OssUploadType)[keyof typeof OssUploadType];
|
||||||
|
|||||||
Reference in New Issue
Block a user