mirror of
https://gitee.com/bootx/dax-pay-ui
synced 2026-08-12 23:45:39 +08:00
feat(dashboard): 渠道分布修复(provider 维度) + analytics 9 维度全量真实数据
修复 channelDist SQL 报错(pay_trade 无 channel 列): - 改为 provider 维度(PayProviderEnum), pay_trade 加 provider 冗余列 - channelDist → providerDist 全面重命名(类/方法/URL/widget 文件) - 新增 providerEnum.ts + 10 语种 payment/provider/common.json analytics 9 维度全量真实数据驱动: - useAnalyticsData 重写为异步加载, Promise.allSettled 并发 8 个 API - 9 个组件加 loading prop + <a-skeleton> - overview 卡片基于后端 prev* 字段计算环比 - 修复 trade-overview/trade-trend widget 的 API 签名兼容(对象参数) i18n 10 语种同步: - dashboard/workspace.json: channelDist → providerDist - 新增 payment/provider/common.json × 10 (PayProviderEnum 7 项)
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import type { Result } from '#/types/web';
|
||||
|
||||
import { defHttp } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 工作台/分析页交易聚合统计 API
|
||||
*
|
||||
* 对接后端 /admin/dashboard/trade/* 聚合接口, 供 workspace widget 与 analytics 9 维度图表使用。
|
||||
*
|
||||
* ## 参数约定
|
||||
* - **天数模式**: `days` 参数(近 N 天含今天)
|
||||
* - **区间模式**: `start` + `end` 参数(yyyy-MM-dd, 均包含), 同时传时优先区间模式
|
||||
*
|
||||
* 金额字段单位为分, 前端按需转元展示。
|
||||
*/
|
||||
export const DashboardTradeApi = {
|
||||
/**
|
||||
* 交易概览(今日/昨日快捷 或 自定义区间含上期对比)
|
||||
* @param date 'today' | 'yesterday' 快捷模式, 仅在未传 start/end 时生效
|
||||
* @param start yyyy-MM-dd(包含), 与 end 同时传时进入区间模式
|
||||
* @param end yyyy-MM-dd(包含)
|
||||
*/
|
||||
overview(params: {
|
||||
date?: 'today' | 'yesterday';
|
||||
end?: string;
|
||||
start?: string;
|
||||
}): Promise<Result<TradeOverviewResult>> {
|
||||
return defHttp.get({ url: '/admin/dashboard/trade/overview', params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 交易趋势(每日成交金额 + 笔数)
|
||||
*/
|
||||
trend(params: { days?: number; end?: string; start?: string }): Promise<Result<TradeTrendItemResult[]>> {
|
||||
return defHttp.get({ url: '/admin/dashboard/trade/trend', params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 退款趋势(每日退款金额 + 笔数)
|
||||
*/
|
||||
refundTrend(params: { days?: number; end?: string; start?: string }): Promise<Result<RefundTrendItemResult[]>> {
|
||||
return defHttp.get({ url: '/admin/dashboard/trade/refund-trend', params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 支付渠道分布(各渠道成交金额 + 笔数)
|
||||
*/
|
||||
providerDist(params: { days?: number; end?: string; start?: string }): Promise<Result<ProviderDistItemResult[]>> {
|
||||
return defHttp.get({ url: '/admin/dashboard/trade/provider-dist', params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 支付渠道成功率(各渠道 success_count / total_count * 100)
|
||||
*/
|
||||
providerSuccess(params: { days?: number; end?: string; start?: string }): Promise<Result<ProviderSuccessItemResult[]>> {
|
||||
return defHttp.get({ url: '/admin/dashboard/trade/provider-success', params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 24 小时时段分布(各小时成交金额 + 笔数, 已补齐 0-23)
|
||||
*/
|
||||
hourlyDist(params: { days?: number; end?: string; start?: string }): Promise<Result<HourlyDistItemResult[]>> {
|
||||
return defHttp.get({ url: '/admin/dashboard/trade/hourly-dist', params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 金额区间分桶(5 档: 0-50/50-200/200-1000/1000-5000/5000+, 已补齐)
|
||||
*/
|
||||
amountRange(params: { days?: number; end?: string; start?: string }): Promise<Result<AmountRangeItemResult[]>> {
|
||||
return defHttp.get({ url: '/admin/dashboard/trade/amount-range', params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 商户交易额排名
|
||||
* @param limit 返回前 N 名(默认 10, 上限 50)
|
||||
*/
|
||||
merchantRank(params: { days?: number; end?: string; limit?: number; start?: string }): Promise<Result<MerchantRankItemResult[]>> {
|
||||
return defHttp.get({ url: '/admin/dashboard/trade/merchant-rank', params });
|
||||
},
|
||||
};
|
||||
|
||||
/** 交易概览结果(金额单位: 分) */
|
||||
export interface TradeOverviewResult {
|
||||
// ===== 本期 =====
|
||||
/** 成交金额(分) */
|
||||
successAmount?: number;
|
||||
/** 成交笔数 */
|
||||
successCount?: number;
|
||||
/** 退款金额(分) */
|
||||
refundAmount?: number;
|
||||
/** 退款笔数 */
|
||||
refundCount?: number;
|
||||
/** 总下单笔数(含成功/失败/关闭, 用于成功率分母, 按 create_time 口径) */
|
||||
totalOrders?: number;
|
||||
// ===== 上期(用于环比, undefined 表示上期无数据) =====
|
||||
prevSuccessAmount?: number;
|
||||
prevSuccessCount?: number;
|
||||
prevRefundAmount?: number;
|
||||
prevRefundCount?: number;
|
||||
prevTotalOrders?: number;
|
||||
}
|
||||
|
||||
/** 交易趋势单项(日期 + 金额 + 笔数) */
|
||||
export interface TradeTrendItemResult {
|
||||
/** 日期(yyyy-MM-dd) */
|
||||
date?: string;
|
||||
/** 成交金额(分) */
|
||||
amount?: number;
|
||||
/** 成交笔数 */
|
||||
count?: number;
|
||||
}
|
||||
|
||||
/** 退款趋势单项 */
|
||||
export interface RefundTrendItemResult {
|
||||
date?: string;
|
||||
amount?: number;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
/** 支付渠道分布单项 */
|
||||
export interface ProviderDistItemResult {
|
||||
/** 支付渠道编码(如 wechat/alipay/union_pay) */
|
||||
provider?: string;
|
||||
/** 成交金额(分) */
|
||||
amount?: number;
|
||||
/** 成交笔数 */
|
||||
count?: number;
|
||||
}
|
||||
|
||||
/** 支付渠道成功率单项 */
|
||||
export interface ProviderSuccessItemResult {
|
||||
/** 支付渠道编码 */
|
||||
provider?: string;
|
||||
/** 成功率(0-100) */
|
||||
rate?: number;
|
||||
}
|
||||
|
||||
/** 24 小时时段分布单项 */
|
||||
export interface HourlyDistItemResult {
|
||||
/** 小时(0-23) */
|
||||
hour?: number;
|
||||
/** 成交金额(分) */
|
||||
amount?: number;
|
||||
/** 成交笔数 */
|
||||
count?: number;
|
||||
}
|
||||
|
||||
/** 金额区间分桶单项 */
|
||||
export interface AmountRangeItemResult {
|
||||
/** 区间标签(如 '0-50', '5000+') */
|
||||
bucket?: string;
|
||||
/** 笔数 */
|
||||
count?: number;
|
||||
}
|
||||
|
||||
/** 商户交易额排名单项 */
|
||||
export interface MerchantRankItemResult {
|
||||
/** 商户号 */
|
||||
mchNo?: string;
|
||||
/** 商户名称 */
|
||||
merchantName?: string;
|
||||
/** 成交金额(分) */
|
||||
amount?: number;
|
||||
/** 成交笔数 */
|
||||
orders?: number;
|
||||
/** 占比百分比(0-100) */
|
||||
proportion?: number;
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './channelEnum';
|
||||
export * from './productEnum';
|
||||
export * from './providerEnum';
|
||||
|
||||
51
apps/daxpay-admin/src/enums/payment/providerEnum.ts
Normal file
51
apps/daxpay-admin/src/enums/payment/providerEnum.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 支付渠道枚举
|
||||
*
|
||||
* 对齐后端 PayProviderEnum, 表达付款方使用的钱包或支付渠道(C 端视角), 非支付通道。
|
||||
* 字典: pay_provider
|
||||
*/
|
||||
export enum ProviderEnum {
|
||||
/** 聚合支付(收银台/一码多付等) */
|
||||
AGGREGATE_PAY = 'aggregate_pay',
|
||||
/** 微信(C 端钱包渠道) */
|
||||
WECHAT = 'wechat',
|
||||
/** 支付宝 */
|
||||
ALIPAY = 'alipay',
|
||||
/** 银联 */
|
||||
UNION_PAY = 'union_pay',
|
||||
/** Visa 卡组织 */
|
||||
VISA = 'visa',
|
||||
/** 万事达卡组织 */
|
||||
MASTERCARD = 'mastercard',
|
||||
/** 抖音支付 */
|
||||
DOUYIN = 'douyin',
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付渠道国际化Key映射
|
||||
*
|
||||
* 用于支付渠道分布报表等"按用户付款方式统计"的场景,
|
||||
* 与支付通道 channelEnum(系统对接通道) 区分。
|
||||
*/
|
||||
export const providerI18nMap: Record<string, string> = {
|
||||
[ProviderEnum.AGGREGATE_PAY]: 'payment.provider.common.aggregatePay',
|
||||
[ProviderEnum.WECHAT]: 'payment.provider.common.wechat',
|
||||
[ProviderEnum.ALIPAY]: 'payment.provider.common.alipay',
|
||||
[ProviderEnum.UNION_PAY]: 'payment.provider.common.unionPay',
|
||||
[ProviderEnum.VISA]: 'payment.provider.common.visa',
|
||||
[ProviderEnum.MASTERCARD]: 'payment.provider.common.mastercard',
|
||||
[ProviderEnum.DOUYIN]: 'payment.provider.common.douyin',
|
||||
};
|
||||
|
||||
/**
|
||||
* 支付渠道默认名称映射(降级用, 正常走 i18n)
|
||||
*/
|
||||
export const providerNameMap: Record<string, string> = {
|
||||
[ProviderEnum.AGGREGATE_PAY]: '聚合支付',
|
||||
[ProviderEnum.WECHAT]: '微信支付',
|
||||
[ProviderEnum.ALIPAY]: '支付宝',
|
||||
[ProviderEnum.UNION_PAY]: '银联',
|
||||
[ProviderEnum.VISA]: 'Visa',
|
||||
[ProviderEnum.MASTERCARD]: '万事达',
|
||||
[ProviderEnum.DOUYIN]: '抖音支付',
|
||||
};
|
||||
@@ -18,7 +18,7 @@
|
||||
"tradeOverview": "Trade Overview",
|
||||
"payOrder": "Payment Orders",
|
||||
"tradeTrend": "Trade Trend",
|
||||
"channelDist": "Payment Channel",
|
||||
"providerDist": "Payment Method Distribution",
|
||||
"noticeList": "Notices"
|
||||
},
|
||||
"tradeOverview": {
|
||||
@@ -29,14 +29,6 @@
|
||||
"refundAmount": "Refund Amount (CNY)",
|
||||
"refundCount": "Refund Count"
|
||||
},
|
||||
"payOrder": {
|
||||
"developing": "Payment order management in development",
|
||||
"status": {
|
||||
"success": "Success",
|
||||
"fail": "Failed",
|
||||
"closed": "Closed"
|
||||
}
|
||||
},
|
||||
"tradeTrend": {
|
||||
"last7days": "7 Days",
|
||||
"last30days": "30 Days"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"aggregatePay": "Aggregate Pay",
|
||||
"wechat": "WeChat Pay",
|
||||
"alipay": "Alipay",
|
||||
"unionPay": "UnionPay",
|
||||
"visa": "Visa",
|
||||
"mastercard": "Mastercard",
|
||||
"douyin": "Douyin Pay"
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"tradeOverview": "Ikhtisar Perdagangan",
|
||||
"payOrder": "Perintah Pembayaran",
|
||||
"tradeTrend": "Tren Perdagangan",
|
||||
"channelDist": "Saluran Pembayaran",
|
||||
"providerDist": "Distribusi Metode Pembayaran",
|
||||
"noticeList": "Pemberitahuan"
|
||||
},
|
||||
"tradeOverview": {
|
||||
@@ -29,14 +29,6 @@
|
||||
"refundAmount": "Jumlah Pengembalian Dana (CNY)",
|
||||
"refundCount": "Jumlah Pengembalian Dana"
|
||||
},
|
||||
"payOrder": {
|
||||
"developing": "Manajemen pesanan pembayaran dalam pengembangan",
|
||||
"status": {
|
||||
"success": "Kesuksesan",
|
||||
"fail": "Gagal",
|
||||
"closed": "Tertutup"
|
||||
}
|
||||
},
|
||||
"tradeTrend": {
|
||||
"last7days": "7 hari",
|
||||
"last30days": "30 Hari"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"aggregatePay": "Pembayaran Agregat",
|
||||
"wechat": "Pembayaran WeChat",
|
||||
"alipay": "Alipay",
|
||||
"unionPay": "UnionPay",
|
||||
"visa": "Visa",
|
||||
"mastercard": "Mastercard",
|
||||
"douyin": "Pembayaran Douyin"
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"tradeOverview": "貿易概要",
|
||||
"payOrder": "支払い命令",
|
||||
"tradeTrend": "貿易動向",
|
||||
"channelDist": "支払いチャネル",
|
||||
"providerDist": "決済方法分布",
|
||||
"noticeList": "お知らせ"
|
||||
},
|
||||
"tradeOverview": {
|
||||
@@ -29,14 +29,6 @@
|
||||
"refundAmount": "払い戻し金額 (CNY)",
|
||||
"refundCount": "払い戻し回数"
|
||||
},
|
||||
"payOrder": {
|
||||
"developing": "支払い注文管理は開発中",
|
||||
"status": {
|
||||
"success": "成功",
|
||||
"fail": "失敗した",
|
||||
"closed": "閉店"
|
||||
}
|
||||
},
|
||||
"tradeTrend": {
|
||||
"last7days": "7日間",
|
||||
"last30days": "30日"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"aggregatePay": "統合決済",
|
||||
"wechat": "WeChat Pay",
|
||||
"alipay": "Alipay",
|
||||
"unionPay": "UnionPay",
|
||||
"visa": "Visa",
|
||||
"mastercard": "Mastercard",
|
||||
"douyin": "Douyin Pay"
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"tradeOverview": "무역개요",
|
||||
"payOrder": "지불 주문",
|
||||
"tradeTrend": "무역동향",
|
||||
"channelDist": "결제 채널",
|
||||
"providerDist": "결제 수단 분포",
|
||||
"noticeList": "공지사항"
|
||||
},
|
||||
"tradeOverview": {
|
||||
@@ -29,14 +29,6 @@
|
||||
"refundAmount": "환불 금액(CNY)",
|
||||
"refundCount": "환불 횟수"
|
||||
},
|
||||
"payOrder": {
|
||||
"developing": "결제 주문 관리 개발 중",
|
||||
"status": {
|
||||
"success": "성공",
|
||||
"fail": "실패한",
|
||||
"closed": "닫은"
|
||||
}
|
||||
},
|
||||
"tradeTrend": {
|
||||
"last7days": "7일",
|
||||
"last30days": "30일"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"aggregatePay": "통합 결제",
|
||||
"wechat": "위챗페이",
|
||||
"alipay": "알리페이",
|
||||
"unionPay": "유니온페이",
|
||||
"visa": "Visa",
|
||||
"mastercard": "Mastercard",
|
||||
"douyin": "두인페이"
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"tradeOverview": "Tinjauan Perdagangan",
|
||||
"payOrder": "Pesanan Pembayaran",
|
||||
"tradeTrend": "Trend Perdagangan",
|
||||
"channelDist": "Saluran Pembayaran",
|
||||
"providerDist": "Agihan Kaedah Pembayaran",
|
||||
"noticeList": "Notis"
|
||||
},
|
||||
"tradeOverview": {
|
||||
@@ -29,14 +29,6 @@
|
||||
"refundAmount": "Amaun Bayaran Balik (CNY)",
|
||||
"refundCount": "Kiraan Bayaran Balik"
|
||||
},
|
||||
"payOrder": {
|
||||
"developing": "Pengurusan pesanan pembayaran dalam pembangunan",
|
||||
"status": {
|
||||
"success": "Kejayaan",
|
||||
"fail": "gagal",
|
||||
"closed": "tertutup"
|
||||
}
|
||||
},
|
||||
"tradeTrend": {
|
||||
"last7days": "7 Hari",
|
||||
"last30days": "30 Hari"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"aggregatePay": "Bayaran Agregat",
|
||||
"wechat": "WeChat Pay",
|
||||
"alipay": "Alipay",
|
||||
"unionPay": "UnionPay",
|
||||
"visa": "Visa",
|
||||
"mastercard": "Mastercard",
|
||||
"douyin": "Douyin Pay"
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"tradeOverview": "ภาพรวมการค้า",
|
||||
"payOrder": "คำสั่งการชำระเงิน",
|
||||
"tradeTrend": "แนวโน้มการค้า",
|
||||
"channelDist": "ช่องทางการชำระเงิน",
|
||||
"providerDist": "การกระจายวิธีการชำระเงิน",
|
||||
"noticeList": "ประกาศ"
|
||||
},
|
||||
"tradeOverview": {
|
||||
@@ -29,14 +29,6 @@
|
||||
"refundAmount": "จำนวนเงินคืน (CNY)",
|
||||
"refundCount": "จำนวนการคืนเงิน"
|
||||
},
|
||||
"payOrder": {
|
||||
"developing": "การจัดการคำสั่งการชำระเงินอยู่ระหว่างการพัฒนา",
|
||||
"status": {
|
||||
"success": "ความสำเร็จ",
|
||||
"fail": "ล้มเหลว",
|
||||
"closed": "ปิด"
|
||||
}
|
||||
},
|
||||
"tradeTrend": {
|
||||
"last7days": "7 วัน",
|
||||
"last30days": "30 วัน"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"aggregatePay": "ชำระเงินแบบรวม",
|
||||
"wechat": "WeChat Pay",
|
||||
"alipay": "Alipay",
|
||||
"unionPay": "UnionPay",
|
||||
"visa": "Visa",
|
||||
"mastercard": "Mastercard",
|
||||
"douyin": "Douyin Pay"
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"tradeOverview": "Tổng quan thương mại",
|
||||
"payOrder": "Lệnh thanh toán",
|
||||
"tradeTrend": "Xu hướng thương mại",
|
||||
"channelDist": "Kênh thanh toán",
|
||||
"providerDist": "Phân bố phương thức thanh toán",
|
||||
"noticeList": "Thông báo"
|
||||
},
|
||||
"tradeOverview": {
|
||||
@@ -29,14 +29,6 @@
|
||||
"refundAmount": "Số tiền hoàn lại (CNY)",
|
||||
"refundCount": "Số tiền hoàn lại"
|
||||
},
|
||||
"payOrder": {
|
||||
"developing": "Quản lý lệnh thanh toán đang phát triển",
|
||||
"status": {
|
||||
"success": "Thành công",
|
||||
"fail": "Thất bại",
|
||||
"closed": "Đã đóng"
|
||||
}
|
||||
},
|
||||
"tradeTrend": {
|
||||
"last7days": "7 ngày",
|
||||
"last30days": "30 ngày"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"aggregatePay": "Thanh toán tổng hợp",
|
||||
"wechat": "WeChat Pay",
|
||||
"alipay": "Alipay",
|
||||
"unionPay": "UnionPay",
|
||||
"visa": "Visa",
|
||||
"mastercard": "Mastercard",
|
||||
"douyin": "Douyin Pay"
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"tradeOverview": "交易概览",
|
||||
"payOrder": "支付订单",
|
||||
"tradeTrend": "交易趋势",
|
||||
"channelDist": "支付渠道",
|
||||
"providerDist": "支付渠道分布",
|
||||
"noticeList": "系统公告"
|
||||
},
|
||||
"tradeOverview": {
|
||||
@@ -29,14 +29,6 @@
|
||||
"refundAmount": "退款金额(元)",
|
||||
"refundCount": "退款笔数"
|
||||
},
|
||||
"payOrder": {
|
||||
"developing": "支付订单管理开发中",
|
||||
"status": {
|
||||
"success": "成功",
|
||||
"fail": "失败",
|
||||
"closed": "已关闭"
|
||||
}
|
||||
},
|
||||
"tradeTrend": {
|
||||
"last7days": "近 7 天",
|
||||
"last30days": "近 30 天"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"aggregatePay": "聚合支付",
|
||||
"wechat": "微信支付",
|
||||
"alipay": "支付宝",
|
||||
"unionPay": "银联",
|
||||
"visa": "Visa",
|
||||
"mastercard": "万事达",
|
||||
"douyin": "抖音支付"
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"tradeOverview": "交易概覽",
|
||||
"payOrder": "支付訂單",
|
||||
"tradeTrend": "交易趨勢",
|
||||
"channelDist": "支付渠道",
|
||||
"providerDist": "支付渠道分佈",
|
||||
"noticeList": "系統公告"
|
||||
},
|
||||
"tradeOverview": {
|
||||
@@ -29,14 +29,6 @@
|
||||
"refundAmount": "退款金額(元)",
|
||||
"refundCount": "退款筆數"
|
||||
},
|
||||
"payOrder": {
|
||||
"developing": "支付訂單管理開發中",
|
||||
"status": {
|
||||
"success": "成功",
|
||||
"fail": "失敗",
|
||||
"closed": "已關閉"
|
||||
}
|
||||
},
|
||||
"tradeTrend": {
|
||||
"last7days": "近 7 天",
|
||||
"last30days": "近 30 天"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"aggregatePay": "聚合支付",
|
||||
"wechat": "微信支付",
|
||||
"alipay": "支付寶",
|
||||
"unionPay": "銀聯",
|
||||
"visa": "Visa",
|
||||
"mastercard": "萬事達",
|
||||
"douyin": "抖音支付"
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"tradeOverview": "交易概覽",
|
||||
"payOrder": "支付訂單",
|
||||
"tradeTrend": "交易趨勢",
|
||||
"channelDist": "支付渠道",
|
||||
"providerDist": "支付渠道分佈",
|
||||
"noticeList": "系統公告"
|
||||
},
|
||||
"tradeOverview": {
|
||||
@@ -29,14 +29,6 @@
|
||||
"refundAmount": "退款金額(元)",
|
||||
"refundCount": "退款筆數"
|
||||
},
|
||||
"payOrder": {
|
||||
"developing": "支付訂單管理開發中",
|
||||
"status": {
|
||||
"success": "成功",
|
||||
"fail": "失敗",
|
||||
"closed": "已關閉"
|
||||
}
|
||||
},
|
||||
"tradeTrend": {
|
||||
"last7days": "近 7 天",
|
||||
"last30days": "近 30 天"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"aggregatePay": "聚合支付",
|
||||
"wechat": "微信支付",
|
||||
"alipay": "支付寶",
|
||||
"unionPay": "銀聯",
|
||||
"visa": "Visa",
|
||||
"mastercard": "萬事達",
|
||||
"douyin": "抖音支付"
|
||||
}
|
||||
@@ -10,11 +10,14 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: undefined,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** 单笔金额区间分桶(用户消费特征) */
|
||||
data?: AmountRangeItem[];
|
||||
/** 加载中(显示骨架屏) */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
@@ -51,6 +54,7 @@
|
||||
<a-card variant="borderless" class="!h-full min-h-[320px] !bg-card">
|
||||
<template #title>{{ $t('dashboard.analytics.amountRange.title') }}</template>
|
||||
|
||||
<EchartsUI ref="chartRef" class="h-[320px]" />
|
||||
<a-skeleton v-if="loading" active :paragraph="{ rows: 6 }" />
|
||||
<EchartsUI v-else ref="chartRef" class="h-[320px]" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
@@ -10,11 +10,14 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: undefined,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** 各支付渠道成功率(横向对比,一眼看出渠道质量差异) */
|
||||
data?: ChannelSuccessItem[];
|
||||
/** 加载中(显示骨架屏) */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
@@ -57,6 +60,7 @@
|
||||
<a-card variant="borderless" class="!h-full min-h-[320px] !bg-card">
|
||||
<template #title>{{ $t('dashboard.analytics.channelSuccess.title') }}</template>
|
||||
|
||||
<EchartsUI ref="chartRef" class="h-[320px]" />
|
||||
<a-skeleton v-if="loading" active :paragraph="{ rows: 6 }" />
|
||||
<EchartsUI v-else ref="chartRef" class="h-[320px]" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
@@ -10,11 +10,14 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: undefined,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** 各支付通道交易笔数 */
|
||||
data?: NameValue[];
|
||||
/** 加载中(显示骨架屏) */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
@@ -51,6 +54,7 @@
|
||||
<a-card variant="borderless" class="!h-full min-h-[320px] !bg-card">
|
||||
<template #title>{{ $t('dashboard.analytics.channelVolume.title') }}</template>
|
||||
|
||||
<EchartsUI ref="chartRef" class="h-[320px]" />
|
||||
<a-skeleton v-if="loading" active :paragraph="{ rows: 6 }" />
|
||||
<EchartsUI v-else ref="chartRef" class="h-[320px]" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: undefined,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** 24 小时交易分布(长度 24 数组,下标即小时) */
|
||||
data?: number[];
|
||||
/** 加载中(显示骨架屏) */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
@@ -46,6 +49,7 @@
|
||||
<a-card variant="borderless" class="!h-full min-h-[320px] !bg-card">
|
||||
<template #title>{{ $t('dashboard.analytics.hourlyDist.title') }}</template>
|
||||
|
||||
<EchartsUI ref="chartRef" class="h-[320px]" />
|
||||
<a-skeleton v-if="loading" active :paragraph="{ rows: 6 }" />
|
||||
<EchartsUI v-else ref="chartRef" class="h-[320px]" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: () => [],
|
||||
loading: false,
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** 商户交易额排名 Top10 */
|
||||
data?: MerchantRankItem[];
|
||||
/** 加载中(显示骨架屏) */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
// 表格列定义(排名 / 商户 / 交易额 / 笔数 / 占比)
|
||||
@@ -45,6 +48,7 @@
|
||||
<template>
|
||||
<a-card variant="borderless" class="!bg-card">
|
||||
<template #title>{{ $t('dashboard.analytics.merchantRank.title') }}</template>
|
||||
<a-table :columns="columns" :data-source="dataSource" :pagination="false" row-key="merchantName" size="small" />
|
||||
<a-skeleton v-if="loading" active :paragraph="{ rows: 5 }" />
|
||||
<a-table v-else :columns="columns" :data-source="dataSource" :pagination="false" row-key="merchantName" size="small" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
@@ -7,11 +7,14 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: () => [],
|
||||
loading: false,
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** 6 个指标卡片数据(规模 + 质量) */
|
||||
data?: OverviewStat[];
|
||||
/** 加载中(显示骨架屏) */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
// 数值千分位格式化
|
||||
@@ -22,7 +25,14 @@
|
||||
|
||||
<template>
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col v-for="item in props.data" :key="item.key" :span="4">
|
||||
<template v-if="loading">
|
||||
<a-col v-for="i in 6" :key="i" :span="4">
|
||||
<a-card variant="borderless" class="!bg-card">
|
||||
<a-skeleton active :paragraph="{ rows: 2 }" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
</template>
|
||||
<a-col v-for="item in props.data" v-else :key="item.key" :span="4">
|
||||
<a-card variant="borderless" class="!bg-card">
|
||||
<div class="text-foreground/60 text-sm">
|
||||
{{ $t(`dashboard.analytics.overview.${item.key}`) }}
|
||||
@@ -34,9 +44,10 @@
|
||||
</div>
|
||||
<div class="mt-1 flex items-center text-xs">
|
||||
<!-- 涨跌色遵循中国惯例:涨红跌绿(西方为涨绿跌红) -->
|
||||
<span :class="item.chainRatio >= 0 ? 'text-red-500' : 'text-emerald-500'">
|
||||
<span v-if="item.chainRatio !== null" :class="item.chainRatio >= 0 ? 'text-red-500' : 'text-emerald-500'">
|
||||
{{ item.chainRatio >= 0 ? '↑' : '↓' }} {{ Math.abs(item.chainRatio) }}%
|
||||
</span>
|
||||
<span v-else class="text-foreground/40">—</span>
|
||||
<span class="text-foreground/40 ml-1">{{ $t('dashboard.analytics.overview.chainRatio') }}</span>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
@@ -10,11 +10,14 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: undefined,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** 各支付方式交易额占比 */
|
||||
data?: NameValue[];
|
||||
/** 加载中(显示骨架屏) */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
@@ -46,6 +49,7 @@
|
||||
<a-card variant="borderless" class="!h-full min-h-[320px] !bg-card">
|
||||
<template #title>{{ $t('dashboard.analytics.payMethod.title') }}</template>
|
||||
|
||||
<EchartsUI ref="chartRef" class="h-[320px]" />
|
||||
<a-skeleton v-if="loading" active :paragraph="{ rows: 6 }" />
|
||||
<EchartsUI v-else ref="chartRef" class="h-[320px]" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: undefined,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** 退款趋势(日期 + 退款额) */
|
||||
data?: { amounts: number[]; dates: string[] };
|
||||
/** 加载中(显示骨架屏) */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
@@ -51,6 +54,7 @@
|
||||
<a-card variant="borderless" class="!h-full min-h-[320px] !bg-card">
|
||||
<template #title>{{ $t('dashboard.analytics.refundTrend.title') }}</template>
|
||||
|
||||
<EchartsUI ref="chartRef" class="h-[320px]" />
|
||||
<a-skeleton v-if="loading" active :paragraph="{ rows: 6 }" />
|
||||
<EchartsUI v-else ref="chartRef" class="h-[320px]" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
@@ -10,11 +10,14 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: undefined,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** 交易趋势序列(日期 + 多度量) */
|
||||
data?: TrendSeries;
|
||||
/** 加载中(显示骨架屏) */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
// 度量切换:交易额 / 交易笔数 / 客单价
|
||||
@@ -69,6 +72,7 @@
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<EchartsUI ref="chartRef" class="h-[320px]" />
|
||||
<a-skeleton v-if="loading" active :paragraph="{ rows: 6 }" />
|
||||
<EchartsUI v-else ref="chartRef" class="h-[320px]" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,11 @@ import { $t } from '@vben/locales';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { DashboardTradeApi } from '#/api/payment/dashboard/trade-dashboard.api';
|
||||
import { providerI18nMap, providerNameMap } from '#/enums/payment/providerEnum';
|
||||
|
||||
import { emptyAnalyticsData } from '../types';
|
||||
|
||||
/** 预设范围 → [start, end](YYYY-MM-DD);本月取月初到今天(未来无数据) */
|
||||
function computePreset(key: PresetKey): DateRange {
|
||||
const today = dayjs();
|
||||
@@ -25,128 +30,47 @@ function computePreset(key: PresetKey): DateRange {
|
||||
}
|
||||
}
|
||||
|
||||
/** 按 [start, end] 生成日期标签(MM-DD,含首尾) */
|
||||
function genDateLabels(start: string, end: string): string[] {
|
||||
const labels: string[] = [];
|
||||
let cur = dayjs(start);
|
||||
const last = dayjs(end);
|
||||
while (cur.isBefore(last) || cur.isSame(last, 'day')) {
|
||||
labels.push(cur.format('MM-DD'));
|
||||
cur = cur.add(1, 'day');
|
||||
/** 支付渠道编码 → 展示名: 优先 i18n 映射, 无映射时降级原编码 */
|
||||
function providerLabel(provider?: string): string {
|
||||
if (!provider) return '-';
|
||||
const i18nKey = providerI18nMap[provider];
|
||||
if (i18nKey) {
|
||||
const text = $t(i18nKey);
|
||||
if (text && text !== i18nKey) return text;
|
||||
}
|
||||
return labels;
|
||||
return providerNameMap[provider] ?? provider;
|
||||
}
|
||||
|
||||
/** 基于索引的稳定波动函数(同输入同输出,避免每次切换数据跳变) */
|
||||
function wave(i: number, seed: number): number {
|
||||
return Math.sin(i * 0.8 + seed) * 0.5 + Math.cos(i * 1.3 + seed) * 0.3;
|
||||
/** 金额分(后端) → 元(前端展示), 保留整数 */
|
||||
function fenToYuan(fen?: number): number {
|
||||
if (!fen || fen <= 0) return 0;
|
||||
return Math.round(fen / 100);
|
||||
}
|
||||
|
||||
/** 按日期范围构造全量 mock 数据(后端聚合 API 就绪后替换为真实请求) */
|
||||
function buildData(start: string, end: string): AnalyticsData {
|
||||
const dates = genDateLabels(start, end);
|
||||
const n = dates.length;
|
||||
|
||||
// 交易额(元):基线 + 上升趋势 + 周期波动
|
||||
const amounts = dates.map((_, i) => Math.round(50_000 + i * 600 + wave(i, 1) * 12_000));
|
||||
// 交易笔数
|
||||
const orders = dates.map((_, i) => Math.round(800 + i * 8 + wave(i, 2) * 120));
|
||||
// 客单价 = 交易额 / 笔数
|
||||
const avgAmounts = amounts.map((a, i) => Math.round(a / (orders[i] || 1)));
|
||||
|
||||
// 汇总指标
|
||||
const totalAmount = amounts.reduce((s, v) => s + v, 0);
|
||||
const totalOrders = orders.reduce((s, v) => s + v, 0);
|
||||
const refundAmount = Math.round(totalAmount * 0.038);
|
||||
const avgAmount = Math.round(totalAmount / totalOrders);
|
||||
|
||||
// 环比百分比(mock,随范围稳定)
|
||||
const ratio = (base: number, seed: number) => Math.round((base + wave(n, seed) * 5) * 10) / 10;
|
||||
|
||||
// 退款趋势(元)
|
||||
const refundTrendAmounts = dates.map((_, i) => Math.round(1800 + wave(i, 3) * 600 + i * 20));
|
||||
|
||||
// 24h 时段分布:模拟双高峰(午间 12 点、晚间 20 点),凌晨低谷
|
||||
const hourlyDist = Array.from({ length: 24 }).map((_, h) => {
|
||||
const noonPeak = Math.exp(-Math.pow((h - 12) / 4, 2)) * 1000;
|
||||
const eveningPeak = Math.exp(-Math.pow((h - 20) / 3, 2)) * 800;
|
||||
const noise = wave(h, 4) * 100;
|
||||
return Math.max(50, Math.round(noonPeak + eveningPeak + noise + 100));
|
||||
});
|
||||
|
||||
return {
|
||||
overview: [
|
||||
{ key: 'totalAmount', value: totalAmount, prefix: '¥', chainRatio: ratio(8.5, 1) },
|
||||
{ key: 'totalOrders', value: totalOrders, chainRatio: ratio(6.2, 2) },
|
||||
{ key: 'avgAmount', value: avgAmount, prefix: '¥', chainRatio: ratio(2.1, 3) },
|
||||
{ key: 'successRate', value: 97.6, suffix: '%', chainRatio: ratio(0.3, 4) },
|
||||
{ key: 'refundAmount', value: refundAmount, prefix: '¥', chainRatio: ratio(-3.4, 5) },
|
||||
{ key: 'refundRate', value: 3.8, suffix: '%', chainRatio: ratio(-1.2, 6) },
|
||||
],
|
||||
tradeTrend: { amounts, avgAmounts, dates, orders },
|
||||
payMethod: [
|
||||
{ name: $t('payment.channel.common.wechat'), value: 38 },
|
||||
{ name: $t('payment.channel.common.alipay'), value: 28 },
|
||||
{ name: $t('payment.channel.common.unionPay'), value: 18 },
|
||||
{ name: $t('payment.channel.common.visa'), value: 8 },
|
||||
{ name: $t('payment.channel.common.mastercard'), value: 5 },
|
||||
{ name: $t('payment.channel.common.douyin'), value: 3 },
|
||||
],
|
||||
channelSuccess: [
|
||||
{ name: $t('payment.channel.common.alipay'), rate: 99.2 },
|
||||
{ name: $t('payment.channel.common.wechat'), rate: 98.7 },
|
||||
{ name: $t('payment.channel.common.unionPay'), rate: 97.5 },
|
||||
{ name: $t('payment.channel.common.douyin'), rate: 96.3 },
|
||||
{ name: $t('payment.channel.common.visa'), rate: 95.8 },
|
||||
{ name: $t('payment.channel.common.mastercard'), rate: 94.6 },
|
||||
],
|
||||
hourlyDist,
|
||||
amountRange: [
|
||||
{ count: 3200, range: '0-50' },
|
||||
{ count: 5800, range: '50-200' },
|
||||
{ count: 4200, range: '200-1000' },
|
||||
{ count: 1800, range: '1000-5000' },
|
||||
{ count: 600, range: '5000+' },
|
||||
],
|
||||
channelVolume: [
|
||||
{ name: $t('payment.channel.common.wechat'), value: 45_000 },
|
||||
{ name: $t('payment.channel.common.alipay'), value: 32_000 },
|
||||
{ name: $t('payment.channel.common.unionPay'), value: 12_000 },
|
||||
{ name: $t('payment.channel.common.visa'), value: 8000 },
|
||||
{ name: $t('payment.channel.common.mastercard'), value: 6000 },
|
||||
{ name: $t('payment.channel.common.douyin'), value: 3000 },
|
||||
],
|
||||
refundTrend: { amounts: refundTrendAmounts, dates },
|
||||
merchantRank: [
|
||||
{ amount: 520_000, merchantName: '云端科技有限公司', orders: 12_300, proportion: 18.5 },
|
||||
{ amount: 438_000, merchantName: '数聚信息科技', orders: 9800, proportion: 15.6 },
|
||||
{ amount: 362_000, merchantName: '极客电子商务', orders: 8600, proportion: 12.9 },
|
||||
{ amount: 298_000, merchantName: '智慧零售集团', orders: 7200, proportion: 10.6 },
|
||||
{ amount: 245_000, merchantName: '新橙科技', orders: 5900, proportion: 8.7 },
|
||||
{ amount: 198_000, merchantName: '环球贸易有限公司', orders: 4700, proportion: 7.1 },
|
||||
{ amount: 165_000, merchantName: '蓝海传媒', orders: 3900, proportion: 5.9 },
|
||||
{ amount: 132_000, merchantName: '星辰网络', orders: 3100, proportion: 4.7 },
|
||||
{ amount: 98_000, merchantName: '迅捷物流', orders: 2300, proportion: 3.5 },
|
||||
{ amount: 76_000, merchantName: '橙意生活', orders: 1800, proportion: 2.7 },
|
||||
],
|
||||
};
|
||||
/**
|
||||
* 环比百分比计算
|
||||
* @returns null 表示无法计算(prev 缺数据或为 0)
|
||||
*/
|
||||
function chainRatio(curr?: number, prev?: number): null | number {
|
||||
if (curr === undefined || prev === undefined || prev === 0) return null;
|
||||
return Math.round(((curr - prev) / prev) * 1000) / 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析页聚合数据异步加载 composable
|
||||
*
|
||||
* 按 dateRange 触发 8 个 API 并发加载(Promise.allSettled), 单个失败时该字段保留空,
|
||||
* 不影响其他图表。chainRatio(环比) 由前端基于后端返回的 curr/prev 字段计算。
|
||||
*/
|
||||
export function useAnalyticsData() {
|
||||
// 当前模式(预设 key 或 'custom' 自定义)
|
||||
const activePreset = ref<PresetKey>('last7days');
|
||||
// 自定义日期范围(仅 custom 模式生效)
|
||||
const customRange = ref<DateRange | undefined>(undefined);
|
||||
|
||||
// 是否自定义模式(控制 RangePicker 启用/禁用)
|
||||
const isCustom = computed(() => activePreset.value === 'custom');
|
||||
|
||||
// 选预设时清空自定义范围(避免残留)
|
||||
watch(activePreset, (key) => {
|
||||
if (key !== 'custom') customRange.value = undefined;
|
||||
});
|
||||
|
||||
// 真实数据源:custom 走自定义范围(未选时兜底近7天),否则走预设
|
||||
const dateRange = computed<DateRange>(() => {
|
||||
if (activePreset.value === 'custom') {
|
||||
return customRange.value ?? computePreset('last7days');
|
||||
@@ -154,15 +78,172 @@ export function useAnalyticsData() {
|
||||
return computePreset(activePreset.value);
|
||||
});
|
||||
|
||||
// 副标题:动态日期摘要(与右侧选择器联动)
|
||||
const subtitle = computed(() => {
|
||||
const [start, end] = dateRange.value;
|
||||
const days = dayjs(end).diff(dayjs(start), 'day') + 1;
|
||||
return $t('dashboard.analytics.subtitle', { days, end, start });
|
||||
});
|
||||
|
||||
// 按日期范围派生的全量数据
|
||||
const data = computed(() => buildData(dateRange.value[0], dateRange.value[1]));
|
||||
const data = ref<AnalyticsData>(emptyAnalyticsData());
|
||||
const loading = ref(false);
|
||||
|
||||
return { activePreset, customRange, data, isCustom, subtitle };
|
||||
/** 公共参数(start/end 同时传时优先区间模式) */
|
||||
function commonParams() {
|
||||
const [start, end] = dateRange.value;
|
||||
return { end, start };
|
||||
}
|
||||
|
||||
/** 并发加载所有维度, allSettled 保证单个失败不阻塞其他 */
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = commonParams();
|
||||
const results = await Promise.allSettled([
|
||||
DashboardTradeApi.overview(params),
|
||||
DashboardTradeApi.trend(params),
|
||||
DashboardTradeApi.refundTrend(params),
|
||||
DashboardTradeApi.providerDist(params),
|
||||
DashboardTradeApi.providerSuccess(params),
|
||||
DashboardTradeApi.hourlyDist(params),
|
||||
DashboardTradeApi.amountRange(params),
|
||||
DashboardTradeApi.merchantRank({ ...params, limit: 10 }),
|
||||
]);
|
||||
|
||||
// 失败的维度单独记录到控制台(开发期可见, 生产静默)
|
||||
const labels = ['overview', 'trend', 'refundTrend', 'providerDist', 'providerSuccess', 'hourlyDist', 'amountRange', 'merchantRank'] as const;
|
||||
results.forEach((r, i) => {
|
||||
if (r.status === 'rejected') console.warn(`[analytics] ${labels[i]} 加载失败:`, r.reason);
|
||||
});
|
||||
|
||||
const overviewRes = results[0].status === 'fulfilled' ? results[0].value?.data : undefined;
|
||||
const trendRes = results[1].status === 'fulfilled' ? results[1].value?.data : undefined;
|
||||
const refundTrendRes = results[2].status === 'fulfilled' ? results[2].value?.data : undefined;
|
||||
const providerDistRes = results[3].status === 'fulfilled' ? results[3].value?.data : undefined;
|
||||
const providerSuccessRes = results[4].status === 'fulfilled' ? results[4].value?.data : undefined;
|
||||
const hourlyDistRes = results[5].status === 'fulfilled' ? results[5].value?.data : undefined;
|
||||
const amountRangeRes = results[6].status === 'fulfilled' ? results[6].value?.data : undefined;
|
||||
const merchantRankRes = results[7].status === 'fulfilled' ? results[7].value?.data : undefined;
|
||||
|
||||
data.value = buildAnalyticsData({
|
||||
amountRangeRes,
|
||||
hourlyDistRes,
|
||||
merchantRankRes,
|
||||
overviewRes,
|
||||
providerDistRes,
|
||||
providerSuccessRes,
|
||||
refundTrendRes,
|
||||
trendRes,
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// dateRange 变化即触发加载
|
||||
watch(dateRange, load, { immediate: true });
|
||||
|
||||
return { activePreset, customRange, data, isCustom, loading, subtitle };
|
||||
}
|
||||
|
||||
/** 将各 API 响应组装为 AnalyticsData */
|
||||
function buildAnalyticsData(input: {
|
||||
amountRangeRes?: { bucket?: string; count?: number }[];
|
||||
hourlyDistRes?: { amount?: number; count?: number; hour?: number }[];
|
||||
merchantRankRes?: { amount?: number; merchantName?: string; orders?: number; proportion?: number }[];
|
||||
overviewRes?: {
|
||||
prevRefundAmount?: number;
|
||||
prevRefundCount?: number;
|
||||
prevSuccessAmount?: number;
|
||||
prevSuccessCount?: number;
|
||||
prevTotalOrders?: number;
|
||||
refundAmount?: number;
|
||||
refundCount?: number;
|
||||
successAmount?: number;
|
||||
successCount?: number;
|
||||
totalOrders?: number;
|
||||
};
|
||||
providerDistRes?: { amount?: number; count?: number; provider?: string }[];
|
||||
providerSuccessRes?: { provider?: string; rate?: number }[];
|
||||
refundTrendRes?: { amount?: number; count?: number; date?: string }[];
|
||||
trendRes?: { amount?: number; count?: number; date?: string }[];
|
||||
}): AnalyticsData {
|
||||
const { overviewRes, trendRes, refundTrendRes, providerDistRes, providerSuccessRes, hourlyDistRes, amountRangeRes, merchantRankRes } = input;
|
||||
|
||||
// ===== overview 6 卡片(含环比) =====
|
||||
const ov = overviewRes ?? {};
|
||||
const successAmountYuan = fenToYuan(ov.successAmount);
|
||||
const refundAmountYuan = fenToYuan(ov.refundAmount);
|
||||
const successCount = ov.successCount ?? 0;
|
||||
const totalOrders = ov.totalOrders ?? 0;
|
||||
const avgAmount = successCount > 0 ? Math.round((successAmountYuan / successCount) * 100) / 100 : 0;
|
||||
// 成功率口径: success_count / total_orders * 100(分子分母时间基准不同, 见后端 Mapper 注释)
|
||||
const successRate = totalOrders > 0 ? Math.round((successCount / totalOrders) * 1000) / 10 : 0;
|
||||
const refundRate = successAmountYuan > 0 ? Math.round((refundAmountYuan / successAmountYuan) * 1000) / 10 : 0;
|
||||
|
||||
const overview = [
|
||||
{ key: 'totalAmount', value: successAmountYuan, prefix: '¥', chainRatio: chainRatio(ov.successAmount, ov.prevSuccessAmount) },
|
||||
{ key: 'totalOrders', value: successCount, chainRatio: chainRatio(ov.successCount, ov.prevSuccessCount) },
|
||||
{ key: 'avgAmount', value: avgAmount, prefix: '¥', chainRatio: chainRatio(avgAmount, ov.prevSuccessAmount && ov.prevSuccessCount ? Math.round((ov.prevSuccessAmount / 100 / ov.prevSuccessCount) * 100) / 100 : undefined) },
|
||||
{ key: 'successRate', value: successRate, suffix: '%', chainRatio: chainRatio(ov.successCount && ov.totalOrders ? successRate : undefined, ov.prevSuccessCount && ov.prevTotalOrders ? Math.round((ov.prevSuccessCount / ov.prevTotalOrders) * 1000) / 10 : undefined) },
|
||||
{ key: 'refundAmount', value: refundAmountYuan, prefix: '¥', chainRatio: chainRatio(ov.refundAmount, ov.prevRefundAmount) },
|
||||
{ key: 'refundRate', value: refundRate, suffix: '%', chainRatio: null },
|
||||
];
|
||||
|
||||
// ===== tradeTrend(按日期 + 多度量) =====
|
||||
// dates 保留 yyyy-MM-dd 原值(与 a-tooltip / 前端期望一致), amounts/orders 取后端原值, avgAmounts 派生
|
||||
const trendList = trendRes ?? [];
|
||||
const tradeTrend = {
|
||||
amounts: trendList.map((i) => fenToYuan(i.amount)),
|
||||
avgAmounts: trendList.map((i) => {
|
||||
const yuan = fenToYuan(i.amount);
|
||||
const cnt = i.count ?? 0;
|
||||
return cnt > 0 ? Math.round((yuan / cnt) * 100) / 100 : 0;
|
||||
}),
|
||||
dates: trendList.map((i) => i.date ?? ''),
|
||||
orders: trendList.map((i) => i.count ?? 0),
|
||||
};
|
||||
|
||||
// ===== refundTrend(按日期) =====
|
||||
const refundTrend = {
|
||||
amounts: (refundTrendRes ?? []).map((i) => fenToYuan(i.amount)),
|
||||
dates: (refundTrendRes ?? []).map((i) => i.date ?? ''),
|
||||
};
|
||||
|
||||
// ===== payMethod(支付方式占比饼图, 用 provider amount) =====
|
||||
const payMethod = (providerDistRes ?? []).map((i) => ({
|
||||
name: providerLabel(i.provider),
|
||||
value: fenToYuan(i.amount),
|
||||
}));
|
||||
|
||||
// ===== channelVolume(渠道交易量柱状图, 用 provider count) =====
|
||||
const channelVolume = (providerDistRes ?? []).map((i) => ({
|
||||
name: providerLabel(i.provider),
|
||||
value: i.count ?? 0,
|
||||
}));
|
||||
|
||||
// ===== channelSuccess(渠道成功率) =====
|
||||
const channelSuccess = (providerSuccessRes ?? []).map((i) => ({
|
||||
name: providerLabel(i.provider),
|
||||
rate: i.rate ?? 0,
|
||||
}));
|
||||
|
||||
// ===== hourlyDist(24 小时时段, 取 count 数组) =====
|
||||
// 后端返回 0-23 已补齐, 取 count 字段(原前端约定: 柱状图值)
|
||||
const hourlyDist = (hourlyDistRes ?? []).map((i) => i.count ?? 0);
|
||||
|
||||
// ===== amountRange(金额区间分桶) =====
|
||||
const amountRange = (amountRangeRes ?? []).map((i) => ({
|
||||
count: i.count ?? 0,
|
||||
range: i.bucket ?? '',
|
||||
}));
|
||||
|
||||
// ===== merchantRank =====
|
||||
const merchantRank = (merchantRankRes ?? []).map((i) => ({
|
||||
amount: fenToYuan(i.amount),
|
||||
merchantName: i.merchantName ?? '-',
|
||||
orders: i.orders ?? 0,
|
||||
proportion: i.proportion ?? 0,
|
||||
}));
|
||||
|
||||
return { amountRange, channelSuccess, channelVolume, hourlyDist, merchantRank, overview, payMethod, refundTrend, tradeTrend };
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
defineOptions({ name: 'Analytics' });
|
||||
|
||||
// 全局时间范围(预设 + 自定义互斥)+ 派生数据(mock,后端聚合 API 就绪后替换)
|
||||
const { activePreset, customRange, data, isCustom, subtitle } = useAnalyticsData();
|
||||
// 全局时间范围(预设 + 自定义互斥)+ 派生数据(异步加载, 8 个维度并发)
|
||||
const { activePreset, customRange, data, isCustom, loading, subtitle } = useAnalyticsData();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -41,40 +41,40 @@
|
||||
</div>
|
||||
|
||||
<!-- 指标卡片行:规模(总额/笔数/客单价) + 质量(成功率/退款额/退款率) -->
|
||||
<AnalysisOverview :data="data.overview" />
|
||||
<AnalysisOverview :data="data.overview" :loading="loading" />
|
||||
|
||||
<!-- 图表网格:核心趋势 → 洞察三连 → 深度 → 明细 -->
|
||||
<a-row :gutter="[16, 16]" align="stretch" class="!mt-4">
|
||||
<!-- 第二行:交易趋势(多度量) + 支付方式占比 -->
|
||||
<a-col :span="16">
|
||||
<AnalysisTradeTrend :data="data.tradeTrend" />
|
||||
<AnalysisTradeTrend :data="data.tradeTrend" :loading="loading" />
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<AnalysisPayMethod :data="data.payMethod" />
|
||||
<AnalysisPayMethod :data="data.payMethod" :loading="loading" />
|
||||
</a-col>
|
||||
|
||||
<!-- 第三行:洞察三连(渠道质量/时段/金额区间) -->
|
||||
<a-col :span="8">
|
||||
<AnalysisChannelSuccess :data="data.channelSuccess" />
|
||||
<AnalysisChannelSuccess :data="data.channelSuccess" :loading="loading" />
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<AnalysisHourlyDist :data="data.hourlyDist" />
|
||||
<AnalysisHourlyDist :data="data.hourlyDist" :loading="loading" />
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<AnalysisAmountRange :data="data.amountRange" />
|
||||
<AnalysisAmountRange :data="data.amountRange" :loading="loading" />
|
||||
</a-col>
|
||||
|
||||
<!-- 第四行:渠道交易量 + 退款趋势 -->
|
||||
<a-col :span="12">
|
||||
<AnalysisChannelVolume :data="data.channelVolume" />
|
||||
<AnalysisChannelVolume :data="data.channelVolume" :loading="loading" />
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<AnalysisRefundTrend :data="data.refundTrend" />
|
||||
<AnalysisRefundTrend :data="data.refundTrend" :loading="loading" />
|
||||
</a-col>
|
||||
|
||||
<!-- 第五行:商户交易额排名明细 -->
|
||||
<a-col :span="24">
|
||||
<AnalysisMerchantRank :data="data.merchantRank" />
|
||||
<AnalysisMerchantRank :data="data.merchantRank" :loading="loading" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
@@ -17,8 +17,8 @@ export interface OverviewStat {
|
||||
prefix?: string;
|
||||
/** 数值后缀(如 %) */
|
||||
suffix?: string;
|
||||
/** 环比百分比(正数上升、负数下降) */
|
||||
chainRatio: number;
|
||||
/** 环比百分比(正数上升、负数下降); null 表示无上期数据, 不展示 */
|
||||
chainRatio: null | number;
|
||||
}
|
||||
|
||||
/** 折线图序列数据(日期 + 多度量) */
|
||||
@@ -59,7 +59,7 @@ export interface MerchantRankItem {
|
||||
proportion: number;
|
||||
}
|
||||
|
||||
/** 分析页聚合数据(由 useAnalyticsData 按 timeRange 派生) */
|
||||
/** 分析页聚合数据(由 useAnalyticsData 按 timeRange 异步加载组装) */
|
||||
export interface AnalyticsData {
|
||||
overview: OverviewStat[];
|
||||
tradeTrend: TrendSeries;
|
||||
@@ -71,3 +71,18 @@ export interface AnalyticsData {
|
||||
refundTrend: { amounts: number[]; dates: string[] };
|
||||
merchantRank: MerchantRankItem[];
|
||||
}
|
||||
|
||||
/** 空数据(初次加载前的占位, 所有图表显示空状态) */
|
||||
export function emptyAnalyticsData(): AnalyticsData {
|
||||
return {
|
||||
overview: [],
|
||||
tradeTrend: { amounts: [], avgAmounts: [], dates: [], orders: [] },
|
||||
payMethod: [],
|
||||
channelSuccess: [],
|
||||
hourlyDist: [],
|
||||
amountRange: [],
|
||||
channelVolume: [],
|
||||
refundTrend: { amounts: [], dates: [] },
|
||||
merchantRank: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { LayoutItem, WidgetMeta } from './types';
|
||||
|
||||
import ChannelDistributionWidget from './widgets/channel-distribution.widget.vue';
|
||||
import NoticeListWidget from './widgets/notice-list.widget.vue';
|
||||
import PayOrderListWidget from './widgets/pay-order-list.widget.vue';
|
||||
import ProviderDistributionWidget from './widgets/provider-distribution.widget.vue';
|
||||
import QuickEntryWidget from './widgets/quick-entry.widget.vue';
|
||||
import TradeOverviewWidget from './widgets/trade-overview.widget.vue';
|
||||
import TradeTrendWidget from './widgets/trade-trend.widget.vue';
|
||||
@@ -69,9 +69,9 @@ export const WIDGET_REGISTRY: WidgetMeta[] = [
|
||||
defaultOrder: 6,
|
||||
},
|
||||
{
|
||||
id: 'channel-distribution',
|
||||
component: ChannelDistributionWidget,
|
||||
titleKey: 'dashboard.workspace.widget.channelDist',
|
||||
id: 'provider-distribution',
|
||||
component: ProviderDistributionWidget,
|
||||
titleKey: 'dashboard.workspace.widget.providerDist',
|
||||
icon: 'lucide:pie-chart',
|
||||
defaultSpan: 8,
|
||||
defaultOrder: 5,
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DashboardData } from '../types';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
interface Props {
|
||||
/** 工作台聚合数据(支付方式分布暂无后端 API,保留以统一 widget props 契约) */
|
||||
data?: DashboardData;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'ChannelDistributionWidget' });
|
||||
|
||||
// 支付方式分布暂无后端 API,保留 data prop 以统一 widget 渲染契约
|
||||
withDefaults(defineProps<Props>(), {
|
||||
data: undefined,
|
||||
});
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
// Mock 数据:各支付方式占比(百分比)
|
||||
// 后端支付方式分布 API 就绪后替换为真实请求结果
|
||||
renderEcharts({
|
||||
legend: { bottom: 0, orient: 'horizontal' },
|
||||
series: [
|
||||
{
|
||||
label: { formatter: '{d}%', show: true },
|
||||
radius: ['45%', '70%'],
|
||||
type: 'pie',
|
||||
data: [
|
||||
{ name: $t('payment.channel.common.wechat'), value: 38 },
|
||||
{ name: $t('payment.channel.common.alipay'), value: 28 },
|
||||
{ name: $t('payment.channel.common.unionPay'), value: 18 },
|
||||
{ name: $t('payment.channel.common.visa'), value: 8 },
|
||||
{ name: $t('payment.channel.common.mastercard'), value: 5 },
|
||||
{ name: $t('payment.channel.common.douyin'), value: 3 },
|
||||
],
|
||||
},
|
||||
],
|
||||
tooltip: { trigger: 'item' },
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-card variant="borderless" class="!h-full min-h-[260px] !bg-card">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ $t('dashboard.workspace.widget.channelDist') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<EchartsUI ref="chartRef" class="h-[280px]" />
|
||||
</a-card>
|
||||
</template>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DashboardData } from '../types';
|
||||
|
||||
import { nextTick, onMounted, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import { DashboardTradeApi, type ProviderDistItemResult } from '#/api/payment/dashboard/trade-dashboard.api';
|
||||
import { providerI18nMap } from '#/enums/payment/providerEnum';
|
||||
|
||||
interface Props {
|
||||
/** 工作台聚合数据(支付渠道分布独立拉数据,保留以统一 widget props 契约) */
|
||||
data?: DashboardData;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'ProviderDistributionWidget' });
|
||||
|
||||
// 支付渠道分布独立拉取数据,不消费聚合统计;保留 data prop 以统一 widget 渲染契约
|
||||
withDefaults(defineProps<Props>(), {
|
||||
data: undefined,
|
||||
});
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
const loading = ref(false);
|
||||
const distData = ref<ProviderDistItemResult[]>([]);
|
||||
|
||||
/** 支付渠道编码 → 展示名: 优先 i18n 映射, 无映射时降级原编码 */
|
||||
function providerLabel(provider?: string): string {
|
||||
if (!provider) return '-';
|
||||
const i18nKey = providerI18nMap[provider];
|
||||
if (i18nKey) {
|
||||
const text = $t(i18nKey);
|
||||
if (text && text !== i18nKey) return text;
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
/** 拉取近 30 天支付渠道分布 */
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await DashboardTradeApi.providerDist({ days: 30 });
|
||||
distData.value = res?.data || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
await nextTick();
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
/** 渲染饼图 */
|
||||
function render(): void {
|
||||
const data = distData.value.map((i) => ({
|
||||
name: providerLabel(i.provider),
|
||||
// 金额分转元(仅影响 tooltip 展示, 占比按 value 相对计算)
|
||||
value: Math.round((i.amount ?? 0) / 100),
|
||||
}));
|
||||
renderEcharts({
|
||||
legend: { bottom: 0, orient: 'horizontal' },
|
||||
series: [
|
||||
{
|
||||
label: { formatter: '{d}%', show: true },
|
||||
radius: ['45%', '70%'],
|
||||
type: 'pie',
|
||||
data,
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
valueFormatter: (val: any) => `¥${Number(val).toLocaleString('en-US')}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-card variant="borderless" class="!h-full min-h-[260px] !bg-card">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ $t('dashboard.workspace.widget.providerDist') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-skeleton v-if="loading && distData.length === 0" active :paragraph="{ rows: 5 }" />
|
||||
<EchartsUI v-else ref="chartRef" class="h-[280px]" />
|
||||
</a-card>
|
||||
</template>
|
||||
@@ -1,19 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DashboardData } from '../types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { DashboardTradeApi, type TradeOverviewResult } from '#/api/payment/dashboard/trade-dashboard.api';
|
||||
|
||||
interface Props {
|
||||
/** 工作台聚合数据(交易概览暂无后端 API,保留以统一 widget props 契约) */
|
||||
/** 工作台聚合数据(交易概览独立拉数据,保留以统一 widget props 契约) */
|
||||
data?: DashboardData;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'TradeOverviewWidget' });
|
||||
|
||||
// 交易概览暂无后端 API,保留 data prop 以统一 widget 渲染契约
|
||||
// 交易概览独立拉取数据,不消费聚合统计;保留 data prop 以统一 widget 渲染契约
|
||||
withDefaults(defineProps<Props>(), {
|
||||
data: undefined,
|
||||
});
|
||||
@@ -21,25 +23,43 @@
|
||||
// 今日 / 昨日 tab 切换
|
||||
const activeTab = ref<'today' | 'yesterday'>('today');
|
||||
|
||||
// Mock 数据:后端交易聚合 API 就绪后替换为真实数据
|
||||
// 成交金额(元)/ 成交笔数 / 退款金额(元)/ 退款笔数
|
||||
const mockData = {
|
||||
today: {
|
||||
successAmount: '128,560.50',
|
||||
refundAmount: '1,280.00',
|
||||
refundCount: 8,
|
||||
successCount: 326,
|
||||
},
|
||||
yesterday: {
|
||||
successAmount: '112,380.20',
|
||||
refundAmount: '980.50',
|
||||
refundCount: 6,
|
||||
successCount: 289,
|
||||
},
|
||||
} as const;
|
||||
const loading = ref(false);
|
||||
const overview = ref<TradeOverviewResult>({});
|
||||
|
||||
// 当前展示数据(随 tab 切换)
|
||||
const overview = computed(() => mockData[activeTab.value]);
|
||||
/** 拉取指定日期的概览统计 */
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await DashboardTradeApi.overview({ date: activeTab.value });
|
||||
overview.value = res?.data || {};
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// tab 切换时重新拉取
|
||||
watch(activeTab, load);
|
||||
onMounted(load);
|
||||
|
||||
/** 金额分转元(千分位 + 2 位小数), null/undefined 显示 '-' */
|
||||
function formatAmount(fen?: number): string {
|
||||
if (fen === null || fen === undefined) return '-';
|
||||
return (fen / 100).toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
/** 数值千分位格式化 */
|
||||
function formatCount(count?: number): string {
|
||||
if (count === null || count === undefined) return '-';
|
||||
return count.toLocaleString('en-US');
|
||||
}
|
||||
|
||||
const successAmount = computed(() => formatAmount(overview.value.successAmount));
|
||||
const successCount = computed(() => formatCount(overview.value.successCount));
|
||||
const refundAmount = computed(() => formatAmount(overview.value.refundAmount));
|
||||
const refundCount = computed(() => formatCount(overview.value.refundCount));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -57,12 +77,13 @@
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<a-skeleton v-if="loading" active :paragraph="{ rows: 3 }" />
|
||||
<!-- 主指标 + 副指标:垂直分布,主指标贴顶、副指标贴底,等高无空白 -->
|
||||
<div class="flex h-full flex-col justify-between">
|
||||
<div v-else class="flex h-full flex-col justify-between">
|
||||
<div>
|
||||
<p class="text-foreground/60 text-sm">{{ $t('dashboard.workspace.tradeOverview.successAmount') }}</p>
|
||||
<p class="text-primary mt-1 text-[40px] font-semibold leading-none tabular-nums">
|
||||
{{ overview.successAmount }}
|
||||
{{ successAmount }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -70,15 +91,15 @@
|
||||
<div class="mt-8 flex justify-between">
|
||||
<div>
|
||||
<p class="text-foreground/50 text-xs">{{ $t('dashboard.workspace.tradeOverview.successCount') }}</p>
|
||||
<span class="text-foreground mt-1 block text-lg font-semibold tabular-nums">{{ overview.successCount }}</span>
|
||||
<span class="text-foreground mt-1 block text-lg font-semibold tabular-nums">{{ successCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-foreground/50 text-xs">{{ $t('dashboard.workspace.tradeOverview.refundAmount') }}</p>
|
||||
<span class="text-foreground mt-1 block text-lg font-semibold tabular-nums">{{ overview.refundAmount }}</span>
|
||||
<span class="text-foreground mt-1 block text-lg font-semibold tabular-nums">{{ refundAmount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-foreground/50 text-xs">{{ $t('dashboard.workspace.tradeOverview.refundCount') }}</p>
|
||||
<span class="text-foreground mt-1 block text-lg font-semibold tabular-nums">{{ overview.refundCount }}</span>
|
||||
<span class="text-foreground mt-1 block text-lg font-semibold tabular-nums">{{ refundCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DashboardData } from '../types';
|
||||
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import { DashboardTradeApi, type TradeTrendItemResult } from '#/api/payment/dashboard/trade-dashboard.api';
|
||||
|
||||
interface Props {
|
||||
/** 工作台聚合数据(交易趋势暂无后端 API,保留以统一 widget props 契约) */
|
||||
/** 工作台聚合数据(交易趋势独立拉数据,保留以统一 widget props 契约) */
|
||||
data?: DashboardData;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'TradeTrendWidget' });
|
||||
|
||||
// 交易趋势暂无后端 API,保留 data prop 以统一 widget 渲染契约
|
||||
// 交易趋势独立拉取数据,不消费聚合统计;保留 data prop 以统一 widget 渲染契约
|
||||
withDefaults(defineProps<Props>(), {
|
||||
data: undefined,
|
||||
});
|
||||
@@ -25,38 +27,40 @@
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
// Mock 数据:近 7 天 / 近 30 天交易额(元)
|
||||
// 后端交易聚合 API 就绪后替换为真实请求结果
|
||||
const mockSeries: Record<TrendRange, number[]> = {
|
||||
'7days': [8200, 9320, 9010, 9340, 12_900, 13_300, 13_200],
|
||||
'30days': [
|
||||
5200, 6100, 5800, 6700, 7200, 6900, 7800, 8200, 7600, 8400, 9100, 8800, 9600, 10_200, 9800, 10_800, 11_500,
|
||||
11_200, 12_300, 12_800, 11_900, 13_200, 12_900, 13_800, 14_500, 14_200, 15_300, 15_800, 14_900, 16_200,
|
||||
],
|
||||
};
|
||||
const loading = ref(false);
|
||||
const trendData = ref<TradeTrendItemResult[]>([]);
|
||||
|
||||
// 近 7 天 x 轴标签(周一~周日)
|
||||
const weekLabels: string[] = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
// 近 30 天 x 轴标签(MM-DD,从今天倒推)
|
||||
const monthLabels: string[] = Array.from({ length: 30 }).map((_, i) => {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - (29 - i));
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
return `${mm}-${dd}`;
|
||||
});
|
||||
/** 拉取指定天数的交易趋势 */
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const days = activeRange.value === '7days' ? 7 : 30;
|
||||
const res = await DashboardTradeApi.trend({ days });
|
||||
trendData.value = res?.data || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
// 首次从骨架屏切到 EchartsUI 需等 DOM 挂载后再渲染
|
||||
await nextTick();
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
/** 按当前时间跨度渲染折线图 */
|
||||
/** 按当前数据渲染折线图 */
|
||||
function render(): void {
|
||||
const isWeek = activeRange.value === '7days';
|
||||
// 日期截取 MM-DD 作为 x 轴; 金额分转元
|
||||
const labels = trendData.value.map((i) => i.date?.slice(5) || '');
|
||||
const amounts = trendData.value.map((i) => Math.round((i.amount ?? 0) / 100));
|
||||
renderEcharts({
|
||||
grid: { bottom: '8%', containLabel: true, left: '3%', right: '4%', top: '10%' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
valueFormatter: (val: any) => `¥${Number(val).toLocaleString('en-US')}`,
|
||||
},
|
||||
xAxis: {
|
||||
axisLabel: { fontSize: 11 },
|
||||
boundaryGap: false,
|
||||
type: 'category',
|
||||
data: isWeek ? weekLabels : monthLabels,
|
||||
data: labels,
|
||||
},
|
||||
yAxis: {
|
||||
axisLabel: { fontSize: 11 },
|
||||
@@ -65,7 +69,7 @@
|
||||
series: [
|
||||
{
|
||||
areaStyle: { opacity: 0.15 },
|
||||
data: mockSeries[activeRange.value],
|
||||
data: amounts,
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
@@ -73,10 +77,10 @@
|
||||
});
|
||||
}
|
||||
|
||||
// tab 切换时重渲染
|
||||
watch(activeRange, render);
|
||||
// tab 切换时重新拉取
|
||||
watch(activeRange, load);
|
||||
|
||||
onMounted(render);
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -93,6 +97,7 @@
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<EchartsUI ref="chartRef" class="h-[280px]" />
|
||||
<a-skeleton v-if="loading && trendData.length === 0" active :paragraph="{ rows: 5 }" />
|
||||
<EchartsUI v-else ref="chartRef" class="h-[280px]" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user