diff --git a/src/api/admin/user.ts b/src/api/admin/user.ts index b3240dc..5a4d151 100644 --- a/src/api/admin/user.ts +++ b/src/api/admin/user.ts @@ -1,3 +1,4 @@ +import { submitExportTask } from "/@/api/asyncTask" import request from "/@/utils/request" /** @@ -165,3 +166,6 @@ export function getSimpleUserAll(params?: { name?: string; mobile?: string; user params, }) } + +/** 按原筛选条件提交后台分批导出。 */ +export const submitUserExport = (query: Record) => submitExportTask("/admin/user/export-async", query) diff --git a/src/api/asyncTask.ts b/src/api/asyncTask.ts new file mode 100644 index 0000000..f80540c --- /dev/null +++ b/src/api/asyncTask.ts @@ -0,0 +1,84 @@ +import request from "/@/utils/request" + +/** 通用异步任务进度状态,由后端 Redis 进度接口返回。 */ +export interface AsyncTaskProgress { + /** 提交接口返回的不透明任务 ID。 */ + taskId: string + /** 业务 Redis Key 命名空间,不包含租户、用户等服务端隔离信息。 */ + redisKey: string + /** 任务展示名称。 */ + taskName: string + /** 固定状态编码:PENDING、RUNNING、SUCCESS、FAILED。 */ + status: string + /** 本次任务需要处理的总项目数。 */ + total: number + /** 当前已经处理的项目数。 */ + completed: number + /** 当前完成百分比,取值为 0 至 100。 */ + percentage: number + /** 当前进度或最终结果说明。 */ + message: string + /** 是否已进入成功或失败终态。 */ + finished: boolean + /** 终态是否成功;任务未结束时为 false。 */ + success: boolean + /** 导出等任务成功后生成的文件下载地址;导入任务为空。 */ + resultFileUrl?: string + /** 导出等任务成功后建议使用的下载文件名称;导入任务为空。 */ + resultFileName?: string +} + +/** 通用异步任务轮询配置,导入、导出等业务只需提供自身命名空间和必要覆盖项。 */ +export interface AsyncTaskPollingConfig { + /** 通用进度查询接口地址。 */ + progressUrl: string + /** 当前业务使用的 Redis Key 命名空间。 */ + redisKey: string + /** 提交接口响应数据中的任务 ID 字段名,默认 taskId。 */ + taskIdField?: string + /** 两次进度查询之间的间隔,单位毫秒,默认 1000。 */ + pollIntervalMs?: number + /** 单次持续轮询的最长时间,单位毫秒,默认 2 小时。 */ + pollTimeoutMs?: number + /** 连续查询失败多少次后暂停轮询并允许用户重试,默认 5 次。 */ + maxConsecutiveErrors?: number + /** 是否等任务进入终态或轮询中断后再显示弹窗,默认 false。 */ + showDialogOnFinished?: boolean + /** 任务执行期间展示的等待提示;为空时不额外展示。 */ + waitingMessage?: string + /** 成功终态存在结果文件时的下载按钮文案,默认“下载结果”。 */ + downloadButtonText?: string + /** 任务成功后由业务组件展示的覆盖文案。 */ + successMessage?: string + /** 任务失败后由业务组件展示的覆盖文案。 */ + failureMessage?: string +} + +/** 通用异步任务进度查询参数。 */ +export interface AsyncTaskProgressQuery { + /** 业务 Redis Key 命名空间。 */ + redisKey: string + /** 提交接口返回的任务 ID。 */ + taskId: string +} + +/** + * 查询当前登录用户创建的异步任务进度。 + * + * @param progressUrl 业务配置的通用进度接口地址 + * @param query Redis Key 命名空间和任务 ID + * @returns 包含当前任务进度的统一响应 + */ +export const getAsyncTaskProgress = (progressUrl: string, query: AsyncTaskProgressQuery) => { + return request({ + url: progressUrl, + method: "get", + params: query, + }) +} + +/** 提交导出快照;仅透传原有排序参数,分页由服务端导出批次控制。 */ +export const submitExportTask = (url: string, query: Record) => request({ + url, method: "post", data: query, + params: { ascs: query.ascs, descs: query.descs }, +}) diff --git a/src/api/hard/hard-order.ts b/src/api/hard/hard-order.ts index fe43bf4..5c124c3 100644 --- a/src/api/hard/hard-order.ts +++ b/src/api/hard/hard-order.ts @@ -1,3 +1,4 @@ +import { submitExportTask } from "/@/api/asyncTask" import request from "/@/utils/request" /** 订单详情中的单张识别证据,字段来自 HardOrderEvidenceImageVo。 */ @@ -54,58 +55,73 @@ export interface HardOrderDetail { [key: string]: unknown } -/** 订单列表统计查询条件;字段与订单分页筛选保持一致。 */ -export type HardOrderStatisticsQuery = Record +/** 与订单列表一致的筛选条件,不含分页、排序和勾选记录。 */ +export type HardOrderStatisticsQuery = Record -/** 订单按餐段汇总的统计,金额单位均为分。 */ -export interface HardOrderMealStatistics { - /** 餐段字典编码;历史订单未记录时为空。 */ - hourType?: string | null - /** 订单数。 */ - orderCount?: number - /** 购物车商品份数。 */ - productCount?: number - /** 商品总价。 */ - totalPrice?: number - /** 额外支付。 */ - extraPrice?: number - /** 配送费。 */ - deliveryPrice?: number - /** 实际支付。 */ - payPrice?: number - /** 已退款金额。 */ - refundPrice?: number - /** 订单实际金额,即实际支付减已退款。 */ - actualPrice?: number -} - -/** 订单统计通用汇总,金额单位均为分。 */ +/** 金额为整数分字符串;计数为字符串,避免累计精度丢失。 */ export interface HardOrderStatisticsSummary { - orderCount?: number - productCount?: number - totalPrice?: number - extraPrice?: number - deliveryPrice?: number - payPrice?: number - refundPrice?: number - actualPrice?: number - meals?: HardOrderMealStatistics[] + orderCount: string + totalPrice: string + extraPrice: string + couponPrice: string + payPrice: string + refundPrice: string + netPrice: string } -/** 订单按预计使用日期拆分的统计。 */ export interface HardOrderDailyStatistics extends HardOrderStatisticsSummary { - date?: string + date: string +} + +export interface HardOrderPayTypeStatistics extends HardOrderStatisticsSummary { + payType: string | null +} + +export interface HardOrderMerchantStatistics extends HardOrderStatisticsSummary { + merchantId: string | null + merchantName: string +} + +export interface HardOrderStatisticsData { + summary: HardOrderStatisticsSummary + daily: HardOrderDailyStatistics[] + payTypes: HardOrderPayTypeStatistics[] + withoutPayTime: HardOrderStatisticsSummary + generatedAt: string } -/** 订单统计接口统一响应。 */ export interface HardOrderStatisticsResponse { - data: { - total?: HardOrderStatisticsSummary - daily?: HardOrderDailyStatistics[] - } + data: HardOrderStatisticsData msg?: string } +export interface HardOrderMerchantStatisticsPage { + records: HardOrderMerchantStatistics[] + current: number + size: number + hasMore: boolean + generatedAt: string +} + +export function statistics(query: HardOrderStatisticsQuery) { + return request({ + url: "/hard/hard-order/statistics", + method: "post", + data: query, + timeout: 25000, + }) +} + +export function merchantStatistics(query: HardOrderStatisticsQuery, current = 1, size = 20) { + return request({ + url: "/hard/hard-order/statistics/merchants", + method: "post", + params: { current, size }, + data: query, + timeout: 25000, + }) +} + /** * 离线订单手动支付 */ @@ -207,3 +223,6 @@ export function putObj(obj?: Object) { data: obj, }) } + +/** 按原筛选条件提交后台分批导出。 */ +export const submitExport = (query: Record) => submitExportTask("/hard/hard-order/export-async", query) diff --git a/src/api/hard/hard-third-order.ts b/src/api/hard/hard-third-order.ts new file mode 100644 index 0000000..6ea0e71 --- /dev/null +++ b/src/api/hard/hard-third-order.ts @@ -0,0 +1,87 @@ +import request from "/@/utils/request" + +export type ThirdOrderId = string | number + +/** 第三方应用支付写入的硬件底单,所有金额均为分。 */ +export interface ThirdOrder { + id: ThirdOrderId + orderNo?: string + orderType: string + orderStatus: string + payType?: string + userId?: ThirdOrderId + username?: string + userMobile?: string + userAvatar?: string + userDeptName?: string + merchantId?: ThirdOrderId + merchantName?: string + merchantType?: string + walletId?: ThirdOrderId + walletTypeUniqueCode?: string + parentCarId?: ThirdOrderId + productDesc?: string + totalPrice?: number + extraPrice?: number + payPrice?: number + refundPrice?: number + refundStatus?: string + payRefundId?: ThirdOrderId + afterSaleNo?: string + createTime?: string + payTime?: string + refundTime?: string + updateTime?: string +} + +export interface ThirdOrderProduct { + id: ThirdOrderId + productName?: string + productPrice?: number + productCount?: number + totalPrice?: number +} + +export interface ThirdOrderRefund { + id: ThirdOrderId + no?: string + status: string + refundPrice?: number + applyReason?: string + applyDescription?: string + auditReason?: string + createTime?: string + auditTime?: string + refundTime?: string +} + +export interface ThirdOrderDetail { + order: ThirdOrder + products: ThirdOrderProduct[] + refunds: ThirdOrderRefund[] +} + +export interface ThirdOrderRefundParams { + orderId: ThirdOrderId + refundPrice: number + requestNo: string + applyReason: string + applyDescription?: string +} + +export function page(query: Record) { + return request({ + url: "/hard/hard-third-order/page", + method: "post", + params: { current: query.current, size: query.size }, + data: query, + }) +} + +export function detail(id: ThirdOrderId) { + return request({ url: `/hard/hard-third-order/${id}`, method: "get" }) +} + +export function refund(data: ThirdOrderRefundParams) { + return request({ url: "/hard/hard-third-order/refund", method: "post", data }) +} diff --git a/src/api/hard/hard-total.ts b/src/api/hard/hard-total.ts index 0f0cef4..ed7a97e 100644 --- a/src/api/hard/hard-total.ts +++ b/src/api/hard/hard-total.ts @@ -1,3 +1,4 @@ +import { submitExportTask } from "/@/api/asyncTask" import request from "/@/utils/request" /** @@ -126,3 +127,12 @@ export function pageListCsByMonth(query?: any) { data: query, }) } + +/** 按原筛选条件提交后台分批导出。 */ +export const submitMerchantExport = (query: Record) => submitExportTask("/hard/hard-total/export-cs-async", query) + +/** 按原筛选条件提交后台分批导出。 */ +export const submitMerchantMonthExport = (query: Record) => submitExportTask("/hard/hard-total/export-cs-by-month-async", query) + +/** 按原筛选条件提交后台分批导出。 */ +export const submitDepartmentExport = (query: Record) => submitExportTask("/hard/hard-total/export-dept-async", query) diff --git a/src/api/wallet/wallet-manage.ts b/src/api/wallet/wallet-manage.ts index b75c0d7..fd82086 100644 --- a/src/api/wallet/wallet-manage.ts +++ b/src/api/wallet/wallet-manage.ts @@ -1,4 +1,11 @@ import request from "/@/utils/request" +import { submitExportTask } from "/@/api/asyncTask" + +/** 按当前筛选条件提交账户余额后台分批导出。 */ +export const submitExport = (query: Record) => submitExportTask("/wallet/wallet-manage/export-async", query) + +/** 按所选月份与原筛选条件提交钱包历史余额分批导出。 */ +export const submitQbExport = (query: Record) => submitExportTask("/wallet/wallet-manage/export-qb-async", query) /** * 不活跃用户统计分页 diff --git a/src/api/wallet/wallet-recharge-apply.ts b/src/api/wallet/wallet-recharge-apply.ts index 56da1bb..9ed870d 100644 --- a/src/api/wallet/wallet-recharge-apply.ts +++ b/src/api/wallet/wallet-recharge-apply.ts @@ -1,3 +1,4 @@ +import { submitExportTask } from "/@/api/asyncTask" import request from "/@/utils/request" export function saveTopup(data?: Object) { @@ -87,3 +88,6 @@ export function validateError(params: any) { params: params, }) } + +/** 按原筛选条件提交后台分批导出。 */ +export const submitExport = (query: Record) => submitExportTask("/wallet/wallet-recharge-apply/export-async", query) diff --git a/src/api/wallet/wallet-recharge-import.ts b/src/api/wallet/wallet-recharge-import.ts index a2c56c2..e0baa07 100644 --- a/src/api/wallet/wallet-recharge-import.ts +++ b/src/api/wallet/wallet-recharge-import.ts @@ -1,3 +1,4 @@ +import { submitExportTask } from "/@/api/asyncTask" import request from "/@/utils/request" export function pageJoin(query: any) { @@ -58,3 +59,6 @@ export function recover(params?: Object) { params, }) } + +/** 按原筛选条件提交后台分批导出。 */ +export const submitExport = (query: Record) => submitExportTask("/wallet/wallet-recharge-import/export-async", query) diff --git a/src/api/wallet/wallet-recharge-invoice.ts b/src/api/wallet/wallet-recharge-invoice.ts index f329481..6fa10b1 100644 --- a/src/api/wallet/wallet-recharge-invoice.ts +++ b/src/api/wallet/wallet-recharge-invoice.ts @@ -1,3 +1,4 @@ +import { submitExportTask } from "/@/api/asyncTask" import request from "/@/utils/request" export function invoiceBack(query: any) { @@ -107,3 +108,6 @@ export function putObj(obj?: Object) { data: obj, }) } + +/** 按原筛选条件提交后台分批导出。 */ +export const submitExport = (query: Record) => submitExportTask("/wallet/walletRechargeInvoice/export-async", query) diff --git a/src/api/wallet/wallet-transaction.ts b/src/api/wallet/wallet-transaction.ts index 08a8859..9b5bf2b 100644 --- a/src/api/wallet/wallet-transaction.ts +++ b/src/api/wallet/wallet-transaction.ts @@ -1,3 +1,4 @@ +import { submitExportTask } from "/@/api/asyncTask" import request from "/@/utils/request" /** @@ -238,3 +239,18 @@ export function putObj(obj?: Object) { data: obj, }) } + +/** 按原筛选条件提交后台分批导出。 */ +export const submitThExport = (query: Record) => submitExportTask("/wallet/wallet-transaction/export-th-async", query) + +/** 按原筛选条件提交后台分批导出。 */ +export const submitTkExport = (query: Record) => submitExportTask("/wallet/wallet-transaction/export-tk-async", query) + +/** 按原筛选条件提交后台分批导出。 */ +export const submitXfExport = (query: Record) => submitExportTask("/wallet/wallet-transaction/export-xf-async", query) + +/** 按当前筛选条件提交充值流水分批导出。 */ +export const submitCzExport = (query: Record) => submitExportTask("/wallet/wallet-transaction/export-cz-async", query) + +/** 按当前筛选条件提交记账扣款流水分批导出。 */ +export const submitJzExport = (query: Record) => submitExportTask("/wallet/wallet-transaction/export-jz-async", query) diff --git a/src/components/AsyncTaskProgress.vue b/src/components/AsyncTaskProgress.vue new file mode 100644 index 0000000..6dfac3d --- /dev/null +++ b/src/components/AsyncTaskProgress.vue @@ -0,0 +1,143 @@ + + + diff --git a/src/hooks/batchExport.ts b/src/hooks/batchExport.ts new file mode 100644 index 0000000..1d05030 --- /dev/null +++ b/src/hooks/batchExport.ts @@ -0,0 +1,65 @@ +import { ref, toRaw, unref } from "vue" +import type { AsyncTaskPollingConfig, AsyncTaskProgress } from "/@/api/asyncTask" +import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue" +import { useMessage } from "/@/hooks/message" +import { downloadExportFile } from "/@/utils/exportDownload" + +/** 页面只声明业务接口、名称及命名空间,任务状态由公共组件管理。 */ +interface BatchExportOptions { + progressUrl: string + redisKey: string + taskName: string + fileName: string + submitTask: (query: Record) => Promise +} + +/** 固定筛选快照,保留排序;当前列表页码和页大小不限制全量导出。 */ +export const snapshotExportQuery = (query: Record) => { + const snapshot = JSON.parse(JSON.stringify(toRaw(unref(query)))) + delete snapshot.current + delete snapshot.size + return snapshot as Record +} + +/** 复用任务提交互斥、进度跟踪及下载。查询中断后继续跟踪原任务,不重新提交。 */ +export const useBatchExport = (options: BatchExportOptions) => { + const exportProgressRef = ref>() + const exportSubmitting = ref(false) + const exportTaskConfig: AsyncTaskPollingConfig = { + progressUrl: options.progressUrl, + redisKey: options.redisKey, + waitingMessage: "正在后台分批生成文件,进度达到 100% 表示文件已就绪", + downloadButtonText: "重新下载 Excel", + } + const submitExport = async (query: Record) => { + if (exportSubmitting.value) { + exportProgressRef.value?.show() + return + } + exportSubmitting.value = true + exportProgressRef.value?.startSubmitting(options.taskName) + try { + const response = await options.submitTask(snapshotExportQuery(query)) + if (!response?.data?.taskId) throw new Error("导出任务响应缺少任务 ID") + exportProgressRef.value?.start(response.data) + } catch (error: any) { + exportSubmitting.value = false + const message = error?.msg || error?.message || "导出任务提交失败,请重试" + exportProgressRef.value?.submissionFailed(message) + useMessage().error(message) + } + } + const handleExportSuccess = (progress: AsyncTaskProgress) => { + exportSubmitting.value = false + try { + downloadExportFile(progress.resultFileUrl || "", progress.resultFileName || options.fileName || "导出结果.xlsx") + } catch (error: any) { + useMessage().error(error?.message || "下载导出文件失败") + } + } + const handleExportFailure = (progress: AsyncTaskProgress) => { + exportSubmitting.value = false + useMessage().error(progress.message || "导出任务失败") + } + return { exportProgressRef, exportTaskConfig, exportSubmitting, submitExport, handleExportSuccess, handleExportFailure } +} diff --git a/src/utils/exportDownload.ts b/src/utils/exportDownload.ts new file mode 100644 index 0000000..70d509e --- /dev/null +++ b/src/utils/exportDownload.ts @@ -0,0 +1,16 @@ +import other from "/@/utils/other" + +/** 文件中心结果由浏览器原生下载,避免 Axios Blob 将整个大文件读进 JS 内存。 */ +export const downloadExportFile = (fileUrl: string, fileName: string) => { + if (!fileUrl) throw new Error("导出完成,但未返回文件下载地址") + const url = /^https?:\/\//i.test(fileUrl) + ? fileUrl + : `${String(import.meta.env.VITE_API_URL || "").replace(/\/$/, "")}${other.adaptationUrl(fileUrl)}` + const link = document.createElement("a") + link.href = url + link.style.display = "none" + link.download = (fileName || "导出结果.xlsx").replace(/[<>:"/\\|?*]/g, "_") + document.body.appendChild(link) + link.click() + link.remove() +} diff --git a/src/utils/request.ts b/src/utils/request.ts index b66285f..4e7075f 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -82,6 +82,8 @@ const handleResponse = (response: AxiosResponse) => { * 添加 Axios 的响应拦截器,用于全局响应结果处理 */ service.interceptors.response.use(handleResponse, (error) => { + // 超时和网络中断没有 response,保留原错误供页面展示和重试。 + if (!error.response) return Promise.reject(error) const status = Number(error.response.status) || 200 if (status === 423) { return Promise.reject({ msg: '"演示环境,仅供预览"' }) diff --git a/src/views/admin/system/user/index.vue b/src/views/admin/system/user/index.vue index 3fd71a6..217a7ba 100644 --- a/src/views/admin/system/user/index.vue +++ b/src/views/admin/system/user/index.vue @@ -224,10 +224,16 @@ + + + + diff --git a/src/views/hard/hard-order/components/statistics-drawer/utils.ts b/src/views/hard/hard-order/components/statistics-drawer/utils.ts new file mode 100644 index 0000000..f8333b3 --- /dev/null +++ b/src/views/hard/hard-order/components/statistics-drawer/utils.ts @@ -0,0 +1,48 @@ +import dayjs from "dayjs" +import type { HardOrderStatisticsQuery } from "/@/api/hard/hard-order" + +const ignoredFields = new Set(["current", "size", "descs", "ascs", "ids", "selectObjs", "tenantId", "mchTypeCodes"]) + +/** 在点击时复制表单,避免分页、导出勾选或后续表单编辑污染统计。 */ +export function snapshotStatisticsQuery(form: HardOrderStatisticsQuery): HardOrderStatisticsQuery { + const query = JSON.parse(JSON.stringify(Object.fromEntries(Object.entries(form).filter(([key]) => !ignoredFields.has(key))))) + let hasTimeRange = false + for (const [key, label] of [["createTimeList", "创建时间"], ["payTimeList", "支付时间"]]) { + const range = query[key] + if (range == null || (Array.isArray(range) && range.length === 0)) continue + if (!Array.isArray(range) || range.length !== 2 || !range[0] || !range[1]) { + throw new Error(`${label}需填写完整`) + } + const start = dayjs(range[0]) + const end = dayjs(range[1]) + if (!start.isValid() || !end.isValid() || end.isBefore(start)) { + throw new Error(`${label}开始时间不能晚于结束时间`) + } + if (end.isAfter(start.add(3, "month").endOf("day"))) { + throw new Error(`${label}区间不能超过三个月,请缩小查询范围`) + } + hasTimeRange = true + } + if (!hasTimeRange) throw new Error("统计需选择创建时间或支付时间,单个区间不能超过三个月") + return query +} + +/** 直接移动整数分的小数点;卡片、表格和提示均不先转 Number。 */ +export function formatStatisticsMoney(value: string | number | null | undefined): string { + const raw = String(value ?? "0") + if (!/^-?\d+$/.test(raw)) return "0.00" + const negative = raw.startsWith("-") + const digits = raw.replace(/^-/, "").replace(/^0+(?=\d)/, "").padStart(3, "0") + const yuan = digits.slice(0, -2).replace(/\B(?=(\d{3})+(?!\d))/g, ",") + return `${negative && /[1-9]/.test(digits) ? "-" : ""}${yuan}.${digits.slice(-2)}` +} + +export function formatStatisticsCount(value: string | number): string { + return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ",") +} + +export function escapeChartText(value: unknown): string { + return String(value ?? "").replace(/[&<>"']/g, (character) => ({ + "&": "&", "<": "<", ">": ">", '"': """, "'": "'", + }[character]!)) +} diff --git a/src/views/hard/hard-order/index.vue b/src/views/hard/hard-order/index.vue index fe3f6d9..3701a05 100644 --- a/src/views/hard/hard-order/index.vue +++ b/src/views/hard/hard-order/index.vue @@ -34,6 +34,13 @@ + + + + + + @@ -67,7 +74,12 @@ + @queryTable="getDataList"> + + + + + + + + + diff --git a/src/views/hard/hard-third-order/components/refund-dialog.vue b/src/views/hard/hard-third-order/components/refund-dialog.vue new file mode 100644 index 0000000..30e60f3 --- /dev/null +++ b/src/views/hard/hard-third-order/components/refund-dialog.vue @@ -0,0 +1,271 @@ + + + + + diff --git a/src/views/hard/hard-third-order/index.vue b/src/views/hard/hard-third-order/index.vue new file mode 100644 index 0000000..cc09576 --- /dev/null +++ b/src/views/hard/hard-third-order/index.vue @@ -0,0 +1,632 @@ + + + + + diff --git a/src/views/hard/hard-third-order/order.ts b/src/views/hard/hard-third-order/order.ts new file mode 100644 index 0000000..fb66d0b --- /dev/null +++ b/src/views/hard/hard-third-order/order.ts @@ -0,0 +1,70 @@ +import type { ThirdOrder, ThirdOrderRefund } from "/@/api/hard/hard-third-order" + +type StatusType = "success" | "warning" | "danger" | "info" | "" + +/** 部分退款与全额退款共用 orderStatus=10,必须结合累计退款金额判断。 */ +export function refundState(order: ThirdOrder): { label: string; type: StatusType } { + if (String(order.refundStatus) === "10") return { label: "退款处理中", type: "warning" } + const refunded = Number(order.refundPrice || 0) + if (refunded > 0 && refunded >= Number(order.payPrice || 0)) return { label: "已全额退款", type: "info" } + if (refunded > 0) return { label: "已部分退款", type: "warning" } + if (String(order.refundStatus) === "30") return { label: "退款未通过", type: "danger" } + return { label: "未退款", type: "info" } +} + +export function orderState(order: ThirdOrder): { label: string; type: StatusType } { + if (Number(order.refundPrice || 0) > 0 || String(order.refundStatus) === "10") return refundState(order) + const states: Record = { + "1": { label: "待支付", type: "warning" }, + "2": { label: "待支付", type: "warning" }, + "5": { label: "已支付", type: "success" }, + "6": { label: "离线支付", type: "success" }, + "10": { label: "退款", type: "warning" }, + "20": { label: "已核销", type: "success" }, + "30": { label: "已取消", type: "info" }, + } + return states[String(order.orderStatus)] || { label: `状态 ${order.orderStatus || "未知"}`, type: "info" } +} + +export const remainingPrice = (order: ThirdOrder) => + order.payTime && ["5", "10", "20"].includes(String(order.orderStatus)) + ? Math.max(0, Number(order.payPrice || 0) - Number(order.refundPrice || 0)) + : 0 + +export function refundDisabledReason(order: ThirdOrder, refunds: ThirdOrderRefund[] = []): string { + if (String(order.orderType) !== "6") return "仅支持第三方应用支付底单" + if (!["5", "10", "20"].includes(String(order.orderStatus)) || !order.payTime) return "订单尚未完成支付" + if (String(order.payType) !== "3" || !order.walletId) return "该支付方式不支持底单钱包退款" + if (String(order.refundStatus) === "10" || refunds.some((item) => String(item.status) === "10")) return "存在处理中的退款" + if (remainingPrice(order) <= 0) return "订单已全额退款" + return "" +} + +export function saleState(status: string): { label: string; type: StatusType } { + return ( + ( + { + "10": { label: "申请中", type: "warning" }, + "50": { label: "退款完成", type: "success" }, + "61": { label: "已取消", type: "info" }, + "62": { label: "已拒绝", type: "danger" }, + } as Record + )[String(status)] || { label: `状态 ${status || "未知"}`, type: "info" } + ) +} + +/** 历史第三方退款未写 refundTime 时,以已完成售后单的审核时间补充展示。 */ +export const refundTime = (refund: ThirdOrderRefund) => + refund.refundTime || (String(refund.status) === "50" ? refund.auditTime : undefined) || refund.createTime + +export function apiResult(log: { error?: string; res?: string }): { label: string; type: StatusType } { + if (log.error) return { label: "调用异常", type: "danger" } + try { + const result = JSON.parse(log.res || "null") + if (result?.code === 200) return { label: "成功", type: "success" } + if (result?.code != null) return { label: result.message || `返回码 ${result.code}`, type: "danger" } + } catch { + // 保留原始响应预览,无法解析时不推断调用成功。 + } + return { label: "查看响应", type: "info" } +} diff --git a/src/views/mch/mch-manage/form.vue b/src/views/mch/mch-manage/form.vue index 4b300f1..d6ec892 100644 --- a/src/views/mch/mch-manage/form.vue +++ b/src/views/mch/mch-manage/form.vue @@ -1,154 +1,194 @@ @@ -202,6 +242,13 @@ const jobTime = reactive<{ }) const merchantTypeRadioDisabled = ref(false) +/** 营业时间仅调整展示布局,沿用当前接口的早、中、晚三个时段及序列化顺序。 */ +const jobTimeSlots = [ + { key: "morning", label: "早上" }, + { key: "noon", label: "中午" }, + { key: "evening", label: "晚上" }, +] + // 定义校验规则 const dataRules = { name: [{ required: true, message: "商户名不能为空", trigger: "change" }], @@ -318,3 +365,146 @@ defineExpose({ openDialog, }) + + diff --git a/src/views/mch/mch-manage/index.vue b/src/views/mch/mch-manage/index.vue index c2bf011..6f4fdfa 100644 --- a/src/views/mch/mch-manage/index.vue +++ b/src/views/mch/mch-manage/index.vue @@ -1,175 +1,273 @@