feat(order): 新增网关支付订单页,重构退款订单列表与工作台订单卡片,补齐东盟四语

This commit is contained in:
DaxPay Dev
2026-07-19 11:55:41 +08:00
parent ae07f8a533
commit 9659d9d3c4
12 changed files with 1042 additions and 277 deletions

View File

@@ -0,0 +1,111 @@
import type { MchEntity, PageResult, Result } from '#/types/web';
import { defHttp } from '#/api/request';
/**
* 网关支付业务订单管理 API
*/
export const GatewayOrderApi = {
/**
* 分页查询网关业务订单
*/
page(params: GatewayOrderQuery & { current?: number; size?: number }): Promise<Result<PageResult<GatewayOrderResult>>> {
return defHttp.get({ url: '/admin/order/gateway-pay/page', params });
},
/**
* 根据ID查询详情
*/
getById(id: string): Promise<Result<GatewayOrderResult>> {
return defHttp.get({ url: '/admin/order/gateway-pay/get-by-id', params: { id } });
},
/**
* 同步支付状态
*/
sync(id: string): Promise<Result<GatewayPaySyncResult>> {
return defHttp.post({ url: '/admin/order/gateway-pay/sync', params: { id } });
},
/**
* 关闭/撤销订单
*/
close(id: string, useCancel = false): Promise<Result<void>> {
return defHttp.post({
url: '/admin/order/gateway-pay/close',
params: { id, useCancel },
});
},
};
/** 网关订单查询参数 */
export interface GatewayOrderQuery {
mchNo?: string;
appId?: string;
/** 平台网关单号 */
orderNo?: string;
bizOrderNo?: string;
title?: string;
status?: string;
gatewayType?: string;
product?: string;
capability?: string;
storeNo?: string;
createTimeStart?: string;
createTimeEnd?: string;
amountMin?: number;
amountMax?: number;
}
/** 网关订单结果 */
export interface GatewayOrderResult extends MchEntity {
orderNo?: string;
bizOrderNo?: string;
gatewayType?: string;
source?: string;
title?: string;
description?: string;
status?: string;
notifyUrl?: string;
returnUrl?: string;
attach?: string;
expiredTime?: string;
amount?: number;
currency?: string;
channel?: string;
method?: string;
product?: string;
limitPay?: string;
clientEnv?: string;
device?: string;
payTime?: string;
closeTime?: string;
channelMchNo?: string;
capability?: string;
channelAppId?: string;
clientIp?: string;
storeNo?: string;
extraParam?: string;
// 资金凭证(详情)
tradeNo?: string;
outOrderNo?: string;
fundStatus?: string;
refundableBalance?: number;
payBody?: string;
payBodyType?: string;
buyerId?: string;
openid?: string;
provider?: string;
tradeProduct?: string;
tradeWay?: string;
bankType?: string;
promotionType?: string;
transOrderNo?: string;
relationOrderNo?: string;
errorMsg?: string;
}
export interface GatewayPaySyncResult {
orderStatus?: string;
adjust?: boolean;
}

View File

