From d073c6036c392f88a9487cfa1722d5014d2bd203 Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Sun, 12 Jul 2026 11:55:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=94=B6=E9=93=B6=E5=8F=B0?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E9=A1=B9=E9=85=8D=E7=BD=AE=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=9B=BE=E6=A0=87=E5=AF=B9=E9=BD=90=E6=B8=A0?= =?UTF-8?q?=E9=81=93=E7=BC=96=E7=A0=81=E3=80=81=E5=95=86=E6=88=B7=E5=8E=BB?= =?UTF-8?q?=E9=87=8D=E3=80=81=E8=83=BD=E5=8A=9B=E6=89=B9=E9=87=8F=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E3=80=81=E7=9C=9F=E5=AE=9E=E5=9B=BE=E6=A0=87=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 图标选项对齐 PayProviderEnum 7 项编码,新增/迁移 union→union_pay、aggregate→aggregate_pay - DIRECT 模式商户列表扁平去重,支付能力改用 batch API 合并该商户全部 method 组合 - 图标联动从商户选择移到支付能力选择(从 batch key 反推 provider) - 图标下拉与已选回显均显示真实 SVG(optionRender + labelRender) - 列表图标列显示 SVG 图标 + 名称 - 弹窗改为抽屉(:size=800),表单字段重排,图标必填 - 新增支付能力 29 项 i18n(zh-CN / en-US) --- .../langs/en-US/payment/merchant/cashier.json | 39 ++- .../langs/zh-CN/payment/merchant/cashier.json | 39 ++- .../merchant/cashier/CashierConfig.vue | 46 ++- .../merchant/cashier/CashierItemEdit.vue | 306 +++++++++++------- .../merchant/cashier/shared/constants.ts | 11 +- 5 files changed, 303 insertions(+), 138 deletions(-) diff --git a/apps/daxpay-admin/src/locales/langs/en-US/payment/merchant/cashier.json b/apps/daxpay-admin/src/locales/langs/en-US/payment/merchant/cashier.json index ef3c1f7b1..823403dd1 100644 --- a/apps/daxpay-admin/src/locales/langs/en-US/payment/merchant/cashier.json +++ b/apps/daxpay-admin/src/locales/langs/en-US/payment/merchant/cashier.json @@ -27,6 +27,7 @@ "capabilityPlaceholder": "Select capability", "methodFilterHint": "Used to load channel merchant candidates; not stored in DIRECT mode", "validationName": "Please enter name", + "validationIcon": "Please select an icon", "validationResolveMode": "Please select resolve mode", "validationMethod": "Please select payment method", "validationChannelMch": "Please select channel merchant", @@ -39,11 +40,43 @@ "douyin": "Douyin" }, "icons": { + "aggregate_pay": "Aggregate Pay", "wechat": "WeChat", "alipay": "Alipay", - "union": "UnionPay", - "douyin": "Douyin", - "aggregate": "Aggregate" + "union_pay": "UnionPay", + "visa": "Visa", + "mastercard": "Mastercard", + "douyin": "Douyin" + }, + "capabilities": { + "aggregate_pay_qrcode": "Aggregate QR Code", + "aggregate_pay_barcode": "Aggregate Barcode", + "wechat_cashier": "WeChat Mini Cashier", + "wechat_jsapi": "WeChat JSAPI", + "wechat_app": "WeChat App", + "wechat_h5": "WeChat H5", + "wechat_qr": "WeChat QR", + "wechat_mini": "WeChat Mini", + "wechat_barcode": "WeChat Barcode", + "alipay_barcode": "Alipay Barcode", + "alipay_qr": "Alipay QR", + "alipay_app": "Alipay App", + "alipay_h5": "Alipay H5", + "alipay_pc": "Alipay PC", + "alipay_jsapi": "Alipay JSAPI", + "alipay_mini": "Alipay Mini", + "union_pay_qr": "UnionPay QR", + "union_pay_barcode": "UnionPay Barcode", + "union_pay_h5": "UnionPay H5", + "union_pay_jsapi": "UnionPay JSAPI", + "douyin_qr": "Douyin QR", + "douyin_jsapi": "Douyin JSAPI", + "douyin_h5": "Douyin H5", + "douyin_app": "Douyin App", + "visa_card_gateway": "Visa Gateway", + "visa_card_present": "Visa Present", + "mastercard_card_gateway": "Mastercard Gateway", + "mastercard_card_present": "Mastercard Present" } } } diff --git a/apps/daxpay-admin/src/locales/langs/zh-CN/payment/merchant/cashier.json b/apps/daxpay-admin/src/locales/langs/zh-CN/payment/merchant/cashier.json index 256ace7fc..7a5582b4b 100644 --- a/apps/daxpay-admin/src/locales/langs/zh-CN/payment/merchant/cashier.json +++ b/apps/daxpay-admin/src/locales/langs/zh-CN/payment/merchant/cashier.json @@ -27,6 +27,7 @@ "capabilityPlaceholder": "请选择支付能力", "methodFilterHint": "用于加载通道商户候选, 保存时不落库", "validationName": "请输入名称", + "validationIcon": "请选择图标", "validationResolveMode": "请选择解析模式", "validationMethod": "请选择支付方式", "validationChannelMch": "请选择通道商户", @@ -39,11 +40,43 @@ "douyin": "抖音" }, "icons": { + "aggregate_pay": "聚合支付", "wechat": "微信", "alipay": "支付宝", - "union": "银联", - "douyin": "抖音", - "aggregate": "聚合" + "union_pay": "银联", + "visa": "Visa", + "mastercard": "Mastercard", + "douyin": "抖音" + }, + "capabilities": { + "aggregate_pay_qrcode": "聚合扫码", + "aggregate_pay_barcode": "聚合付款码", + "wechat_cashier": "微信小程序收银台", + "wechat_jsapi": "微信 JSAPI", + "wechat_app": "微信 APP", + "wechat_h5": "微信 H5", + "wechat_qr": "微信扫码", + "wechat_mini": "微信小程序", + "wechat_barcode": "微信付款码", + "alipay_barcode": "支付宝付款码", + "alipay_qr": "支付宝扫码", + "alipay_app": "支付宝 APP", + "alipay_h5": "支付宝 H5", + "alipay_pc": "支付宝电脑", + "alipay_jsapi": "支付宝 JSAPI", + "alipay_mini": "支付宝小程序", + "union_pay_qr": "银联扫码", + "union_pay_barcode": "银联付款码", + "union_pay_h5": "银联 H5", + "union_pay_jsapi": "银联 JSAPI", + "douyin_qr": "抖音扫码", + "douyin_jsapi": "抖音 JSAPI", + "douyin_h5": "抖音 H5", + "douyin_app": "抖音 APP", + "visa_card_gateway": "Visa 网关", + "visa_card_present": "Visa 线下", + "mastercard_card_gateway": "万事达网关", + "mastercard_card_present": "万事达线下" } } } diff --git a/apps/daxpay-admin/src/views/payment/merchant/cashier/CashierConfig.vue b/apps/daxpay-admin/src/views/payment/merchant/cashier/CashierConfig.vue index 56fecd0ba..ddf3afecd 100644 --- a/apps/daxpay-admin/src/views/payment/merchant/cashier/CashierConfig.vue +++ b/apps/daxpay-admin/src/views/payment/merchant/cashier/CashierConfig.vue @@ -17,6 +17,7 @@ import { useMessage } from '#/hooks/useMessage'; import { normalizeRouteQueryValue, useRequiredRouteQuery } from '#/hooks/useRequiredRouteQuery'; import { usePermission } from '#/hooks/usePermission'; + import { getProviderSvgUrl } from '#/views/payment/shared/payProviderDisplay'; import CashierItemEdit from './CashierItemEdit.vue'; import { @@ -58,6 +59,8 @@ const appInfo = ref({}); const tableData = ref([]); const methodLabelMap = ref>({}); + // DIRECT 模式列表展示用:通道商户号 → 名称 + const channelMchLabelMap = ref>({}); // 一级 / 二级 Tab const activeType = ref(CASHIER_TYPE.H5); @@ -89,7 +92,12 @@ /** 支付解析摘要 */ function paySummary(row: CashierItemResult) { if (row.resolveMode === RESOLVE_MODE.DIRECT) { - const parts = [row.channelMchNo, row.capability].filter(Boolean); + const mchLabel = channelMchLabelMap.value[row.channelMchNo || ''] || row.channelMchNo || ''; + // 支付能力为固定枚举(PayCapabilityEnum),直接用 i18n 静态翻译 + const capLabel = row.capability + ? $t(`payment.merchant.cashier.cashier.capabilities.${row.capability}`) + : ''; + const parts = [mchLabel, capLabel].filter(Boolean); return parts.join(' / ') || '-'; } return methodLabelMap.value[row.method || ''] || row.method || '-'; @@ -113,6 +121,26 @@ methodLabelMap.value = map; } + /** 加载通道商户标签(每应用仅加载一次) */ + async function loadChannelMchLabels() { + if (Object.keys(channelMchLabelMap.value).length > 0) { + return; + } + const { data } = await PayRouteApi.listSceneChannelMchCandidatesBatch({ + appId: appId.value, + }); + if (!data) { + return; + } + const labelMap: Record = {}; + for (const list of Object.values(data)) { + for (const item of list || []) { + labelMap[item.value] = item.label; + } + } + channelMchLabelMap.value = labelMap; + } + /** 加载当前桶列表 */ async function loadList() { if (!appId.value) return; @@ -181,7 +209,7 @@ onMounted(async () => { if (!routeContext.isValid.value) return; - await Promise.all([loadAppInfo(), loadMethodLabels()]); + await Promise.all([loadAppInfo(), loadMethodLabels(), loadChannelMchLabels()]); await loadList(); }); @@ -239,10 +267,18 @@ - - + + ([]); + const channelMchCombosMap = ref>>({}); + const capabilityProviderMap = ref>({}); const formState = ref({ mchNo: '', @@ -54,9 +57,8 @@ }); const methodOptions = ref([]); - // method value → provider(加载通道商户用) + // method value → provider(METHOD 模式联动图标用) const methodProviderMap = ref>({}); - const channelMchOptions = ref([]); const capabilityOptions = ref([]); const iconOptions = computed(() => @@ -69,6 +71,7 @@ const formRules = computed(() => { const rules: Record = { name: [{ required: true, message: $t('payment.merchant.cashier.cashier.validationName') }], + icon: [{ required: true, message: $t('payment.merchant.cashier.cashier.validationIcon') }], resolveMode: [{ required: true, message: $t('payment.merchant.cashier.cashier.validationResolveMode') }], }; if (formState.value.resolveMode === RESOLVE_MODE.METHOD) { @@ -100,8 +103,9 @@ channelMchNo: undefined, capability: undefined, }; - filterMethod.value = ''; channelMchOptions.value = []; + channelMchCombosMap.value = {}; + capabilityProviderMap.value = {}; capabilityOptions.value = []; formRef.value?.resetFields(); } @@ -113,9 +117,9 @@ const providerMap: Record = {}; for (const item of data || []) { const label = item.methodLabel || item.method; - const providerLabel = item.provider || ''; + // 只展示方式名称,不显示 provider 编码前缀 options.push({ - label: providerLabel ? `${providerLabel} / ${label}` : label, + label, value: item.method, }); providerMap[item.method] = item.provider; @@ -124,42 +128,90 @@ methodProviderMap.value = providerMap; } - /** DIRECT:按 filterMethod 加载通道商户 */ - async function loadChannelMchOptions(method: string) { + /** DIRECT:批量加载全部通道商户(扁平去重,记录每商户的全部 provider|method 组合) */ + async function loadAllChannelMchOptions() { channelMchOptions.value = []; - capabilityOptions.value = []; - if (!method || !context.value.appId) { + channelMchCombosMap.value = {}; + if (!context.value.appId) { return; } - const provider = methodProviderMap.value[method]; - if (!provider) { - return; - } - const { data } = await PayRouteApi.listSceneChannelMchCandidates({ + const { data } = await PayRouteApi.listSceneChannelMchCandidatesBatch({ appId: context.value.appId, - provider, - method, }); - channelMchOptions.value = data || []; + if (!data) { + return; + } + // 按 channelMchNo 去重,同时收集每商户的全部 provider|method 组合 + const labelMap = new Map(); + const combosMap: Record> = {}; + for (const [key, list] of Object.entries(data)) { + const parts = key.split('|'); + const provider = parts[0] || ''; + const method = parts.length >= 2 ? parts[1]! : ''; + if (!method) { + continue; + } + for (const item of list || []) { + labelMap.set(item.value, item.label); + if (!combosMap[item.value]) { + combosMap[item.value] = []; + } + combosMap[item.value]!.push({ method, provider }); + } + } + channelMchOptions.value = [...labelMap.entries()].map(([value, label]) => ({ + label, + value, + })); + channelMchCombosMap.value = combosMap; } - /** DIRECT:加载支付能力 */ - async function loadCapabilityOptions(method: string, channelMchNo: string) { + /** DIRECT:按通道商户加载支付能力候选(batch API 合并该商户全部 method 组合) */ + async function loadCapabilityByChannelMch(channelMchNo: string) { capabilityOptions.value = []; - if (!method || !channelMchNo || !context.value.appId) { + capabilityProviderMap.value = {}; + if (!channelMchNo || !context.value.appId) { return; } - const provider = methodProviderMap.value[method]; - if (!provider) { + const combos = channelMchCombosMap.value[channelMchNo]; + if (!combos?.length) { return; } - const { data } = await PayRouteApi.listSceneCapabilityCandidates({ + const { data } = await PayRouteApi.listSceneCapabilityCandidatesBatch({ appId: context.value.appId, - provider, - method, - channelMchNo, + items: combos.map((c) => ({ + provider: c.provider, + method: c.method, + channelMchNo, + })), }); - capabilityOptions.value = data || []; + if (!data) { + return; + } + // 合并去重,同时建立 capability → provider 映射(用于图标联动) + const capLabelMap = new Map(); + const capProviderMap: Record = {}; + for (const [key, list] of Object.entries(data)) { + const provider = key.split('|')[0] || ''; + for (const item of list || []) { + capLabelMap.set(item.value, item.label); + capProviderMap[item.value] = provider; + } + } + capabilityProviderMap.value = capProviderMap; + // label 统一用 i18n 翻译 + const toLabel = (code: string) => + $t(`payment.merchant.cashier.cashier.capabilities.${code}`) || code; + let options = [...capLabelMap.keys()].map((value) => ({ + label: toLabel(value), + value, + })); + // 编辑回显兜底:确保已选值在选项中 + const currentCap = formState.value.capability; + if (currentCap && !options.some((o) => o.value === currentCap)) { + options = [...options, { label: toLabel(currentCap), value: currentCap }]; + } + capabilityOptions.value = options; } /** 解析模式切换 */ @@ -167,27 +219,37 @@ formState.value.method = undefined; formState.value.channelMchNo = undefined; formState.value.capability = undefined; - filterMethod.value = ''; - channelMchOptions.value = []; capabilityOptions.value = []; + capabilityProviderMap.value = {}; } - /** DIRECT 过滤 method 变更 */ - async function onFilterMethodChange(method: any) { - filterMethod.value = method || ''; - formState.value.channelMchNo = undefined; - formState.value.capability = undefined; - await loadChannelMchOptions(filterMethod.value); + /** METHOD 模式支付方式变更 — 联动图标(icon = provider) */ + function onMethodChange(method: any) { + const provider = method ? methodProviderMap.value[method as string] : undefined; + if (provider) { + formState.value.icon = provider; + } } - /** 通道商户变更 */ + /** DIRECT 通道商户变更 — 加载该商户全部支付能力 */ async function onChannelMchChange(channelMchNo: any) { formState.value.channelMchNo = channelMchNo || undefined; formState.value.capability = undefined; - if (channelMchNo && filterMethod.value) { - await loadCapabilityOptions(filterMethod.value, channelMchNo); - } else { + if (!channelMchNo) { capabilityOptions.value = []; + return; + } + await loadCapabilityByChannelMch(channelMchNo as string); + } + + /** DIRECT 支付能力变更 — 联动图标(icon = provider,从 batch key 反推) */ + function onCapabilityChange(capability: any) { + if (!capability) { + return; + } + const provider = capabilityProviderMap.value[capability as string]; + if (provider) { + formState.value.icon = provider; } } @@ -202,6 +264,7 @@ initFormEditType(FormEditType.Add); resetForm(); await loadMethodDirectory(); + await loadAllChannelMchOptions(); } /** 编辑 */ @@ -221,6 +284,7 @@ initFormEditType(FormEditType.Edit); resetForm(); await loadMethodDirectory(); + await loadAllChannelMchOptions(); confirmLoading.value = true; try { const { data } = await CashierConfigApi.getById(opts.record.id!); @@ -240,46 +304,15 @@ channelMchNo: row.channelMchNo || undefined, capability: row.capability || undefined, }; - // DIRECT 编辑时无法还原 filterMethod,需用户重选目录方式加载候选;若已有 channelMch 则尝试 batch 匹配 + // DIRECT 编辑时还原支付能力候选 if (formState.value.resolveMode === RESOLVE_MODE.DIRECT && row.channelMchNo) { - await tryRestoreDirectCandidates(row.channelMchNo, row.capability); + await loadCapabilityByChannelMch(row.channelMchNo); } } finally { confirmLoading.value = false; } } - /** - * 编辑 DIRECT 项时尝试用 batch 候选还原 filterMethod / 选项 - */ - async function tryRestoreDirectCandidates(channelMchNo: string, capability?: string) { - const { data } = await PayRouteApi.listSceneChannelMchCandidatesBatch({ - appId: context.value.appId, - }); - if (!data) { - return; - } - for (const [key, list] of Object.entries(data)) { - const hit = (list || []).some((item) => item.value === channelMchNo); - if (!hit) { - continue; - } - // key = provider|method - const parts = key.split('|'); - const method = parts.length >= 2 ? parts[1]! : ''; - if (method) { - filterMethod.value = method; - await loadChannelMchOptions(method); - if (channelMchNo) { - formState.value.channelMchNo = channelMchNo; - await loadCapabilityOptions(method, channelMchNo); - formState.value.capability = capability; - } - return; - } - } - } - /** 保存 */ async function handleOk() { try { @@ -322,15 +355,13 @@ + + + + + {{ + $t('payment.merchant.cashier.cashier.recommendNo') + }} + {{ + $t('payment.merchant.cashier.cashier.recommendYes') + }} + + + + + + + + + + + - + + + + + diff --git a/apps/daxpay-admin/src/views/payment/merchant/cashier/shared/constants.ts b/apps/daxpay-admin/src/views/payment/merchant/cashier/shared/constants.ts index d7702d6e3..af407681c 100644 --- a/apps/daxpay-admin/src/views/payment/merchant/cashier/shared/constants.ts +++ b/apps/daxpay-admin/src/views/payment/merchant/cashier/shared/constants.ts @@ -33,11 +33,16 @@ export const CASHIER_H5_SCENES: CashierSceneConfig[] = [ { scene: 'douyin', provider: 'douyin' }, ]; -/** 图标选项 */ +/** + * 图标选项(值与 PayProviderEnum.code 完全对齐) + * 选支付方式/通道商户时 icon = provider 直接赋值,实现联动 + */ export const CASHIER_ICON_OPTIONS = [ + { value: 'aggregate_pay', labelKey: 'payment.merchant.cashier.cashier.icons.aggregate_pay' }, { value: 'wechat', labelKey: 'payment.merchant.cashier.cashier.icons.wechat' }, { value: 'alipay', labelKey: 'payment.merchant.cashier.cashier.icons.alipay' }, - { value: 'union', labelKey: 'payment.merchant.cashier.cashier.icons.union' }, + { value: 'union_pay', labelKey: 'payment.merchant.cashier.cashier.icons.union_pay' }, + { value: 'visa', labelKey: 'payment.merchant.cashier.cashier.icons.visa' }, + { value: 'mastercard', labelKey: 'payment.merchant.cashier.cashier.icons.mastercard' }, { value: 'douyin', labelKey: 'payment.merchant.cashier.cashier.icons.douyin' }, - { value: 'aggregate', labelKey: 'payment.merchant.cashier.cashier.icons.aggregate' }, ] as const;