mirror of
https://gitee.com/bootx/dax-pay-ui
synced 2026-08-11 23:25:33 +08:00
feat(payment): 通道商户下拉显示支付产品图标
- 新增 ChannelMchOption 类型(继承 LabelValue, 扩展 product/channel/ channelMchNo/channelMerchantName), 与后端 DTO 对齐 - 新建 ChannelMerchantSelect 共享组件, 下拉项与选中态均渲染 ChannelLogo 产品图标, 解决多通道商户同名歧义 - optionRender 通过 option.data 读取业务字段(antdv-next 包装类型) - 3 个 API 文件返回类型升级为 ChannelMchOption[] / Record<string, ChannelMchOption[]> - 5 个页面替换为 ChannelMerchantSelect: CashierItemEdit / AggregateScanConfig / CodePayConfig / DevelopTrade / ChannelAuth - PayRouteScenePanel 保持纯文本(表头已有通道图标且按 provider 分组)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { PayProviderMethod } from '#/api/payment/masterdata/provider.api';
|
||||
import type { LabelValue, Result } from '#/types/web';
|
||||
import type { ChannelMchOption, LabelValue, Result } from '#/types/web';
|
||||
|
||||
import { defHttp } from '#/api/request';
|
||||
|
||||
@@ -26,7 +26,7 @@ export const DevelopTradeApi = {
|
||||
/**
|
||||
* 直接指定: 按商户号筛选通道商户候选, provider 非空时仅返回声明支持该支付渠道的通道商户
|
||||
*/
|
||||
channelMchCandidates(mchNo: string, provider?: string): Promise<Result<LabelValue[]>> {
|
||||
channelMchCandidates(mchNo: string, provider?: string): Promise<Result<ChannelMchOption[]>> {
|
||||
return defHttp.get({
|
||||
url: '/admin/develop/trade/channel-mch-candidates',
|
||||
params: { mchNo, provider },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BaseEntity, LabelValue, Result } from '#/types/web';
|
||||
import type { BaseEntity, ChannelMchOption, LabelValue, Result } from '#/types/web';
|
||||
|
||||
import { defHttp } from '#/api/request';
|
||||
|
||||
@@ -26,10 +26,7 @@ export const CodeConfigApi = {
|
||||
/**
|
||||
* DIRECT: 按商户+支付渠道列通道商户候选(不绑默认 JSAPI method)
|
||||
*/
|
||||
listDirectChannelMchCandidates(params: {
|
||||
mchNo: string
|
||||
provider: string
|
||||
}): Promise<Result<LabelValue[]>> {
|
||||
listDirectChannelMchCandidates(params: { mchNo: string; provider: string }): Promise<Result<ChannelMchOption[]>> {
|
||||
return defHttp.get({
|
||||
url: '/admin/gateway/code-config/direct-channel-mch-candidates',
|
||||
params,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PayProviderMethod } from '#/api/payment/masterdata/provider.api';
|
||||
import type { BaseEntity, LabelValue, Result } from '#/types/web';
|
||||
import type { BaseEntity, ChannelMchOption, LabelValue, Result } from '#/types/web';
|
||||
|
||||
import { defHttp } from '#/api/request';
|
||||
|
||||
@@ -31,7 +31,7 @@ export const PayRouteApi = {
|
||||
},
|
||||
|
||||
/** 通道路由白名单目录下全部 (provider|method) 通道商户候选 */
|
||||
listSceneChannelMchCandidatesBatch(params: { appId: string }): Promise<Result<Record<string, LabelValue[]>>> {
|
||||
listSceneChannelMchCandidatesBatch(params: { appId: string }): Promise<Result<Record<string, ChannelMchOption[]>>> {
|
||||
return defHttp.get({
|
||||
url: '/admin/merchant/pay-route/scene-config/channel-mch-candidates-batch',
|
||||
params,
|
||||
@@ -39,7 +39,7 @@ export const PayRouteApi = {
|
||||
},
|
||||
|
||||
/** 目录项下商户已开通的通道商户候选 */
|
||||
listSceneChannelMchCandidates(params: PayRouteSceneChannelMchCandidatesQuery): Promise<Result<LabelValue[]>> {
|
||||
listSceneChannelMchCandidates(params: PayRouteSceneChannelMchCandidatesQuery): Promise<Result<ChannelMchOption[]>> {
|
||||
return defHttp.get({
|
||||
url: '/admin/merchant/pay-route/scene-config/channel-mch-candidates',
|
||||
params,
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ChannelMchOption } from '#/types/web';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import ChannelLogo from './ChannelLogo.vue';
|
||||
|
||||
/**
|
||||
* 通道商户下拉选择
|
||||
*
|
||||
* - 选项数据由调用方通过 `options` 传入(类型 [ChannelMchOption], 含 product/channel 字段)
|
||||
* - 下拉项与选中态都会渲染支付产品图标(通过 [ChannelLogo] 派生)
|
||||
* - 当多个通道商户名重复时, 图标可辅助区分
|
||||
*/
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
allowClear?: boolean;
|
||||
disabled?: boolean;
|
||||
/** 通道商户候选, 携带 product/channel 用于显示图标 */
|
||||
options?: ChannelMchOption[];
|
||||
placeholder?: string;
|
||||
/** 自定义下拉尺寸的 class */
|
||||
rootClassName?: string;
|
||||
/** 是否启用搜索(默认开启) */
|
||||
showSearch?: boolean;
|
||||
/** 当前选中的通道商户号 */
|
||||
value?: string;
|
||||
}>(),
|
||||
{
|
||||
value: '',
|
||||
options: () => [],
|
||||
placeholder: '',
|
||||
disabled: false,
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
rootClassName: '',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', val: string | undefined): void;
|
||||
(e: 'change', val: string | undefined, option?: ChannelMchOption): void;
|
||||
}>();
|
||||
|
||||
const innerOptions = computed(() => props.options);
|
||||
|
||||
// 选中态根据 value 查回 option 拿 product/channel 渲染图标
|
||||
const selectedOption = computed<ChannelMchOption | undefined>(() =>
|
||||
innerOptions.value.find((o) => o.value === props.value),
|
||||
);
|
||||
|
||||
function onChange(val: string | undefined, option: ChannelMchOption | undefined) {
|
||||
emit('update:value', val);
|
||||
emit('change', val, option);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-select
|
||||
:value="value"
|
||||
:options="innerOptions"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:allow-clear="allowClear"
|
||||
:show-search="showSearch"
|
||||
:class="rootClassName"
|
||||
option-filter-prop="label"
|
||||
option-label-prop="label"
|
||||
@change="onChange"
|
||||
>
|
||||
<template #optionRender="{ option }">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 支付产品图标(优先产品级, 回退到通道级) -->
|
||||
<ChannelLogo
|
||||
:product="(option.data as ChannelMchOption).product"
|
||||
:channel="(option.data as ChannelMchOption).channel"
|
||||
:size="18"
|
||||
/>
|
||||
<span>{{ option.data.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #labelRender>
|
||||
<div class="inline-flex items-center gap-1">
|
||||
<ChannelLogo
|
||||
v-if="selectedOption"
|
||||
:product="selectedOption.product"
|
||||
:channel="selectedOption.channel"
|
||||
:size="14"
|
||||
/>
|
||||
<span>{{ selectedOption?.label ?? value }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-select>
|
||||
</template>
|
||||
14
apps/daxpay-admin/src/types/web.d.ts
vendored
14
apps/daxpay-admin/src/types/web.d.ts
vendored
@@ -83,6 +83,20 @@ export interface LabelValue {
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通道商户下拉选项(扩展 LabelValue, 携带通道/产品编码用于展示支付产品图标)
|
||||
*/
|
||||
export interface ChannelMchOption extends LabelValue {
|
||||
/** 通道商户号(等于 value) */
|
||||
channelMchNo: string;
|
||||
/** 通道商户名称(空时回退到 channelMchNo) */
|
||||
channelMerchantName?: string;
|
||||
/** 所属支付通道编码(如 wechat / alipay) */
|
||||
channel?: string;
|
||||
/** 所属支付产品编码(如 wechat_pay / lakala_pay), 优先用于匹配产品图标 */
|
||||
product?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页表格列表对象
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { AuthResult, AuthUrlResult, ChannelAuthUrlParam } from '#/api/payment/develop/developAuth.api';
|
||||
import type { LabelValue } from '#/types/web';
|
||||
import type { ChannelMchOption, LabelValue } from '#/types/web';
|
||||
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import { DevelopAuthApi } from '#/api/payment/develop/developAuth.api';
|
||||
import { DevelopTradeApi } from '#/api/payment/develop/developTrade.api';
|
||||
import { MerchantApi } from '#/api/payment/merchant/merchant.api';
|
||||
import ChannelMerchantSelect from '#/components/channel/ChannelMerchantSelect.vue';
|
||||
import { QrCode } from '#/components/qrcode';
|
||||
import { useMessage } from '#/hooks/useMessage';
|
||||
|
||||
@@ -26,7 +27,7 @@
|
||||
} as const;
|
||||
|
||||
/** 认证类型 */
|
||||
type AuthType = 'alipay' | 'alipayMini' | 'wechatChannel' | 'wechatMini' | 'wechatMp' | 'douyin';
|
||||
type AuthType = 'alipay' | 'alipayMini' | 'douyin' | 'wechatChannel' | 'wechatMini' | 'wechatMp';
|
||||
const authType = ref<AuthType>('alipay');
|
||||
|
||||
const { message } = useMessage();
|
||||
@@ -55,7 +56,7 @@
|
||||
|
||||
/** 下拉选项 */
|
||||
const mchNoOptions = ref<LabelValue[]>([]);
|
||||
const channelMchNoOptions = ref<LabelValue[]>([]);
|
||||
const channelMchNoOptions = ref<ChannelMchOption[]>([]);
|
||||
const capabilityOptions = ref<LabelValue[]>([]);
|
||||
|
||||
/** 微信小程序端类型选项 */
|
||||
@@ -207,12 +208,12 @@
|
||||
: authType.value === 'douyin'
|
||||
? DevelopAuthApi.generateDouyinAuthUrl()
|
||||
: DevelopAuthApi.generateChannelAuthUrl({
|
||||
channel: 'wechat',
|
||||
authType: 'wechat',
|
||||
mchNo: form.mchNo,
|
||||
channelMchNo: form.channelMchNo,
|
||||
capability: form.capability,
|
||||
} as ChannelAuthUrlParam);
|
||||
channel: 'wechat',
|
||||
authType: 'wechat',
|
||||
mchNo: form.mchNo,
|
||||
channelMchNo: form.channelMchNo,
|
||||
capability: form.capability,
|
||||
} as ChannelAuthUrlParam);
|
||||
const { data } = await promise;
|
||||
authUrl.value = data ?? {};
|
||||
if (data?.queryCode) {
|
||||
@@ -248,7 +249,15 @@
|
||||
<template #extra>
|
||||
<a-space :size="8">
|
||||
<a-tag color="blue">OAuth2.0</a-tag>
|
||||
<a-tag :color="authType === 'alipay' || authType === 'alipayMini' ? 'processing' : authType === 'douyin' ? 'black' : 'green'">
|
||||
<a-tag
|
||||
:color="
|
||||
authType === 'alipay' || authType === 'alipayMini'
|
||||
? 'processing'
|
||||
: authType === 'douyin'
|
||||
? 'black'
|
||||
: 'green'
|
||||
"
|
||||
>
|
||||
{{ tagLabel }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
@@ -303,13 +312,7 @@
|
||||
|
||||
<!-- 微信支付:商户参数表单 -->
|
||||
<div v-if="authType === 'wechatChannel'" class="form-panel">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
layout="vertical"
|
||||
class="channel-auth-form"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
>
|
||||
<a-form ref="formRef" layout="vertical" class="channel-auth-form" :model="form" :rules="formRules">
|
||||
<a-form-item :label="$t('payment.develop.auth.form.mchNo')" name="mchNo">
|
||||
<a-select
|
||||
v-model:value="form.mchNo"
|
||||
@@ -322,17 +325,18 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('payment.develop.auth.form.channelMchNo')" name="channelMchNo">
|
||||
<a-select
|
||||
<ChannelMerchantSelect
|
||||
v-model:value="form.channelMchNo"
|
||||
:options="channelMchNoOptions"
|
||||
:placeholder="$t('payment.develop.auth.form.rule.channelMchNo')"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
allow-clear
|
||||
@change="channelMchNoChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('payment.develop.auth.form.capability')" name="capability" class="form-item-last">
|
||||
<a-form-item
|
||||
:label="$t('payment.develop.auth.form.capability')"
|
||||
name="capability"
|
||||
class="form-item-last"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="form.capability"
|
||||
:options="capabilityOptions"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import type { PayParam, PayResult } from '#/api/payment/develop/developTrade.api';
|
||||
import type { DaxResult } from '#/api/payment/unipay/unipay-request';
|
||||
import type { LabelValue } from '#/types/web';
|
||||
import type { ChannelMchOption, LabelValue } from '#/types/web';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { MchAppInfoApi } from '#/api/payment/merchant/mch-app-info.api';
|
||||
import { MerchantApi } from '#/api/payment/merchant/merchant.api';
|
||||
import { uniPay } from '#/api/payment/unipay/unipay-trade.api';
|
||||
import ChannelMerchantSelect from '#/components/channel/ChannelMerchantSelect.vue';
|
||||
import { QrCode } from '#/components/qrcode';
|
||||
import { useMessage } from '#/hooks/useMessage';
|
||||
|
||||
@@ -51,7 +52,7 @@
|
||||
{
|
||||
validator: async () => {
|
||||
if (!privateKey.value) {
|
||||
return Promise.reject(new Error($t('payment.develop.trade.msg.inputPrivateKey')));
|
||||
throw new Error($t('payment.develop.trade.msg.inputPrivateKey'));
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -86,7 +87,7 @@
|
||||
const mchNoOptions = ref<LabelValue[]>([]);
|
||||
const mchAppOptions = ref<LabelValue[]>([]);
|
||||
const methodOptions = ref<LabelValue[]>([]);
|
||||
const channelMchNoOptions = ref<LabelValue[]>([]);
|
||||
const channelMchNoOptions = ref<ChannelMchOption[]>([]);
|
||||
const capabilityOptions = ref<LabelValue[]>([]);
|
||||
|
||||
// ===== 调试结果(完整 unipay DaxResult) =====
|
||||
@@ -552,13 +553,10 @@
|
||||
<!-- 直接指定: 通道商户(必填) -->
|
||||
<a-col v-if="routeMode === 'direct'" :span="8">
|
||||
<a-form-item :label="$t('payment.develop.trade.field.channelMchNo')" name="channelMchNo">
|
||||
<a-select
|
||||
<ChannelMerchantSelect
|
||||
v-model:value="form.channelMchNo"
|
||||
show-search
|
||||
:options="channelMchNoOptions"
|
||||
:placeholder="$t('payment.develop.trade.field.channelMchNo')"
|
||||
:filter-option="filterOption"
|
||||
allow-clear
|
||||
@change="channelMchNoChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
@@ -789,13 +787,9 @@
|
||||
>
|
||||
<!-- 响应摘要 -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-2">
|
||||
<a-tag :color="resultData.code === 0 ? 'success' : 'error'">
|
||||
code: {{ resultData.code }}
|
||||
</a-tag>
|
||||
<a-tag :color="resultData.code === 0 ? 'success' : 'error'"> code: {{ resultData.code }} </a-tag>
|
||||
<span class="text-sm text-muted-foreground">{{ resultData.msg }}</span>
|
||||
<span v-if="resultData.traceId" class="text-xs text-muted-foreground">
|
||||
traceId: {{ resultData.traceId }}
|
||||
</span>
|
||||
<span v-if="resultData.traceId" class="text-xs text-muted-foreground"> traceId: {{ resultData.traceId }} </span>
|
||||
</div>
|
||||
|
||||
<template v-if="hasPayBody">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LabelValue } from '#/types/web';
|
||||
import type { ChannelMchOption, LabelValue } from '#/types/web';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
@@ -8,22 +8,23 @@
|
||||
|
||||
import { IconifyIcon } from '@vben-core/icons';
|
||||
|
||||
import { MchAppInfoApi, type MchAppInfoResult } from '#/api/payment/merchant/mch-app-info.api';
|
||||
import {
|
||||
type AggregateClientEnvParam,
|
||||
AggregateConfigApi,
|
||||
type AggregateConfigResult,
|
||||
type AggregateClientEnvParam,
|
||||
} from '#/api/payment/merchant/aggregate.api';
|
||||
import { MchAppInfoApi, type MchAppInfoResult } from '#/api/payment/merchant/mch-app-info.api';
|
||||
import { PayRouteApi } from '#/api/payment/route/pay-route.api';
|
||||
import ChannelMerchantSelect from '#/components/channel/ChannelMerchantSelect.vue';
|
||||
import RouteQueryMissingState from '#/components/route/RouteQueryMissingState.vue';
|
||||
import { useMessage } from '#/hooks/useMessage';
|
||||
import { normalizeRouteQueryValue, useRequiredRouteQuery } from '#/hooks/useRequiredRouteQuery';
|
||||
import { PAY_ROUTE_MODE } from '#/views/payment/merchant/route/shared/payRoute.constants';
|
||||
import { modeDisplayName } from '#/views/payment/merchant/route/shared/payRoute.labels';
|
||||
import RouteHitPreviewBlock from '#/views/payment/merchant/shared/RouteHitPreviewBlock.vue';
|
||||
import { useRouteHitPreview } from '#/views/payment/merchant/shared/useRouteHitPreview';
|
||||
import { modeDisplayName } from '#/views/payment/merchant/route/shared/payRoute.labels';
|
||||
import { PAY_ROUTE_MODE } from '#/views/payment/merchant/route/shared/payRoute.constants';
|
||||
|
||||
import { AGGREGATE_LEVEL, AGGREGATE_CLIENT_ENVS, type AggregateLevel } from './shared/constants';
|
||||
import { AGGREGATE_CLIENT_ENVS, AGGREGATE_LEVEL, type AggregateLevel } from './shared/constants';
|
||||
|
||||
defineOptions({ name: 'AggregateScanConfig' });
|
||||
|
||||
@@ -66,7 +67,7 @@
|
||||
|
||||
// 候选数据
|
||||
const methodDirectory = ref<Record<string, LabelValue[]>>({}); // provider → 方式列表
|
||||
const channelMchMap = ref<Record<string, LabelValue[]>>({}); // clientEnv → 通道商户列表
|
||||
const channelMchMap = ref<Record<string, ChannelMchOption[]>>({}); // clientEnv → 通道商户列表
|
||||
const capabilityMap = ref<Record<string, LabelValue[]>>({}); // clientEnv → 能力列表
|
||||
|
||||
// 通道路由命中预览(与路由页同源;解构以便模板自动解包 ref)
|
||||
@@ -187,7 +188,7 @@
|
||||
|
||||
/** 加载通道商户候选(DIRECT 模式用) */
|
||||
async function loadChannelMchCandidates() {
|
||||
const map: Record<string, LabelValue[]> = {};
|
||||
const map: Record<string, ChannelMchOption[]> = {};
|
||||
await Promise.all(
|
||||
AGGREGATE_CLIENT_ENVS.map(async (sc) => {
|
||||
const { data } = await PayRouteApi.listSceneChannelMchCandidates({
|
||||
@@ -298,9 +299,7 @@
|
||||
}
|
||||
if (!hasMch || !hasCap) {
|
||||
message.error(
|
||||
$t('payment.merchant.aggregate.aggregate.partialRowIncomplete') +
|
||||
': ' +
|
||||
clientEnvLabel(sc.clientEnv),
|
||||
$t('payment.merchant.aggregate.aggregate.partialRowIncomplete') + ': ' + clientEnvLabel(sc.clientEnv),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -391,9 +390,7 @@
|
||||
</template>
|
||||
</a-button>
|
||||
<span class="text-lg font-bold">{{ $t('payment.merchant.aggregate.aggregate.title') }}</span>
|
||||
<span v-if="appInfo.appName" class="text-sm text-muted-foreground">
|
||||
({{ appInfo.appName }})
|
||||
</span>
|
||||
<span v-if="appInfo.appName" class="text-sm text-muted-foreground"> ({{ appInfo.appName }}) </span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -468,11 +465,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 数据行 -->
|
||||
<div
|
||||
v-for="sc in AGGREGATE_CLIENT_ENVS"
|
||||
:key="sc.clientEnv"
|
||||
class="env-grid-row"
|
||||
>
|
||||
<div v-for="sc in AGGREGATE_CLIENT_ENVS" :key="sc.clientEnv" class="env-grid-row">
|
||||
<div class="cell-env font-medium">{{ clientEnvLabel(sc.clientEnv) }}</div>
|
||||
|
||||
<!-- AUTO:只读支付方式 + 路由预览 -->
|
||||
@@ -508,13 +501,12 @@
|
||||
<!-- DIRECT:通道商户 + 能力(跳过路由) -->
|
||||
<template v-else>
|
||||
<div>
|
||||
<a-select
|
||||
<ChannelMerchantSelect
|
||||
:value="getClientEnvData(sc.clientEnv).channelMchNo"
|
||||
:options="channelMchOptions(sc.clientEnv)"
|
||||
:placeholder="$t('payment.merchant.aggregate.aggregate.channelMerchantPlaceholder')"
|
||||
:disabled="!editing"
|
||||
allow-clear
|
||||
class="w-full min-w-[160px]"
|
||||
root-class-name="w-full min-w-[160px]"
|
||||
@change="(val: any) => onChannelMchChange(sc.clientEnv, val)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LabelValue } from '#/types/web';
|
||||
import type { ChannelMchOption, LabelValue } from '#/types/web';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
CashierConfigApi,
|
||||
type CashierItemParam,
|
||||
type CashierItemResult,
|
||||
} from '#/api/payment/merchant/cashier.api';
|
||||
import { CashierConfigApi, type CashierItemParam, type CashierItemResult } from '#/api/payment/merchant/cashier.api';
|
||||
import { PayRouteApi } from '#/api/payment/route/pay-route.api';
|
||||
import { FormEditType } from '#/enums/formEditType';
|
||||
import { useFormEdit } from '#/hooks/useFormEdit';
|
||||
import { useMessage } from '#/hooks/useMessage';
|
||||
import { getProviderSvgUrl } from '#/views/payment/shared/payProviderDisplay';
|
||||
import ChannelMerchantSelect from '#/components/channel/ChannelMerchantSelect.vue';
|
||||
import { FormEditType } from '#/enums/formEditType';
|
||||
import { useFormEdit } from '#/hooks/useFormEdit';
|
||||
import { useMessage } from '#/hooks/useMessage';
|
||||
import { getProviderSvgUrl } from '#/views/payment/shared/payProviderDisplay';
|
||||
|
||||
import {
|
||||
CASHIER_ICON_OPTIONS,
|
||||
CASHIER_TYPE,
|
||||
RESOLVE_MODE,
|
||||
cashierTypeRequiresClientEnv,
|
||||
type CashierType,
|
||||
cashierTypeRequiresClientEnv,
|
||||
RESOLVE_MODE,
|
||||
} from './shared/constants';
|
||||
|
||||
const emit = defineEmits<{ ok: [] }>();
|
||||
@@ -32,10 +29,10 @@
|
||||
const { visible, confirmLoading, title, initFormEditType, handleCancel, formEditType, showable } = useFormEdit();
|
||||
|
||||
const context = ref<{
|
||||
mchNo: string;
|
||||
appId: string;
|
||||
cashierType: CashierType;
|
||||
clientEnv?: string;
|
||||
mchNo: string;
|
||||
}>({
|
||||
mchNo: '',
|
||||
appId: '',
|
||||
@@ -43,7 +40,7 @@
|
||||
});
|
||||
|
||||
// DIRECT 模式:通道商户扁平选项(去重)+ 商户→method 组合映射 + 能力→provider 映射(图标联动)
|
||||
const channelMchOptions = ref<LabelValue[]>([]);
|
||||
const channelMchOptions = ref<ChannelMchOption[]>([]);
|
||||
const channelMchCombosMap = ref<Record<string, Array<{ method: string; provider: string }>>>({});
|
||||
const capabilityProviderMap = ref<Record<string, string>>({});
|
||||
|
||||
@@ -78,12 +75,8 @@
|
||||
if (formState.value.resolveMode === RESOLVE_MODE.METHOD) {
|
||||
rules.method = [{ required: true, message: $t('payment.merchant.cashier.cashier.validationMethod') }];
|
||||
} else {
|
||||
rules.channelMchNo = [
|
||||
{ required: true, message: $t('payment.merchant.cashier.cashier.validationChannelMch') },
|
||||
];
|
||||
rules.capability = [
|
||||
{ required: true, message: $t('payment.merchant.cashier.cashier.validationCapability') },
|
||||
];
|
||||
rules.channelMchNo = [{ required: true, message: $t('payment.merchant.cashier.cashier.validationChannelMch') }];
|
||||
rules.capability = [{ required: true, message: $t('payment.merchant.cashier.cashier.validationCapability') }];
|
||||
}
|
||||
return rules;
|
||||
});
|
||||
@@ -94,9 +87,7 @@
|
||||
mchNo: context.value.mchNo,
|
||||
appId: context.value.appId,
|
||||
cashierType: context.value.cashierType,
|
||||
clientEnv: cashierTypeRequiresClientEnv(context.value.cashierType)
|
||||
? context.value.clientEnv
|
||||
: undefined,
|
||||
clientEnv: cashierTypeRequiresClientEnv(context.value.cashierType) ? context.value.clientEnv : undefined,
|
||||
name: '',
|
||||
icon: undefined,
|
||||
recommend: false,
|
||||
@@ -145,7 +136,7 @@
|
||||
return;
|
||||
}
|
||||
// 按 channelMchNo 去重,同时收集每商户的全部 provider|method 组合
|
||||
const labelMap = new Map<string, string>();
|
||||
const optionMap = new Map<string, ChannelMchOption>();
|
||||
const combosMap: Record<string, Array<{ method: string; provider: string }>> = {};
|
||||
for (const [key, list] of Object.entries(data)) {
|
||||
const parts = key.split('|');
|
||||
@@ -155,17 +146,14 @@
|
||||
continue;
|
||||
}
|
||||
for (const item of list || []) {
|
||||
labelMap.set(item.value, item.label);
|
||||
optionMap.set(item.value, item);
|
||||
if (!combosMap[item.value]) {
|
||||
combosMap[item.value] = [];
|
||||
}
|
||||
combosMap[item.value]!.push({ method, provider });
|
||||
}
|
||||
}
|
||||
channelMchOptions.value = [...labelMap.entries()].map(([value, label]) => ({
|
||||
label,
|
||||
value,
|
||||
}));
|
||||
channelMchOptions.value = [...optionMap.values()];
|
||||
channelMchCombosMap.value = combosMap;
|
||||
}
|
||||
|
||||
@@ -203,8 +191,7 @@
|
||||
}
|
||||
capabilityProviderMap.value = capProviderMap;
|
||||
// label 统一用 i18n 翻译
|
||||
const toLabel = (code: string) =>
|
||||
$t(`payment.merchant.cashier.cashier.capabilities.${code}`) || code;
|
||||
const toLabel = (code: string) => $t(`payment.merchant.cashier.cashier.capabilities.${code}`) || code;
|
||||
let options = [...capLabelMap.keys()].map((value) => ({
|
||||
label: toLabel(value),
|
||||
value,
|
||||
@@ -257,12 +244,7 @@
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
async function show(opts: {
|
||||
mchNo: string;
|
||||
appId: string;
|
||||
cashierType: CashierType;
|
||||
clientEnv?: string;
|
||||
}) {
|
||||
async function show(opts: { appId: string; cashierType: CashierType; clientEnv?: string; mchNo: string }) {
|
||||
context.value = { ...opts };
|
||||
initFormEditType(FormEditType.Add);
|
||||
resetForm();
|
||||
@@ -272,10 +254,10 @@
|
||||
|
||||
/** 编辑 */
|
||||
async function showEdit(opts: {
|
||||
mchNo: string;
|
||||
appId: string;
|
||||
cashierType: CashierType;
|
||||
clientEnv?: string;
|
||||
mchNo: string;
|
||||
record: CashierItemResult;
|
||||
}) {
|
||||
context.value = {
|
||||
@@ -330,9 +312,7 @@
|
||||
mchNo: context.value.mchNo,
|
||||
appId: context.value.appId,
|
||||
cashierType: context.value.cashierType,
|
||||
clientEnv: cashierTypeRequiresClientEnv(context.value.cashierType)
|
||||
? context.value.clientEnv
|
||||
: undefined,
|
||||
clientEnv: cashierTypeRequiresClientEnv(context.value.cashierType) ? context.value.clientEnv : undefined,
|
||||
recommend: !!formState.value.recommend,
|
||||
sortNo: formState.value.sortNo ?? 0,
|
||||
};
|
||||
@@ -343,11 +323,9 @@
|
||||
payload.channelMchNo = undefined;
|
||||
payload.capability = undefined;
|
||||
}
|
||||
if (formEditType.value === FormEditType.Edit) {
|
||||
await CashierConfigApi.update(payload);
|
||||
} else {
|
||||
await CashierConfigApi.save(payload);
|
||||
}
|
||||
await (formEditType.value === FormEditType.Edit
|
||||
? CashierConfigApi.update(payload)
|
||||
: CashierConfigApi.save(payload));
|
||||
message.success($t('common.operationSuccess'));
|
||||
handleCancel();
|
||||
emit('ok');
|
||||
@@ -369,13 +347,7 @@
|
||||
@close="handleCancel"
|
||||
>
|
||||
<a-spin :spinning="confirmLoading">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
class="pt-2"
|
||||
>
|
||||
<a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical" class="pt-2">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$t('payment.merchant.cashier.cashier.name')" name="name">
|
||||
@@ -432,16 +404,11 @@
|
||||
|
||||
<template v-else>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
:label="$t('payment.merchant.cashier.cashier.channelMerchant')"
|
||||
name="channelMchNo"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="formState.channelMchNo"
|
||||
:disabled="showable"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
<a-form-item :label="$t('payment.merchant.cashier.cashier.channelMerchant')" name="channelMchNo">
|
||||
<ChannelMerchantSelect
|
||||
:value="formState.channelMchNo"
|
||||
:options="channelMchOptions"
|
||||
:disabled="showable"
|
||||
:placeholder="$t('payment.merchant.cashier.cashier.channelMerchantPlaceholder')"
|
||||
@change="onChannelMchChange"
|
||||
/>
|
||||
@@ -465,12 +432,8 @@
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="$t('payment.merchant.cashier.cashier.recommend')" name="recommend">
|
||||
<a-radio-group v-model:value="formState.recommend" button-style="solid" :disabled="showable">
|
||||
<a-radio-button :value="false">{{
|
||||
$t('payment.merchant.cashier.cashier.recommendNo')
|
||||
}}</a-radio-button>
|
||||
<a-radio-button :value="true">{{
|
||||
$t('payment.merchant.cashier.cashier.recommendYes')
|
||||
}}</a-radio-button>
|
||||
<a-radio-button :value="false">{{ $t('payment.merchant.cashier.cashier.recommendNo') }}</a-radio-button>
|
||||
<a-radio-button :value="true">{{ $t('payment.merchant.cashier.cashier.recommendYes') }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LabelValue } from '#/types/web';
|
||||
import type { ChannelMchOption, LabelValue } from '#/types/web';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
@@ -8,26 +8,27 @@
|
||||
|
||||
import { IconifyIcon } from '@vben-core/icons';
|
||||
|
||||
import { MchAppInfoApi, type MchAppInfoResult } from '#/api/payment/merchant/mch-app-info.api';
|
||||
import {
|
||||
type CodeClientEnvParam,
|
||||
CodeConfigApi,
|
||||
type CodeConfigResult,
|
||||
type CodeClientEnvParam,
|
||||
} from '#/api/payment/merchant/code-config.api';
|
||||
import { MchAppInfoApi, type MchAppInfoResult } from '#/api/payment/merchant/mch-app-info.api';
|
||||
import { PayRouteApi } from '#/api/payment/route/pay-route.api';
|
||||
import ChannelMerchantSelect from '#/components/channel/ChannelMerchantSelect.vue';
|
||||
import RouteQueryMissingState from '#/components/route/RouteQueryMissingState.vue';
|
||||
import { useMessage } from '#/hooks/useMessage';
|
||||
import { normalizeRouteQueryValue, useRequiredRouteQuery } from '#/hooks/useRequiredRouteQuery';
|
||||
import { PAY_ROUTE_MODE } from '#/views/payment/merchant/route/shared/payRoute.constants';
|
||||
import { modeDisplayName } from '#/views/payment/merchant/route/shared/payRoute.labels';
|
||||
import RouteHitPreviewBlock from '#/views/payment/merchant/shared/RouteHitPreviewBlock.vue';
|
||||
import { useRouteHitPreview } from '#/views/payment/merchant/shared/useRouteHitPreview';
|
||||
import { modeDisplayName } from '#/views/payment/merchant/route/shared/payRoute.labels';
|
||||
import { PAY_ROUTE_MODE } from '#/views/payment/merchant/route/shared/payRoute.constants';
|
||||
|
||||
import {
|
||||
CODE_LEVEL,
|
||||
CODE_CLIENT_ENVS,
|
||||
CODE_PAY_FORMS,
|
||||
CODE_LEVEL,
|
||||
CODE_PAY_FORM,
|
||||
CODE_PAY_FORMS,
|
||||
type CodeLevel,
|
||||
type CodePayForm,
|
||||
defaultMethodFor,
|
||||
@@ -71,7 +72,7 @@
|
||||
const clientEnvForm = ref<Record<string, CodeClientEnvParam>>({});
|
||||
|
||||
const methodDirectory = ref<Record<string, LabelValue[]>>({});
|
||||
const channelMchMap = ref<Record<string, LabelValue[]>>({});
|
||||
const channelMchMap = ref<Record<string, ChannelMchOption[]>>({});
|
||||
const capabilityMap = ref<Record<string, LabelValue[]>>({});
|
||||
|
||||
// 通道路由命中预览(与路由页同源;解构以便模板自动解包 ref)
|
||||
@@ -86,9 +87,7 @@
|
||||
const effectiveLevel = computed(() => config.value.level || CODE_LEVEL.AUTO);
|
||||
const isLevelActive = computed(() => editLevel.value === effectiveLevel.value);
|
||||
|
||||
const showRoutePreview = computed(
|
||||
() => editLevel.value === CODE_LEVEL.AUTO || editLevel.value === CODE_LEVEL.METHOD,
|
||||
);
|
||||
const showRoutePreview = computed(() => editLevel.value === CODE_LEVEL.AUTO || editLevel.value === CODE_LEVEL.METHOD);
|
||||
|
||||
const modeHint = computed(() => {
|
||||
if (editLevel.value === CODE_LEVEL.AUTO) {
|
||||
@@ -124,9 +123,7 @@
|
||||
for (const sc of CODE_CLIENT_ENVS) {
|
||||
for (const pf of CODE_PAY_FORMS) {
|
||||
const key = rowKey(sc.clientEnv, pf);
|
||||
const serverEnv = config.value.clientEnvs?.find(
|
||||
(s) => s.clientEnv === sc.clientEnv && s.payForm === pf,
|
||||
);
|
||||
const serverEnv = config.value.clientEnvs?.find((s) => s.clientEnv === sc.clientEnv && s.payForm === pf);
|
||||
form[key] = {
|
||||
clientEnv: sc.clientEnv,
|
||||
payForm: pf,
|
||||
@@ -203,8 +200,8 @@
|
||||
*/
|
||||
type DisplayFormRow = {
|
||||
key: string;
|
||||
payForms: CodePayForm[];
|
||||
merged: boolean;
|
||||
payForms: CodePayForm[];
|
||||
/** 预览/展示用主形态(合并时取 H5) */
|
||||
primaryForm: CodePayForm;
|
||||
};
|
||||
@@ -233,11 +230,7 @@
|
||||
}
|
||||
|
||||
/** 未配置时:同环境另一形态已配置则强调告警,否则灰色降噪 */
|
||||
function emptyToneFor(
|
||||
provider: string,
|
||||
clientEnv: string,
|
||||
payForm: CodePayForm,
|
||||
): 'soft' | 'emphasize' {
|
||||
function emptyToneFor(provider: string, clientEnv: string, payForm: CodePayForm): 'emphasize' | 'soft' {
|
||||
const method = resolveMethodForRow(clientEnv, payForm);
|
||||
if (!method) {
|
||||
return 'soft';
|
||||
@@ -279,11 +272,11 @@
|
||||
const hit = previewRouteHit(sc.provider, method);
|
||||
if (hit.status === 'ok') {
|
||||
ok += 1;
|
||||
} else if (hit.status === 'notConfigured' || hit.status === 'noStrategy') {
|
||||
// 仅收集「同环境另一形态已配」的缺口,避免刷屏
|
||||
if (emptyToneFor(sc.provider, sc.clientEnv, pf) === 'emphasize') {
|
||||
gapLabels.push(`${clientEnvLabel(sc.clientEnv)} · ${payFormLabel(pf)}`);
|
||||
}
|
||||
} else if (
|
||||
(hit.status === 'notConfigured' || hit.status === 'noStrategy') && // 仅收集「同环境另一形态已配」的缺口,避免刷屏
|
||||
emptyToneFor(sc.provider, sc.clientEnv, pf) === 'emphasize'
|
||||
) {
|
||||
gapLabels.push(`${clientEnvLabel(sc.clientEnv)} · ${payFormLabel(pf)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +287,7 @@
|
||||
* DIRECT: 按商户+渠道列通道商户(不绑默认 JSAPI method,与路由直接指定一致)
|
||||
*/
|
||||
async function loadChannelMchCandidates() {
|
||||
const map: Record<string, LabelValue[]> = {};
|
||||
const map: Record<string, ChannelMchOption[]> = {};
|
||||
await Promise.all(
|
||||
CODE_CLIENT_ENVS.map(async (sc) => {
|
||||
const { data } = await CodeConfigApi.listDirectChannelMchCandidates({
|
||||
@@ -491,9 +484,7 @@
|
||||
</template>
|
||||
</a-button>
|
||||
<span class="text-lg font-bold">{{ $t('payment.merchant.codeConfig.codeConfig.title') }}</span>
|
||||
<span v-if="appInfo.appName" class="text-sm text-muted-foreground">
|
||||
({{ appInfo.appName }})
|
||||
</span>
|
||||
<span v-if="appInfo.appName" class="text-sm text-muted-foreground"> ({{ appInfo.appName }}) </span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -533,27 +524,24 @@
|
||||
</div>
|
||||
<!-- openId 风控与码牌支付方式关系说明 -->
|
||||
<div class="mb-4">
|
||||
<a-alert
|
||||
:message="$t('payment.merchant.codeConfig.codeConfig.openIdRiskHint')"
|
||||
type="warning"
|
||||
show-icon
|
||||
/>
|
||||
<a-alert :message="$t('payment.merchant.codeConfig.codeConfig.openIdRiskHint')" type="warning" show-icon />
|
||||
</div>
|
||||
|
||||
<div v-if="showRoutePreview" class="mb-5 flex flex-wrap items-center gap-3 text-sm">
|
||||
<span class="text-muted-foreground">{{ $t('payment.merchant.codeConfig.codeConfig.currentRouteMode') }}:</span>
|
||||
<span class="text-muted-foreground"
|
||||
>{{ $t('payment.merchant.codeConfig.codeConfig.currentRouteMode') }}:</span
|
||||
>
|
||||
<a-tag color="blue">{{ routeModeLabel }}</a-tag>
|
||||
<template v-if="routeCoverage">
|
||||
<span class="text-muted-foreground">
|
||||
{{ $t('payment.merchant.codeConfig.codeConfig.routeCoverage', {
|
||||
ok: routeCoverage.ok,
|
||||
total: routeCoverage.total,
|
||||
}) }}
|
||||
{{
|
||||
$t('payment.merchant.codeConfig.codeConfig.routeCoverage', {
|
||||
ok: routeCoverage.ok,
|
||||
total: routeCoverage.total,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
v-if="routeCoverage.gapLabels.length"
|
||||
class="text-xs text-orange-500"
|
||||
>
|
||||
<span v-if="routeCoverage.gapLabels.length > 0" class="text-xs text-orange-500">
|
||||
{{ $t('payment.merchant.codeConfig.codeConfig.routeGaps') }}:
|
||||
{{ routeCoverage.gapLabels.join(' · ') }}
|
||||
</span>
|
||||
@@ -577,11 +565,7 @@
|
||||
<div>{{ $t('payment.merchant.route.route.payCapability') }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="drow in displayFormRows(sc.clientEnv)"
|
||||
:key="drow.key"
|
||||
class="env-grid-row"
|
||||
>
|
||||
<div v-for="drow in displayFormRows(sc.clientEnv)" :key="drow.key" class="env-grid-row">
|
||||
<!-- 形态:合并时展示 H5 + 小程序 -->
|
||||
<div class="form-tags">
|
||||
<template v-if="drow.merged">
|
||||
@@ -589,11 +573,7 @@
|
||||
<span class="text-muted-foreground text-xs">/</span>
|
||||
<a-tag color="purple" class="!m-0">{{ payFormLabel(CODE_PAY_FORM.MINI) }}</a-tag>
|
||||
</template>
|
||||
<a-tag
|
||||
v-else
|
||||
:color="drow.primaryForm === CODE_PAY_FORM.MINI ? 'purple' : 'blue'"
|
||||
class="!m-0"
|
||||
>
|
||||
<a-tag v-else :color="drow.primaryForm === CODE_PAY_FORM.MINI ? 'purple' : 'blue'" class="!m-0">
|
||||
{{ payFormLabel(drow.primaryForm) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
@@ -636,13 +616,12 @@
|
||||
<!-- DIRECT -->
|
||||
<template v-else>
|
||||
<div>
|
||||
<a-select
|
||||
<ChannelMerchantSelect
|
||||
:value="getRow(sc.clientEnv, drow.primaryForm).channelMchNo"
|
||||
:options="channelMchOptions(sc.clientEnv, drow.primaryForm)"
|
||||
:placeholder="$t('payment.merchant.codeConfig.codeConfig.channelMerchantPlaceholder')"
|
||||
:disabled="!editing"
|
||||
allow-clear
|
||||
class="w-full min-w-[160px]"
|
||||
root-class-name="w-full min-w-[160px]"
|
||||
@change="(val: any) => onChannelMchChange(sc.clientEnv, drow.primaryForm, val)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user