@@ -45,18 +45,18 @@ export interface NormalOrderQuery {
mchNo?: string;
/** 应用号 */
appId?: string;
/** 平台业务单号 */
orderNo?: string;
/** 商户业务单号 */
bizOrderNo?: string;
/** 订单标题 */
title?: string;
/** 业务状态 */
status?: string;
/** 支付通道 */
channel?: string;
/** 支付方式 */
method?: string;
/** 支付产品 */
product?: string;
/** 支付能力 */
capability?: string;
/** 门店号 */
storeNo?: string;
/** 创建时间-开始 */
@@ -71,8 +71,12 @@ export interface NormalOrderQuery {
/** 业务订单结果(含详情联表字段) */
export interface NormalOrderResult extends MchEntity {
/** 平台业务单号 */
orderNo?: string;
/** 商户业务单号 */
bizOrderNo?: string;
/** 订单来源 */
source?: string;
/** 标题 */
title?: string;
/** 描述 */
@@ -97,14 +101,14 @@ export interface NormalOrderResult extends MchEntity {
method?: string;
/** 支付产品 */
product?: string;
/** 支付能力 */
capability?: string;
/** 支付成功时间 */
payTime?: string;
/** 关闭时间 */
closeTime?: string;
/** 通道商户号 */
channelMchNo?: string;
/** 支付能力编码 */
capability?: string;
/** 通道应用 AppId */
channelAppId?: string;
/** 客户端IP */

View File

@@ -53,12 +53,7 @@ export interface PayTradeQuery {
status?: string;
/** 交易形态 */
tradeType?: string;
/** 支付通道 */
channel?: string;
/** 支付方式 */
method?: string;
/** 支付产品 */
product?: string;
// pay_trade 无 channel/method/product 列,不可筛选
/** 通道商户号 */
channelMchNo?: string;
/** 门店号 */
@@ -142,6 +137,8 @@ export interface PayTradeResult extends MchEntity {
/** 错误信息 */
errorMsg?: string;
// ===== 容器联表字段(详情) =====
/** 容器平台业务单号 */
containerOrderNo?: string;
/** 商户业务单号 */
bizOrderNo?: string;
/** 订单标题 */

View File

@@ -23,7 +23,7 @@ export const RefundOrderApi = {
/**
* 发起退款
*/
refund(data: PayRefundParam): Promise<Result<RefundOrderResult>> {
refund(data: RefundParam): Promise<Result<RefundOrderResult>> {
return defHttp.post({ url: '/admin/order/refund/refund', data });
},
@@ -37,78 +37,57 @@ export const RefundOrderApi = {
/** 退款订单查询参数 */
export interface RefundOrderQuery {
/** 商户号 */
mchNo?: string;
/** 应用号 */
appId?: string;
/** 退款号 */
refundNo?: string;
/** 商户退款号 */
bizRefundNo?: string;
/** 原支付订单号 */
orderNo?: string;
/** 商户业务订单号 */
/** 原支付资金交易号 */
tradeNo?: string;
/** 交易类型 normal/gateway */
tradeType?: string;
bizOrderNo?: string;
/** 退款状态 */
status?: string;
/** 支付通道 */
channel?: string;
/** 门店号 */
/** 支付产品 */
product?: string;
storeNo?: string;
/** 创建时间-开始 */
createTimeStart?: string;
/** 创建时间-结束 */
createTimeEnd?: string;
}
/** 退款发起参数 */
export interface PayRefundParam {
/** 原支付订单号 */
orderNo?: string;
/** 商户业务订单号 */
export interface RefundParam {
/** 原支付资金交易号 */
tradeNo?: string;
bizOrderNo?: string;
/** 退款金额(分) */
amount: number;
/** 退款原因 */
reason?: string;
/** 商户退款号(可选) */
bizRefundNo?: string;
}
/** 退款订单结果 */
/** 退款订单结果(管理端不暴露 capability */
export interface RefundOrderResult extends MchEntity {
/** 退款号 */
refundNo?: string;
/** 商户退款号 */
bizRefundNo?: string;
/** 标题 */
title?: string;
/** 原支付订单号 */
orderNo?: string;
/** 商户业务订单号 */
tradeNo?: string;
tradeType?: string;
bizOrderNo?: string;
/** 通道支付订单号 */
outOrderNo?: string;
/** 通道退款流水号 */
outRefundNo?: string;
/** 退款金额(分) */
relationOrderNo?: string;
amount?: number;
/** 订单总金额(分) */
orderAmount?: number;
/** 退款原因 */
currency?: string;
reason?: string;
/** 退款状态 */
status?: string;
/** 退款完成时间 */
finishTime?: string;
/** 支付通道 */
channel?: string;
/** 支付产品 */
product?: string;
/** 支付方式 */
method?: string;
/** 门店号 */
channelMchNo?: string;
channelAppId?: string;
notifyUrl?: string;
attach?: string;
clientIp?: string;
storeNo?: string;
/** 错误信息 */
errorMsg?: string;
}

View File

@@ -1,7 +1,11 @@
{
"normal": {
"title": "Perintah Pembayaran",
"detail": "Detail Pesanan Pembayaran"
"title": "Pembayaran Normal",
"detail": "Detail Pembayaran Normal"
},
"gateway": {
"title": "Pembayaran Gateway",
"detail": "Detail Pembayaran Gateway"
},
"trade": {
"title": "Perdagangan Dana",
@@ -10,24 +14,30 @@
"refund": {
"title": "Pengembalian Dana Pesanan",
"detail": "Detail Pengembalian Dana Pesanan",
"statusLabel": "Refund Status",
"section": {
"identity": "Identity",
"fund": "Fund",
"amount": "Amount",
"channel": "Channel",
"notify": "Notify"
},
"status": {
"init": "Awal",
"progress": "Pengembalian dana",
"success": "Kesuksesan",
"fail": "Gagal",
"close": "Tertutup"
"progress": "Refunding",
"success": "Success",
"fail": "Failed",
"close": "Closed"
},
"statusColor": {
"init": "bawaan",
"progress": "pengolahan",
"success": "hijau",
"fail": "merah",
"close": "bawaan"
"progress": "processing",
"success": "green",
"fail": "red",
"close": "default"
}
},
"field": {
"bizOrderNo": "Nomor Pesanan Pedagang.",
"tradeNo": "Nomor Perdagangan.",
"tradeNo": "Fund Trade No.",
"title": "Judul",
"description": "Keterangan",
"amount": "Jumlah (Yuan)",
@@ -41,7 +51,7 @@
"provider": "Penyedia",
"outOrderNo": "Nomor Pesanan Saluran.",
"transOrderNo": "Nomor Pesanan Trans.",
"relationOrderNo": "Nomor Pesanan Relasi.",
"relationOrderNo": "Channel Relation No.",
"postedAmount": "Jumlah dibukukan (Yuan)",
"refundableBalance": "Dapat dikembalikan (Yuan)",
"payBody": "Badan Bayar",
@@ -77,7 +87,7 @@
"reason": "Alasan Pengembalian Dana",
"finishTime": "Waktu Selesai",
"orderAmount": "Jumlah Pesanan",
"orderNo": "Nomor Pesanan Asli.",
"orderNo": "Platform Order No.",
"authCode": "Kode pembayaran",
"merchant": "Merchant"
},
@@ -85,27 +95,33 @@
"wait_pay": "Menunggu",
"paid": "Dibayar",
"closed": "Tertutup",
"expired": "Kedaluwarsa"
"expired": "Kedaluwarsa",
"paying": "Paying",
"failed": "Failed"
},
"fundStatus": {
"init": "Awal",
"processing": "Pengolahan",
"success": "Kesuksesan",
"fail": "Gagal",
"close": "Tertutup"
"close": "Tertutup",
"cancel": "Cancelled"
},
"bizStatusColor": {
"wait_pay": "oranye",
"paid": "hijau",
"closed": "bawaan",
"expired": "merah"
"expired": "merah",
"paying": "processing",
"failed": "red"
},
"fundStatusColor": {
"init": "bawaan",
"processing": "pengolahan",
"success": "hijau",
"fail": "merah",
"close": "bawaan"
"close": "bawaan",
"cancel": "default"
},
"placeholder": {
"bizOrderNo": "Masukkan nomor pesanan pedagang.",
@@ -143,9 +159,6 @@
"refundReasonPlaceholder": "Masukkan alasan pengembalian dana (opsional)",
"refundAmountExceed": "Jumlah pengembalian dana tidak dapat melebihi jumlah yang dapat dikembalikan"
},
"tradeType": {
"normal": "Pembayaran Biasa"
},
"method": {
"jsapi": "JSAPI",
"qrcode": "Kode QR",
@@ -153,5 +166,13 @@
"app": "APP",
"barcode": "Kode Batang",
"wap": "WAP"
},
"tradeType": {
"normal": "Normal Pay",
"gateway": "Gateway Pay"
},
"gatewayType": {
"cashier": "Cashier",
"aggregate": "Aggregate"
}
}
}

View File

@@ -1,7 +1,11 @@
{
"normal": {
"title": "Pesanan Pembayaran",
"detail": "Butiran Pesanan Pembayaran"
"title": "Bayaran Biasa",
"detail": "Butiran Bayaran Biasa"
},
"gateway": {
"title": "Bayaran Gerbang",
"detail": "Butiran Bayaran Gerbang"
},
"trade": {
"title": "Dagangan Dana",
@@ -10,24 +14,30 @@
"refund": {
"title": "Bayaran Balik Pesanan",
"detail": "Butiran Pesanan Bayaran Balik",
"statusLabel": "Refund Status",
"section": {
"identity": "Identity",
"fund": "Fund",
"amount": "Amount",
"channel": "Channel",
"notify": "Notify"
},
"status": {
"init": "Permulaan",
"progress": "Membayar balik",
"success": "Kejayaan",
"fail": "gagal",
"close": "tertutup"
"progress": "Refunding",
"success": "Success",
"fail": "Failed",
"close": "Closed"
},
"statusColor": {
"init": "lalai",
"progress": "pemprosesan",
"success": "hijau",
"fail": "merah",
"close": "lalai"
"progress": "processing",
"success": "green",
"fail": "red",
"close": "default"
}
},
"field": {
"bizOrderNo": "Pesanan Saudagar No.",
"tradeNo": "Perdagangan No.",
"tradeNo": "Fund Trade No.",
"title": "Tajuk",
"description": "Penerangan",
"amount": "Amaun(Yuan)",
@@ -41,7 +51,7 @@
"provider": "Pembekal",
"outOrderNo": "Pesanan Saluran No.",
"transOrderNo": "Pesanan Trans",
"relationOrderNo": "Perintah Perhubungan No.",
"relationOrderNo": "Channel Relation No.",
"postedAmount": "Amaun diakaun(Yuan)",
"refundableBalance": "Boleh dipulangkan(Yuan)",
"payBody": "Badan Bayar",
@@ -77,7 +87,7 @@
"reason": "Sebab Bayaran Balik",
"finishTime": "Tamat Masa",
"orderAmount": "Jumlah Pesanan",
"orderNo": "Pesanan Asal No.",
"orderNo": "Platform Order No.",
"authCode": "Kod pembayaran",
"merchant": "Peniaga"
},
@@ -85,27 +95,33 @@
"wait_pay": "Menunggu",
"paid": "Dibayar",
"closed": "tertutup",
"expired": "Tamat tempoh"
"expired": "Tamat tempoh",
"paying": "Paying",
"failed": "Failed"
},
"fundStatus": {
"init": "Permulaan",
"processing": "Memproses",
"success": "Kejayaan",
"fail": "gagal",
"close": "tertutup"
"close": "tertutup",
"cancel": "Cancelled"
},
"bizStatusColor": {
"wait_pay": "oren",
"paid": "hijau",
"closed": "lalai",
"expired": "merah"
"expired": "merah",
"paying": "processing",
"failed": "red"
},
"fundStatusColor": {
"init": "lalai",
"processing": "pemprosesan",
"success": "hijau",
"fail": "merah",
"close": "lalai"
"close": "lalai",
"cancel": "default"
},
"placeholder": {
"bizOrderNo": "Masukkan no pesanan peniaga.",
@@ -143,9 +159,6 @@
"refundReasonPlaceholder": "Masukkan sebab bayaran balik (pilihan)",
"refundAmountExceed": "Jumlah bayaran balik tidak boleh melebihi jumlah yang boleh dikembalikan"
},
"tradeType": {
"normal": "Bayaran Biasa"
},
"method": {
"jsapi": "JSAPI",
"qrcode": "Kod QR",
@@ -153,5 +166,13 @@
"app": "APP",
"barcode": "Kod Bar",
"wap": "WAP"
},
"tradeType": {
"normal": "Normal Pay",
"gateway": "Gateway Pay"
},
"gatewayType": {
"cashier": "Cashier",
"aggregate": "Aggregate"
}
}
}

View File

@@ -1,7 +1,11 @@
{
"normal": {
"title": "คำสั่งการชำระเงิน",
"detail": "รายละเอียดการสั่งซื้อการชำระเงิน"
"title": "ชำระเงินปกติ",
"detail": "รายละเอียดชำระเงินปกติ"
},
"gateway": {
"title": "ชำระเงินเกตเวย์",
"detail": "รายละเอียดชำระเงินเกตเวย์"
},
"trade": {
"title": "การซื้อขายกองทุน",
@@ -10,24 +14,30 @@
"refund": {
"title": "การคืนเงินคำสั่งซื้อ",
"detail": "รายละเอียดการขอคืนเงิน",
"statusLabel": "Refund Status",
"section": {
"identity": "Identity",
"fund": "Fund",
"amount": "Amount",
"channel": "Channel",
"notify": "Notify"
},
"status": {
"init": "อักษรย่อ",
"progress": "การคืนเงิน",
"success": "ความสำเร็จ",
"fail": "ล้มเหลว",
"close": "ปิด"
"progress": "Refunding",
"success": "Success",
"fail": "Failed",
"close": "Closed"
},
"statusColor": {
"init": "ค่าเริ่มต้น",
"progress": "กำลังประมวลผล",
"success": "สีเขียว",
"fail": "สีแดง",
"close": "ค่าเริ่มต้น"
"progress": "processing",
"success": "green",
"fail": "red",
"close": "default"
}
},
"field": {
"bizOrderNo": "เลขที่ใบสั่งซื้อของผู้ขาย",
"tradeNo": "หมายเลขการค้า",
"tradeNo": "Fund Trade No.",
"title": "ชื่อ",
"description": "คำอธิบาย",
"amount": "จำนวนเงิน(หยวน)",
@@ -41,7 +51,7 @@
"provider": "ผู้ให้บริการ",
"outOrderNo": "ช่องทางการสั่งซื้อเลขที่",
"transOrderNo": "หมายเลขคำสั่งทรานส์",
"relationOrderNo": "ความสัมพันธ์เลขที่คำสั่ง",
"relationOrderNo": "Channel Relation No.",
"postedAmount": "ยอดเข้าบัญชี (หยวน)",
"refundableBalance": "สามารถขอคืนเงินได้ (หยวน)",
"payBody": "จ่ายร่างกาย",
@@ -77,7 +87,7 @@
"reason": "เหตุผลในการคืนเงิน",
"finishTime": "เสร็จสิ้นเวลา",
"orderAmount": "จำนวนการสั่งซื้อ",
"orderNo": "หมายเลขคำสั่งซื้อเดิม",
"orderNo": "Platform Order No.",
"authCode": "รหัสชำระเงิน",
"merchant": "ร้านค้า"
},
@@ -85,27 +95,33 @@
"wait_pay": "ซึ่งรอคอย",
"paid": "จ่าย",
"closed": "ปิด",
"expired": "หมดอายุแล้ว"
"expired": "หมดอายุแล้ว",
"paying": "Paying",
"failed": "Failed"
},
"fundStatus": {
"init": "อักษรย่อ",
"processing": "กำลังประมวลผล",
"success": "ความสำเร็จ",
"fail": "ล้มเหลว",
"close": "ปิด"
"close": "ปิด",
"cancel": "Cancelled"
},
"bizStatusColor": {
"wait_pay": "ส้ม",
"paid": "สีเขียว",
"closed": "ค่าเริ่มต้น",
"expired": "สีแดง"
"expired": "สีแดง",
"paying": "processing",
"failed": "red"
},
"fundStatusColor": {
"init": "ค่าเริ่มต้น",
"processing": "กำลังประมวลผล",
"success": "สีเขียว",
"fail": "สีแดง",
"close": "ค่าเริ่มต้น"
"close": "ค่าเริ่มต้น",
"cancel": "default"
},
"placeholder": {
"bizOrderNo": "ป้อนหมายเลขคำสั่งซื้อของร้านค้า",
@@ -143,9 +159,6 @@
"refundReasonPlaceholder": "ป้อนเหตุผลการคืนเงิน (ไม่บังคับ)",
"refundAmountExceed": "จำนวนเงินคืนต้องไม่เกินจำนวนเงินที่สามารถขอคืนได้"
},
"tradeType": {
"normal": "ชำระเงินปกติ"
},
"method": {
"jsapi": "JSAPI",
"qrcode": "สแกนโค้ด",
@@ -153,5 +166,13 @@
"app": "APP",
"barcode": "บาร์โค้ด",
"wap": "WAP"
},
"tradeType": {
"normal": "Normal Pay",
"gateway": "Gateway Pay"
},
"gatewayType": {
"cashier": "Cashier",
"aggregate": "Aggregate"
}
}
}

View File

@@ -1,7 +1,11 @@
{
"normal": {
"title": "Lệnh thanh toán",
"detail": "Chi tiết đơn hàng thanh toán"
"title": "Thanh toán thường",
"detail": "Chi tiết thanh toán thường"
},
"gateway": {
"title": "Thanh toán cổng",
"detail": "Chi tiết thanh toán cổng"
},
"trade": {
"title": "Giao dịch quỹ",
@@ -10,24 +14,30 @@
"refund": {
"title": "Lệnh hoàn tiền",
"detail": "Chi tiết đơn hàng hoàn tiền",
"statusLabel": "Refund Status",
"section": {
"identity": "Identity",
"fund": "Fund",
"amount": "Amount",
"channel": "Channel",
"notify": "Notify"
},
"status": {
"init": "Ban đầu",
"progress": "Hoàn tiền",
"success": "Thành công",
"fail": "Thất bại",
"close": "Đã đóng"
"progress": "Refunding",
"success": "Success",
"fail": "Failed",
"close": "Closed"
},
"statusColor": {
"init": "mặc định",
"progress": "xử lý",
"success": "màu xanh lá",
"fail": "màu đỏ",
"close": "mặc định"
"progress": "processing",
"success": "green",
"fail": "red",
"close": "default"
}
},
"field": {
"bizOrderNo": "Số đơn đặt hàng của người bán",
"tradeNo": "Số giao dịch",
"tradeNo": "Fund Trade No.",
"title": "Tiêu đề",
"description": "Sự miêu tả",
"amount": "Số tiền(Nhân dân tệ)",
@@ -41,7 +51,7 @@
"provider": "nhà cung cấp",
"outOrderNo": "Số thứ tự kênh",
"transOrderNo": "Số đơn hàng chuyển khoản",
"relationOrderNo": "Số thứ tự quan hệ",
"relationOrderNo": "Channel Relation No.",
"postedAmount": "Số đã ghi sổ(Nhân dân tệ)",
"refundableBalance": "Có thể hoàn lại(Nhân dân tệ)",
"payBody": "Cơ quan thanh toán",
@@ -77,7 +87,7 @@
"reason": "Lý do hoàn tiền",
"finishTime": "Thời gian kết thúc",
"orderAmount": "Số tiền đặt hàng",
"orderNo": "Số đơn đặt hàng ban đầu",
"orderNo": "Platform Order No.",
"authCode": "Mã thanh toán",
"merchant": "Thương nhân"
},
@@ -85,27 +95,33 @@
"wait_pay": "Chờ",
"paid": "Trả",
"closed": "Đã đóng",
"expired": "Hết hạn"
"expired": "Hết hạn",
"paying": "Paying",
"failed": "Failed"
},
"fundStatus": {
"init": "Ban đầu",
"processing": "Xử lý",
"success": "Thành công",
"fail": "Thất bại",
"close": "Đã đóng"
"close": "Đã đóng",
"cancel": "Cancelled"
},
"bizStatusColor": {
"wait_pay": "quả cam",
"paid": "màu xanh lá",
"closed": "mặc định",
"expired": "màu đỏ"
"expired": "màu đỏ",
"paying": "processing",
"failed": "red"
},
"fundStatusColor": {
"init": "mặc định",
"processing": "xử lý",
"success": "màu xanh lá",
"fail": "màu đỏ",
"close": "mặc định"
"close": "mặc định",
"cancel": "default"
},
"placeholder": {
"bizOrderNo": "Nhập số đơn đặt hàng của người bán.",
@@ -143,9 +159,6 @@
"refundReasonPlaceholder": "Nhập lý do hoàn tiền (tùy chọn)",
"refundAmountExceed": "Số tiền hoàn lại không được vượt quá số tiền được hoàn lại"
},
"tradeType": {
"normal": "Thanh toán thường"
},
"method": {
"jsapi": "JSAPI",
"qrcode": "Quét mã",
@@ -153,5 +166,13 @@
"app": "APP",
"barcode": "Mã vạch",
"wap": "WAP"
},
"tradeType": {
"normal": "Normal Pay",
"gateway": "Gateway Pay"
},
"gatewayType": {
"cashier": "Cashier",
"aggregate": "Aggregate"
}
}
}

View File

@@ -1,93 +1,83 @@
<script lang="ts" setup>
import type { DashboardData } from '../types';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { formatDateTime } from '@vben/utils';
import { useMessage } from '#/hooks/useMessage';
import { NormalOrderApi, type NormalOrderResult } from '#/api/payment/order/normal-order.api';
import { channelI18nMap } from '#/enums/payment/channelEnum';
interface Props {
/** 工作台聚合数据(支付订单暂无后端 API,保留以统一 widget props 契约) */
/** 工作台聚合数据(支付订单独立拉数据,保留以统一 widget props 契约) */
data?: DashboardData;
}
defineOptions({ name: 'PayOrderListWidget' });
// 支付订单暂无后端 API保留 data prop 以统一 widget 渲染契约
// 支付订单独立拉取数据,不消费聚合统计;保留 data prop 以统一 widget 渲染契约
withDefaults(defineProps<Props>(), {
data: undefined,
});
const { message } = useMessage();
const router = useRouter();
// 订单状态枚举
type OrderStatus = 'closed' | 'fail' | 'success';
const loading = ref(false);
const orders = ref<NormalOrderResult[]>([]);
interface MockOrder {
// 订单号
orderNo: string;
// 商品标题
title: string;
// 金额(元)
amount: string;
// 状态
status: OrderStatus;
// 支付渠道 code对应 payment.channel.common.*
channel: string;
// 时间mock 字符串,直接展示)
time: string;
/** 拉取最近支付订单(按创建时间倒序取 20 条) */
async function load() {
loading.value = true;
try {
const res = await NormalOrderApi.page({ current: 1, size: 20 });
orders.value = res?.data?.records || [];
} finally {
loading.value = false;
}
}
// 订单标题模板池
const titlePool = [
'会员包年套餐',
'商品购买',
'VIP月卡续费',
'电子书-架构之道',
'课程订阅-进阶',
'实物商品-配件',
'软件授权-年度',
'礼品卡充值',
'直播打赏',
'云服务-包月',
];
const channelPool = ['wechat', 'alipay', 'unionPay'];
// 状态权重success 出现概率更高
const statusPool: OrderStatus[] = ['success', 'success', 'success', 'fail', 'closed'];
const amountPool = ['299.00', '59.90', '19.90', '45.00', '199.00', '128.00', '88.88', '599.00', '36.50', '1080.00'];
onMounted(load);
// 生成 20 条 mock 订单数据(后端支付订单管理 API 就绪后替换为真实请求结果)
const mockOrders: MockOrder[] = Array.from({ length: 20 }).map((_, i) => {
// 日期从 06-27 往前推,每 4 条一天
const day = String(27 - Math.floor(i / 4)).padStart(2, '0');
const hour = String(8 + (i % 14)).padStart(2, '0');
const min = String((i * 13) % 60).padStart(2, '0');
const seq = String(1001 + i);
return {
orderNo: `PAY202606${day}${hour}${min}${seq}`,
title: i >= titlePool.length ? `${titlePool[i % titlePool.length]!} #${seq}` : titlePool[i]!,
amount: amountPool[i % amountPool.length]!,
status: statusPool[i % statusPool.length]!,
channel: channelPool[i % channelPool.length]!,
time: `2026-06-${day} ${hour}:${min}`,
} as MockOrder;
});
/** 订单状态 → a-tag color 映射 */
function statusColor(status: OrderStatus): string {
if (status === 'success') return 'green';
if (status === 'fail') return 'red';
return 'default';
/** 跳转到普通支付订单列表("更多"按钮) */
function goAll() {
router.push({ name: '/trade/pay-order/normal' }).catch(() => {});
}
/** 订单状态 → i18n key */
function statusLabel(status: OrderStatus): string {
return $t(`dashboard.workspace.payOrder.status.${status}`);
/** 金额分转元 */
function formatAmount(amount?: number): string {
if (amount === null || amount === undefined) return '-';
return (amount / 100).toFixed(2);
}
/** "更多"按钮:支付订单管理页尚未开发,提示开发中 */
function handleMore() {
message.info($t('dashboard.workspace.payOrder.developing'));
/** 业务状态颜色:复用订单页 bizStatusColor 配置 */
function statusColor(status?: string): string {
return status ? $t(`payment.order.bizStatusColor.${status}`) : 'default';
}
/** 业务状态文案:复用订单页 bizStatus 配置 */
function statusLabel(status?: string): string {
return status ? $t(`payment.order.bizStatus.${status}`) : '-';
}
/** 支付通道展示名channelI18nMap 取 i18n key 再翻译,无映射时降级原 code */
function channelLabel(channel?: string): string {
if (!channel) return '-';
const i18nKey = channelI18nMap[channel];
if (i18nKey) {
const text = $t(i18nKey);
if (text && text !== i18nKey) return text;
}
return channel;
}
/** 时间格式化:优先支付成功时间,降级创建时间 */
function fmtTime(order: NormalOrderResult): string {
const time = order.payTime || order.createTime;
if (!time) return '-';
return formatDateTime(time) || '-';
}
</script>
@@ -100,27 +90,31 @@
</div>
</template>
<template #extra>
<a-button type="link" size="small" @click="handleMore">{{ $t('common.more') }}</a-button>
<a-button type="link" size="small" @click="goAll">{{ $t('common.more') }}</a-button>
</template>
<a-skeleton v-if="loading" active :paragraph="{ rows: 5 }" />
<a-empty v-else-if="orders.length === 0" class="!my-6" />
<!-- 订单列表固定高度滚动与左右卡片等高协调 -->
<ul class="pay-order-scroll flex max-h-[230px] flex-col gap-3 overflow-y-auto pr-1">
<li v-for="(order, index) in mockOrders" :key="index" class="flex flex-col gap-1">
<ul v-else class="pay-order-scroll flex max-h-[230px] flex-col gap-3 overflow-y-auto pr-1">
<li v-for="(order, index) in orders" :key="order.id ?? index" class="flex flex-col gap-1">
<!-- 行1状态标签 + 标题 + 金额 -->
<div class="flex items-center gap-2">
<a-tag :color="statusColor(order.status)" class="!m-0 !shrink-0 text-xs">
{{ statusLabel(order.status) }}
</a-tag>
<span class="text-foreground/80 flex-1 truncate text-sm">{{ order.title }}</span>
<span class="text-foreground shrink-0 text-sm font-semibold tabular-nums">¥{{ order.amount }}</span>
<span class="text-foreground/80 flex-1 truncate text-sm">{{ order.title || '-' }}</span>
<span class="text-foreground shrink-0 text-sm font-semibold tabular-nums"
>¥{{ formatAmount(order.amount) }}</span
>
</div>
<!-- 行2订单号 · 渠道 · 时间 -->
<p class="text-foreground/40 truncate text-xs">
<span>{{ order.orderNo }}</span>
<span>{{ order.orderNo || '-' }}</span>
<span class="mx-1">·</span>
<span>{{ $t(`payment.channel.common.${order.channel}`) }}</span>
<span>{{ channelLabel(order.channel) }}</span>
<span class="mx-1">·</span>
<span>{{ order.time }}</span>
<span>{{ fmtTime(order) }}</span>
</p>
</li>
</ul>

View File

@@ -113,7 +113,7 @@ export const ENTRY_CATALOG: QuickEntryMeta[] = [
// 以下为可选扩展入口(默认不显示,用户在编辑抽屉按需添加)
{
key: 'payOrder',
routeName: '/trade/normal-pay',
routeName: '/trade/pay-order/normal',
icon: 'lucide:receipt',
titleKey: 'dashboard.workspace.quickEntry.payOrder',
color: 'bg-blue-600',

View File

@@ -0,0 +1,529 @@
<script lang="ts" setup>
import type { MenuProps } from 'antdv-next';
import type { VxeTableInstance, VxeToolbarInstance } from 'vxe-table';
import { computed, onMounted, ref } from 'vue';
import { $t } from '@vben/locales';
import { IconifyIcon } from '@vben-core/icons';
import {
GatewayOrderApi,
type GatewayOrderQuery,
type GatewayOrderResult,
} from '#/api/payment/order/gateway-order.api';
import { type RefundParam, RefundOrderApi } from '#/api/payment/order/refund-order.api';
import { BQuery, type QueryField } from '#/components/query';
import { PermCodes } from '#/constants/perm-codes';
import { productI18nMap, productNameMap } from '#/enums/payment';
import { useMessage } from '#/hooks/useMessage';
import { usePermission } from '#/hooks/usePermission';
defineOptions({ name: 'GatewayOrderList' });
const { confirm, message } = useMessage();
const { hasPermission } = usePermission();
const loading = ref(false);
const xTable = ref<VxeTableInstance>();
const xToolbar = ref<VxeToolbarInstance>();
const queryForm = ref<GatewayOrderQuery>({});
const pageConfig = ref({
currentPage: 1,
pageSize: 10,
total: 0,
});
const tableData = ref<GatewayOrderResult[]>([]);
const drawerVisible = ref(false);
const drawerLoading = ref(false);
const detail = ref<GatewayOrderResult>({});
const actionLoading = ref(false);
// 退款弹窗
const refundVisible = ref(false);
const refundLoading = ref(false);
const refundFetching = ref(false);
const refundFormRef = ref();
const refundForm = ref<{ amount?: number; tradeNo?: string; reason?: string }>({ amount: undefined, reason: '' });
const refundRow = ref<GatewayOrderResult | null>(null);
const refundableYuan = computed(() => (refundRow.value?.refundableBalance ?? 0) / 100);
const refundRules = computed(() => ({
amount: [
{ required: true, message: $t('payment.order.action.refundAmountPlaceholder') },
{
type: 'number',
min: 0.01,
message: $t('payment.order.action.refundAmountPlaceholder'),
},
{
validator: async (_rule: unknown, value: number) => {
if (value != null && value > refundableYuan.value) {
return Promise.reject(new Error($t('payment.order.action.refundAmountExceed')));
}
},
},
],
}));
// 网关业务状态
const statusOptions = computed(() =>
['wait_pay', 'paying', 'paid', 'failed', 'closed', 'expired'].map((v) => ({
label: $t(`payment.order.bizStatus.${v}`),
value: v,
})),
);
const gatewayTypeOptions = computed(() =>
['cashier', 'aggregate'].map((v) => ({
label: $t(`payment.order.gatewayType.${v}`),
value: v,
})),
);
// 支付产品下拉
const productOptions = computed(() =>
Object.keys(productNameMap).map((code) => ({
label: productLabel(code),
value: code,
})),
);
const queryFields = computed<QueryField[]>(() => [
{
type: 'string',
field: 'orderNo',
name: $t('payment.order.field.orderNo'),
placeholder: $t('payment.order.placeholder.orderNo'),
},
{
type: 'string',
field: 'bizOrderNo',
name: $t('payment.order.field.bizOrderNo'),
placeholder: $t('payment.order.placeholder.bizOrderNo'),
},
{
type: 'list',
field: 'status',
name: $t('payment.order.field.bizStatus'),
selectList: statusOptions.value,
},
{
type: 'list',
field: 'gatewayType',
// 网关类型
name: $t('payment.order.field.gatewayType'),
selectList: gatewayTypeOptions.value,
},
{
type: 'list',
field: 'product',
// 支付产品
name: $t('payment.order.field.product'),
selectList: productOptions.value,
},
{
type: 'string',
field: 'capability',
// 支付能力
name: $t('payment.order.field.capability'),
},
{
type: 'date_time_range',
field: 'createTime',
name: $t('payment.order.field.createTime'),
startField: 'createTimeStart',
endField: 'createTimeEnd',
},
]);
function queryPage() {
loading.value = true;
return GatewayOrderApi.page({
current: pageConfig.value.currentPage,
size: pageConfig.value.pageSize,
...queryForm.value,
})
.then((res) => {
tableData.value = res.data?.records || [];
pageConfig.value.total = Number(res.data?.total) || 0;
loading.value = false;
})
.catch(() => {
loading.value = false;
});
}
function resetQuery() {
queryForm.value = {};
pageConfig.value.currentPage = 1;
queryPage();
}
function handlePageChange({ currentPage, pageSize }: { currentPage: number; pageSize: number }) {
pageConfig.value.currentPage = currentPage;
pageConfig.value.pageSize = pageSize;
queryPage();
}
function formatAmount(amount?: number): string {
if (amount === null || amount === undefined) return '-';
return (amount / 100).toFixed(2);
}
function statusColor(status?: string): string {
return status ? $t(`payment.order.bizStatusColor.${status}`) : 'default';
}
/**
* 支付产品展示名
*/
function productLabel(code?: string): string {
if (!code) return '-';
const i18nKey = productI18nMap[code];
if (i18nKey) {
const text = $t(i18nKey);
if (text && text !== i18nKey) {
return text;
}
}
return productNameMap[code] || code;
}
/**
* 支付能力展示名
*/
function capabilityLabel(code?: string): string {
if (!code) return '-';
const i18nKey = `payment.merchant.cashier.cashier.capabilities.${code}`;
const text = $t(i18nKey);
return text && text !== i18nKey ? text : code;
}
async function handleView(row: GatewayOrderResult) {
drawerVisible.value = true;
drawerLoading.value = true;
try {
const { data } = await GatewayOrderApi.getById(row.id!);
detail.value = data || {};
} finally {
drawerLoading.value = false;
}
}
function handleSync(row: GatewayOrderResult) {
confirm({
title: $t('payment.order.action.syncConfirmTitle'),
content: $t('payment.order.action.syncConfirmContent'),
onOk() {
actionLoading.value = true;
return GatewayOrderApi.sync(row.id!)
.then(() => {
message.success($t('payment.order.action.syncSuccess'));
queryPage();
})
.finally(() => {
actionLoading.value = false;
});
},
});
}
function handleClose(row: GatewayOrderResult) {
confirm({
title: $t('payment.order.action.closeConfirmTitle'),
content: $t('payment.order.action.closeConfirmContent'),
onOk() {
actionLoading.value = true;
return GatewayOrderApi.close(row.id!)
.then(() => {
message.success($t('payment.order.action.closeSuccess'));
queryPage();
})
.finally(() => {
actionLoading.value = false;
});
},
});
}
async function openRefund(row: GatewayOrderResult) {
refundRow.value = row;
refundVisible.value = true;
refundFetching.value = true;
try {
const { data } = await GatewayOrderApi.getById(row.id!);
refundRow.value = data || row;
refundForm.value = {
tradeNo: data?.tradeNo,
reason: '',
amount: (data?.refundableBalance ?? 0) / 100,
};
} finally {
refundFetching.value = false;
}
}
async function submitRefund() {
if (!refundRow.value) {
return;
}
try {
await refundFormRef.value?.validate();
} catch {
return;
}
const amountYuan = refundForm.value.amount ?? 0;
const param: RefundParam = {
tradeNo: refundForm.value.tradeNo,
bizOrderNo: refundRow.value.bizOrderNo,
amount: Math.round(amountYuan * 100),
reason: refundForm.value.reason,
};
confirm({
title: $t('payment.order.action.refundConfirmTitle'),
content: $t('payment.order.action.refundConfirmContent', { amount: amountYuan.toFixed(2) }),
onOk() {
refundLoading.value = true;
return RefundOrderApi.refund(param)
.then(() => {
message.success($t('payment.order.action.refundSuccess'));
refundVisible.value = false;
queryPage();
})
.finally(() => {
refundLoading.value = false;
});
},
});
}
function getActionMenu(row: GatewayOrderResult): MenuProps {
const items: MenuProps['items'] = [];
if (hasPermission(PermCodes.Trade.GatewayOrder.MANAGE)) {
if (row.status === 'wait_pay' || row.status === 'paying') {
items.push({ key: 'sync', label: $t('payment.order.action.sync') });
items.push({ key: 'close', label: $t('payment.order.action.close'), danger: true });
}
if (row.status === 'paid') {
items.push({ key: 'sync', label: $t('payment.order.action.sync') });
if (hasPermission(PermCodes.Trade.Refund.MANAGE)) {
items.push({ key: 'refund', label: $t('payment.order.action.refund'), danger: true });
}
}
}
return {
items,
onClick: ({ key }) => {
switch (key) {
case 'sync': {
handleSync(row);
break;
}
case 'close': {
handleClose(row);
break;
}
case 'refund': {
openRefund(row);
break;
}
}
},
};
}
function handleDrawerClose() {
drawerVisible.value = false;
detail.value = {};
}
onMounted(() => {
xTable.value?.connectToolbar(xToolbar.value as VxeToolbarInstance);
queryPage();
});
</script>
<template>
<div class="m-3 p-3 bg-background rounded-lg list-page-compact">
<a-card>
<BQuery :fields="queryFields" :query-params="queryForm" @query="queryPage" @reset="resetQuery" />
</a-card>
<div class="mt-4">
<a-card>
<vxe-toolbar ref="xToolbar" custom refresh :refresh-options="{ queryMethod: queryPage }" />
<vxe-table ref="xTable" :row-config="{ keyField: 'id' }" :data="tableData" :loading="loading">
<vxe-column type="seq" :title="$t('common.seq')" width="60" align="center" />
<vxe-column field="mchName" :title="$t('payment.order.field.merchant')" :min-width="160">
<template #default="{ row }">
<div class="flex flex-col">
<span>{{ row.mchName || '-' }}</span>
<span v-if="row.mchNo" class="text-xs text-muted-foreground">{{ row.mchNo }}</span>
</div>
</template>
</vxe-column>
<vxe-column field="orderNo" :title="$t('payment.order.field.orderNo')" :min-width="200" show-overflow />
<vxe-column field="bizOrderNo" :title="$t('payment.order.field.bizOrderNo')" :min-width="180" show-overflow />
<vxe-column field="gatewayType" :title="$t('payment.order.field.gatewayType')" :min-width="100">
<template #default="{ row }">
{{ row.gatewayType ? $t(`payment.order.gatewayType.${row.gatewayType}`) : '-' }}
</template>
</vxe-column>
<vxe-column field="title" :title="$t('payment.order.field.title')" :min-width="140" show-overflow />
<vxe-column field="amount" :title="$t('payment.order.field.amount')" :min-width="100" align="right">
<template #default="{ row }">{{ formatAmount(row.amount) }}</template>
</vxe-column>
<vxe-column field="status" :title="$t('payment.order.field.bizStatus')" :min-width="100" align="center">
<template #default="{ row }">
<a-tag :color="statusColor(row.status)">
{{ $t(`payment.order.bizStatus.${row.status}`) }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="product" :title="$t('payment.order.field.product')" :min-width="140" show-overflow>
<template #default="{ row }">{{ productLabel(row.product) }}</template>
</vxe-column>
<vxe-column field="capability" :title="$t('payment.order.field.capability')" :min-width="140" show-overflow>
<template #default="{ row }">{{ capabilityLabel(row.capability) }}</template>
</vxe-column>
<vxe-column
field="createTime"
:title="$t('payment.order.field.createTime')"
:min-width="160"
formatter="formatDateTime"
/>
<vxe-column :title="$t('common.operation')" width="160" fixed="right" :show-overflow="false">
<template #default="{ row }">
<a-space :size="2">
<template #separator>
<a-divider type="vertical" />
</template>
<a-button type="link" size="small" @click="handleView(row)">
{{ $t('common.view') }}
</a-button>
<a-dropdown v-if="getActionMenu(row).items?.length" :menu="getActionMenu(row)">
<a-button type="link" size="small">
{{ $t('common.more') }}
<IconifyIcon icon="ant-design:down-outlined" class="inline" />
</a-button>
</a-dropdown>
</a-space>
</template>
</vxe-column>
</vxe-table>
<vxe-pager
size="medium"
:loading="loading"
:current-page="pageConfig.currentPage"
:page-size="pageConfig.pageSize"
:total="pageConfig.total"
@page-change="handlePageChange"
/>
</a-card>
</div>
<a-drawer
v-model:open="drawerVisible"
:title="$t('payment.order.gateway.detail')"
:size="900"
@close="handleDrawerClose"
>
<a-spin :spinning="drawerLoading">
<a-descriptions :column="2" size="small" bordered>
<a-descriptions-item :label="$t('payment.order.field.merchant')">
{{ detail.mchName || '-' }}
<span v-if="detail.mchNo" class="text-muted-foreground"> ({{ detail.mchNo }})</span>
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.orderNo')">
{{ detail.orderNo || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.bizOrderNo')">
{{ detail.bizOrderNo || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.tradeNo')">
{{ detail.tradeNo || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.title')">
{{ detail.title || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.bizStatus')">
<a-tag :color="statusColor(detail.status)">
{{ detail.status ? $t(`payment.order.bizStatus.${detail.status}`) : '-' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.amount')">
{{ formatAmount(detail.amount) }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.fundStatus')">
{{ detail.fundStatus ? $t(`payment.order.fundStatus.${detail.fundStatus}`) : '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.product')">
{{ productLabel(detail.product) }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.capability')">
{{ capabilityLabel(detail.capability) }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.channel')">
{{ detail.channel || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.method')">
{{ detail.method || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.outOrderNo')">
{{ detail.outOrderNo || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.refundableBalance')">
{{ formatAmount(detail.refundableBalance) }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.payTime')">
{{ detail.payTime || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.clientIp')">
{{ detail.clientIp || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.storeNo')">
{{ detail.storeNo || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.errorMsg')" :span="2">
{{ detail.errorMsg || '-' }}
</a-descriptions-item>
</a-descriptions>
</a-spin>
<template #footer>
<a-button @click="handleDrawerClose">{{ $t('common.close') }}</a-button>
</template>
</a-drawer>
<!-- 退款弹窗 -->
<a-modal
v-model:open="refundVisible"
:title="$t('payment.order.action.refund')"
:confirm-loading="refundLoading"
:ok-button-props="{ disabled: refundFetching || !refundForm.tradeNo }"
@ok="submitRefund"
@cancel="refundVisible = false"
>
<a-spin :spinning="refundFetching">
<a-form ref="refundFormRef" layout="vertical" :model="refundForm" :rules="refundRules">
<a-form-item :label="$t('payment.order.field.tradeNo')">
<a-input :value="refundForm.tradeNo" disabled />
</a-form-item>
<a-form-item :label="$t('payment.order.action.refundableBalanceLabel')">
{{ refundableYuan.toFixed(2) }}
</a-form-item>
<a-form-item :label="$t('payment.order.action.refundAmountLabel')" name="amount">
<a-input-number v-model:value="refundForm.amount" class="w-full" :min="0.01" :precision="2" />
</a-form-item>
<a-form-item :label="$t('payment.order.action.refundReasonLabel')" name="reason">
<a-textarea v-model:value="refundForm.reason" :placeholder="$t('payment.order.action.refundReasonPlaceholder')" />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</div>
</template>

View File

@@ -8,6 +8,7 @@
import { RefundOrderApi, type RefundOrderQuery, type RefundOrderResult } from '#/api/payment/order/refund-order.api';
import { BQuery, type QueryField } from '#/components/query';
import { PermCodes } from '#/constants/perm-codes';
import { productI18nMap, productNameMap } from '#/enums/payment';
import { useMessage } from '#/hooks/useMessage';
import { usePermission } from '#/hooks/usePermission';
@@ -20,7 +21,6 @@
const xTable = ref<VxeTableInstance>();
const xToolbar = ref<VxeToolbarInstance>();
// 查询条件
const queryForm = ref<RefundOrderQuery>({});
const pageConfig = ref({
@@ -31,29 +31,35 @@
const tableData = ref<RefundOrderResult[]>([]);
// 详情抽屉
const drawerVisible = ref(false);
const drawerLoading = ref(false);
const detail = ref<RefundOrderResult>({});
const actionLoading = ref(false);
// 退款状态下拉
// 退款状态(无 init
const statusOptions = computed(() =>
['init', 'progress', 'success', 'fail', 'close'].map((v) => ({
['progress', 'success', 'fail', 'close'].map((v) => ({
label: $t(`payment.order.refund.status.${v}`),
value: v,
})),
);
// 支付通道下拉(常用通道)
const channelOptions = computed(() =>
['alipay', 'wechat', 'douyin'].map((v) => ({
label: $t(`payment.channel.common.${v}`),
// 交易类型(原支付形态)
const tradeTypeOptions = computed(() =>
['normal', 'gateway'].map((v) => ({
label: $t(`payment.order.tradeType.${v}`),
value: v,
})),
);
// 查询字段
// 支付产品
const productOptions = computed(() =>
Object.keys(productNameMap).map((code) => ({
label: productLabel(code),
value: code,
})),
);
const queryFields = computed<QueryField[]>(() => [
{
type: 'string',
@@ -63,9 +69,10 @@
},
{
type: 'string',
field: 'orderNo',
name: $t('payment.order.field.orderNo'),
placeholder: $t('payment.order.placeholder.orderNo'),
field: 'tradeNo',
// 资金交易号
name: $t('payment.order.field.tradeNo'),
placeholder: $t('payment.order.placeholder.tradeNo'),
},
{
type: 'string',
@@ -76,14 +83,23 @@
{
type: 'list',
field: 'status',
name: $t('payment.order.field.bizStatus'),
// 退款状态
name: $t('payment.order.refund.statusLabel'),
selectList: statusOptions.value,
},
{
type: 'list',
field: 'channel',
name: $t('payment.order.field.channel'),
selectList: channelOptions.value,
field: 'tradeType',
// 交易类型
name: $t('payment.order.field.tradeType'),
selectList: tradeTypeOptions.value,
},
{
type: 'list',
field: 'product',
// 支付产品
name: $t('payment.order.field.product'),
selectList: productOptions.value,
},
{
type: 'date_time_range',
@@ -94,9 +110,6 @@
},
]);
/**
* 分页查询
*/
function queryPage() {
loading.value = true;
return RefundOrderApi.page({
@@ -126,29 +139,33 @@
queryPage();
}
/**
* 金额分转元
*/
function formatAmount(amount?: number): string {
if (amount === null || amount === undefined) return '-';
return (amount / 100).toFixed(2);
}
/**
* 退款状态颜色
*/
function statusColor(status?: string): string {
return status ? $t(`payment.order.refund.statusColor.${status}`) : 'default';
}
function channelLabel(code?: string): string {
function productLabel(code?: string): string {
if (!code) return '-';
return channelOptions.value.find((o) => o.value === code)?.label || code;
const i18nKey = productI18nMap[code];
if (i18nKey) {
const text = $t(i18nKey);
if (text && text !== i18nKey) {
return text;
}
}
return productNameMap[code] || code;
}
function tradeTypeLabel(code?: string): string {
if (!code) return '-';
const text = $t(`payment.order.tradeType.${code}`);
return text && text !== `payment.order.tradeType.${code}` ? text : code;
}
/**
* 查看详情
*/
async function handleView(row: RefundOrderResult) {
drawerVisible.value = true;
drawerLoading.value = true;
@@ -160,9 +177,6 @@
}
}
/**
* 同步退款状态
*/
function handleSync(row: RefundOrderResult) {
confirm({
title: $t('payment.order.action.syncConfirmTitle'),
@@ -203,39 +217,35 @@
<vxe-toolbar ref="xToolbar" custom refresh :refresh-options="{ queryMethod: queryPage }" />
<vxe-table ref="xTable" :row-config="{ keyField: 'id' }" :data="tableData" :loading="loading">
<vxe-column type="seq" :title="$t('common.seq')" width="60" align="center" />
<!-- 商户: 名称上 + 号下小字两排 -->
<vxe-column field="mchName" :title="$t('payment.order.field.merchant')" :min-width="160">
<template #default="{ row }">
<div class="flex flex-col">
<span>{{ row.mchName || '-' }}</span>
<span>{{ row.mchName || row.mchNo || '-' }}</span>
<span v-if="row.mchNo" class="text-xs text-muted-foreground">{{ row.mchNo }}</span>
</div>
</template>
</vxe-column>
<vxe-column field="refundNo" :title="$t('payment.order.field.refundNo')" :min-width="200" show-overflow />
<vxe-column field="orderNo" :title="$t('payment.order.field.orderNo')" :min-width="200" show-overflow />
<vxe-column field="tradeNo" :title="$t('payment.order.field.tradeNo')" :min-width="200" show-overflow />
<vxe-column field="bizOrderNo" :title="$t('payment.order.field.bizOrderNo')" :min-width="180" show-overflow />
<vxe-column field="amount" :title="$t('payment.order.field.amount')" :min-width="100" align="right">
<template #default="{ row }">
{{ formatAmount(row.amount) }}
</template>
</vxe-column>
<vxe-column field="status" :title="$t('payment.order.field.bizStatus')" :min-width="100" align="center">
<vxe-column field="status" :title="$t('payment.order.refund.statusLabel')" :min-width="100" align="center">
<template #default="{ row }">
<a-tag :color="statusColor(row.status)">
{{ $t(`payment.order.refund.status.${row.status}`) }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="channel" :title="$t('payment.order.field.channel')" :min-width="100">
<template #default="{ row }">{{ channelLabel(row.channel) }}</template>
<vxe-column field="tradeType" :title="$t('payment.order.field.tradeType')" :min-width="110">
<template #default="{ row }">{{ tradeTypeLabel(row.tradeType) }}</template>
</vxe-column>
<vxe-column field="product" :title="$t('payment.order.field.product')" :min-width="140" show-overflow>
<template #default="{ row }">{{ productLabel(row.product) }}</template>
</vxe-column>
<vxe-column
field="finishTime"
:title="$t('payment.order.field.finishTime')"
:min-width="160"
formatter="formatDateTime"
/>
<vxe-column
field="createTime"
:title="$t('payment.order.field.createTime')"
@@ -275,18 +285,22 @@
</a-card>
</div>
<!-- 详情抽屉 -->
<a-drawer
v-model:open="drawerVisible"
:title="$t('payment.order.refund.detail')"
:size="700"
:size="720"
@close="handleDrawerClose"
>
<a-spin :spinning="drawerLoading">
<a-descriptions :column="2" size="small" bordered>
<!-- 身份信息 -->
<div class="mb-4 text-sm font-medium">{{ $t('payment.order.refund.section.identity') }}</div>
<a-descriptions :column="2" size="small" bordered class="mb-4">
<a-descriptions-item :label="$t('payment.order.field.merchant')">
{{ detail.mchName || '-' }}
<span v-if="detail.mchNo" class="text-muted-foreground"> ({{ detail.mchNo }})</span>
{{ detail.mchName || detail.mchNo || '-' }}
<span v-if="detail.mchName && detail.mchNo" class="text-muted-foreground"> ({{ detail.mchNo }})</span>
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.appId')">
{{ detail.appId || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.refundNo')">
{{ detail.refundNo || '-' }}
@@ -294,32 +308,51 @@
<a-descriptions-item :label="$t('payment.order.field.bizRefundNo')">
{{ detail.bizRefundNo || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.orderNo')">
{{ detail.orderNo || '-' }}
<a-descriptions-item :label="$t('payment.order.field.title')" :span="2">
{{ detail.title || '-' }}
</a-descriptions-item>
</a-descriptions>
<!-- 关联资金 -->
<div class="mb-4 text-sm font-medium">{{ $t('payment.order.refund.section.fund') }}</div>
<a-descriptions :column="2" size="small" bordered class="mb-4">
<a-descriptions-item :label="$t('payment.order.field.tradeNo')">
{{ detail.tradeNo || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.tradeType')">
{{ tradeTypeLabel(detail.tradeType) }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.bizOrderNo')">
{{ detail.bizOrderNo || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.outOrderNo')">
{{ detail.outOrderNo || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.relationOrderNo')">
{{ detail.relationOrderNo || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.outRefundNo')">
{{ detail.outRefundNo || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.bizStatus')">
</a-descriptions>
<!-- 金额与状态 -->
<div class="mb-4 text-sm font-medium">{{ $t('payment.order.refund.section.amount') }}</div>
<a-descriptions :column="2" size="small" bordered class="mb-4">
<a-descriptions-item :label="$t('payment.order.refund.statusLabel')">
<a-tag :color="statusColor(detail.status)">
{{ detail.status ? $t(`payment.order.refund.status.${detail.status}`) : '-' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.currency')">
{{ detail.currency || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.amount')">
{{ formatAmount(detail.amount) }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.orderAmount')">
{{ formatAmount(detail.orderAmount) }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.channel')">
{{ channelLabel(detail.channel) }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.method')">
{{ detail.method || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.reason')" :span="2">
{{ detail.reason || '-' }}
</a-descriptions-item>
@@ -333,6 +366,40 @@
{{ detail.errorMsg || '-' }}
</a-descriptions-item>
</a-descriptions>
<!-- 通道快照产品为主不展示能力 -->
<div class="mb-4 text-sm font-medium">{{ $t('payment.order.refund.section.channel') }}</div>
<a-descriptions :column="2" size="small" bordered class="mb-4">
<a-descriptions-item :label="$t('payment.order.field.product')">
{{ productLabel(detail.product) }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.channel')">
{{ detail.channel || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.channelMchNo')">
{{ detail.channelMchNo || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.channelAppId')">
{{ detail.channelAppId || '-' }}
</a-descriptions-item>
</a-descriptions>
<!-- 通知与审计 -->
<div class="mb-4 text-sm font-medium">{{ $t('payment.order.refund.section.notify') }}</div>
<a-descriptions :column="2" size="small" bordered>
<a-descriptions-item :label="$t('payment.order.field.notifyUrl')" :span="2">
{{ detail.notifyUrl || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.attach')" :span="2">
{{ detail.attach || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.clientIp')">
{{ detail.clientIp || '-' }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.order.field.storeNo')">
{{ detail.storeNo || '-' }}
</a-descriptions-item>
</a-descriptions>
</a-spin>
<template #footer>