refactor(admin,merchant): 通道商户路由参数精简, 冗余字段改接收端反查

URL 透传的 channelMerchantId/channelMerchantName/sandbox/product 改为
接收端用 findAll/findAllByMchNo 反查, 避免中文暴露、sandbox 快照过期、URL 冗余。
This commit is contained in:
DaxPay Dev
2026-07-25 10:43:22 +08:00
parent 0c63489aaa
commit bf6b3e7d52
12 changed files with 337 additions and 119 deletions

View File

@@ -109,12 +109,12 @@
return;
}
if (card.route) {
// 仅传 mchNo + channelMchNo, 接收端反查名称等元数据
router.push({
path: card.route,
query: {
mchNo: mchNo.value,
channelMchNo: channelMchNo.value,
channelMerchantId: channelMerchant.value.id,
},
});
}

View File

@@ -7,6 +7,7 @@
import { IconifyIcon } from '@vben-core/icons';
import { type AlipayMchApp, AlipayMchAppApi } from '#/api/payment/channel/alipay/mch-app.api';
import { ChannelMerchantApi } from '#/api/payment/global/channel-merchant/channel-merchant.api';
import RouteQueryMissingState from '#/components/route/RouteQueryMissingState.vue';
import { PermCodes } from '#/constants/perm-codes';
import { ProductEnum } from '#/enums/payment/productEnum';
@@ -33,12 +34,6 @@
}),
fallbackPath: computed(() => {
const mchNo = normalizeRouteQueryValue(route.query.mchNo);
const id = normalizeRouteQueryValue(route.query.channelMerchantId);
// product 由本页所属渠道决定(支付宝直连),无需从路由读取
const product = ProductEnum.ALIPAY;
if (mchNo && id) {
return { path: '/payment/global/channel-merchant/detail', query: { mchNo, id, product } };
}
return mchNo ? { path: '/payment/global/channel-merchant', query: { mchNo } } : '/payment/merchant';
}),
});
@@ -47,6 +42,7 @@
const mchNo = ref('');
const channelMchNo = ref('');
const channelMerchantName = ref('');
const channelMerchantId = ref('');
const appList = ref<AlipayMchApp[]>([]);
const editRef = ref<InstanceType<typeof AlipayMchAppEdit>>();
const detailRef = ref<InstanceType<typeof AlipayMchAppDetail>>();
@@ -79,18 +75,31 @@
}
mchNo.value = routeContext.query.value.mchNo;
channelMchNo.value = routeContext.query.value.channelMchNo;
channelMerchantName.value = normalizeRouteQueryValue(route.query.channelMerchantName);
}
/** 反查通道商户元数据(名称/主键), 替代 URL 透传, 保证数据实时 */
function loadMerchantMeta() {
if (!mchNo.value || !channelMchNo.value) {
return;
}
ChannelMerchantApi.findAllByMchNo(mchNo.value)
.then(({ data }) => {
const merchant = (data || []).find((m) => m.channelMchNo === channelMchNo.value);
if (merchant) {
channelMerchantName.value = merchant.channelMerchantName ?? '';
channelMerchantId.value = String(merchant.id ?? '');
}
});
}
/** 返回通道商户详情或列表 */
function handleBack() {
const id = normalizeRouteQueryValue(route.query.channelMerchantId);
if (id) {
if (channelMerchantId.value) {
// product 由本页所属渠道决定(支付宝直连)
const product = ProductEnum.ALIPAY;
router.push({
path: '/payment/global/channel-merchant/detail',
query: { mchNo: mchNo.value, id, product },
query: { mchNo: mchNo.value, id: channelMerchantId.value, product },
});
} else {
router.push({
@@ -118,6 +127,8 @@
return;
}
loadAppList();
// 反查通道商户元数据(标题/返回)
loadMerchantMeta();
});
</script>
@@ -150,14 +161,6 @@
</a-button>
<span class="text-lg font-bold text-foreground">{{ pageTitle }}</span>
</div>
<a
href="https://open.alipay.com/develop/manage"
target="_blank"
rel="noopener noreferrer"
class="shrink-0 text-sm font-normal text-primary"
>
{{ $t('payment.channel.alipayMchApp.consoleLink') }}
</a>
</div>
</template>

View File

@@ -115,13 +115,12 @@
keyConfigRef.value?.init();
}
if (card.key === 'appManage') {
const id = channelMerchant.value.id;
// 应用管理: 仅传 mchNo + channelMchNo, 接收端反查名称等元数据
router.push({
path: '/payment/global/channel-merchant/douyin-app-manage',
query: {
mchNo: mchNo.value,
channelMchNo: channelMchNo.value,
channelMerchantId: id,
},
});
}

View File

@@ -7,6 +7,7 @@
import { IconifyIcon } from '@vben-core/icons';
import { DouyinMchAppApi, type DouyinMchApp } from '#/api/payment/channel/douyin/mch-app.api';
import { ChannelMerchantApi } from '#/api/payment/global/channel-merchant/channel-merchant.api';
import RouteQueryMissingState from '#/components/route/RouteQueryMissingState.vue';
import { ProductEnum } from '#/enums/payment/productEnum';
import { PermCodes } from '#/constants/perm-codes';
@@ -33,12 +34,6 @@
}),
fallbackPath: computed(() => {
const mchNo = normalizeRouteQueryValue(route.query.mchNo);
const id = normalizeRouteQueryValue(route.query.channelMerchantId);
// product 由本页所属渠道决定(抖音直连),无需从路由读取
const product = ProductEnum.DOUYIN_PAY;
if (mchNo && id) {
return { path: '/payment/global/channel-merchant/detail', query: { mchNo, id, product } };
}
return mchNo
? { path: '/payment/global/channel-merchant', query: { mchNo } }
: '/payment/merchant';
@@ -49,6 +44,7 @@
const mchNo = ref('');
const channelMchNo = ref('');
const channelMerchantName = ref('');
const channelMerchantId = ref('');
const appList = ref<DouyinMchApp[]>([]);
const editRef = ref<InstanceType<typeof DouyinMchAppEdit>>();
const detailRef = ref<InstanceType<typeof DouyinMchAppDetail>>();
@@ -80,17 +76,30 @@
}
mchNo.value = routeContext.query.value.mchNo!;
channelMchNo.value = routeContext.query.value.channelMchNo!;
channelMerchantName.value = normalizeRouteQueryValue(route.query.channelMerchantName);
}
/** 反查通道商户元数据(名称/主键), 替代 URL 透传, 保证数据实时 */
function loadMerchantMeta() {
if (!mchNo.value || !channelMchNo.value) {
return;
}
ChannelMerchantApi.findAllByMchNo(mchNo.value)
.then(({ data }) => {
const merchant = (data || []).find((m) => m.channelMchNo === channelMchNo.value);
if (merchant) {
channelMerchantName.value = merchant.channelMerchantName ?? '';
channelMerchantId.value = String(merchant.id ?? '');
}
});
}
function handleBack() {
const id = normalizeRouteQueryValue(route.query.channelMerchantId);
if (id) {
if (channelMerchantId.value) {
// product 由本页所属渠道决定(抖音直连)
const product = ProductEnum.DOUYIN_PAY;
router.push({
path: '/payment/global/channel-merchant/detail',
query: { mchNo: mchNo.value, id, product },
query: { mchNo: mchNo.value, id: channelMerchantId.value, product },
});
} else {
router.push({
@@ -118,6 +127,8 @@
return;
}
loadAppList();
// 反查通道商户元数据(标题/返回)
loadMerchantMeta();
});
</script>

View File

@@ -99,7 +99,6 @@
query: {
mchNo: record.mchNo!,
id: record.id!,
product: record.product || '',
},
});
}

View File

@@ -100,7 +100,6 @@
query: {
mchNo: mchNo.value,
id: record.id!,
product: record.product || '',
},
});
}

View File

@@ -31,15 +31,12 @@
const router = useRouter();
const routeContext = useRequiredRouteQuery({
keys: ['mchNo', 'id', 'product'],
keys: ['mchNo', 'id'],
messageKey: computed(() => {
if (!normalizeRouteQueryValue(route.query.mchNo)) {
return 'payment.common.route.missingMchNo';
}
if (!normalizeRouteQueryValue(route.query.id)) {
return 'payment.merchant.channelMerchant.missingId';
}
return 'payment.common.route.missingProduct';
return 'payment.merchant.channelMerchant.missingId';
}),
fallbackPath: computed(() => {
const no = normalizeRouteQueryValue(route.query.mchNo);
@@ -53,6 +50,9 @@
const channelMerchant = ref<ChannelMerchantResult>({});
const loading = ref(false);
/** product 优先取路由(可选), 缺失时用详情反查回填 */
const resolvedProduct = computed(() => product.value || channelMerchant.value.product || '');
const alipayMchManageRef = ref<InstanceType<typeof AlipayMchManage>>();
const alipayChannelMerchantManageRef = ref<InstanceType<typeof AlipayChannelMerchantManage>>();
const wechatManageRef = ref<InstanceType<typeof WechatChannelMerchantManage>>();
@@ -147,7 +147,7 @@
return $t('payment.merchant.channelMerchant.manageTitleDefault');
}
const pageTitle = computed(() => resolvePageTitle(product.value));
const pageTitle = computed(() => resolvePageTitle(resolvedProduct.value));
/** 银联商务产品类型展示名称(多产品共页时区分具体产品) */
const productTypeName = computed(() => {
@@ -170,6 +170,10 @@
.then(({ data }) => {
if (data) {
channelMerchant.value = data;
// product 缺失时用详情回填
if (!product.value && data.product) {
product.value = data.product;
}
}
})
.finally(() => {
@@ -242,7 +246,8 @@
}
mchNo.value = routeContext.query.value.mchNo;
channelMerchantId.value = routeContext.query.value.id;
product.value = routeContext.query.value.product;
// product 可选(不在 keys), 缺失时由 loadChannelMerchant 回填
product.value = normalizeRouteQueryValue(route.query.product) || '';
}
watch(() => route.query, syncRouteState, { deep: true });
@@ -266,13 +271,11 @@
<template>
<RouteQueryMissingState
v-if="!routeContext.isValid"
:description="
:description="
$t(
!routeContext.query.value.mchNo
? 'payment.common.route.missingMchNo'
: !routeContext.query.value.id
? 'payment.merchant.channelMerchant.missingId'
: 'payment.common.route.missingProduct',
: 'payment.merchant.channelMerchant.missingId',
)
"
:back-text="$t('payment.merchant.workbench.workbench.backToList')"
@@ -291,94 +294,84 @@
<!-- 国际化按通道动态展示页头标题 -->
<span class="text-lg font-bold text-foreground">{{ pageTitle }}</span>
<!-- 银联商务多产品共页, 用标签区分具体产品类型("银联商务(C扫B)") -->
<a-tag v-if="isUmsProduct(product) && productTypeName" color="blue" class="!ml-1">
<a-tag v-if="isUmsProduct(resolvedProduct) && productTypeName" color="blue" class="!ml-1">
{{ productTypeName }}
</a-tag>
<span v-if="channelMerchant.channelMerchantName" class="text-sm text-muted-foreground">
({{ channelMerchant.channelMerchantName }})
</span>
</div>
<!-- 国际化支付宝开放平台外链直连产品 -->
<a
v-if="product === ProductEnum.ALIPAY"
href="https://open.alipay.com/develop/manage"
target="_blank"
rel="noopener noreferrer"
class="shrink-0 text-sm font-normal text-primary"
>
{{ $t('payment.channel.alipayMchApp.consoleLink') }}
</a>
</div>
</template>
<a-spin :spinning="loading">
<AlipayMchManage
v-if="product === ProductEnum.ALIPAY"
v-if="resolvedProduct === ProductEnum.ALIPAY"
ref="alipayMchManageRef"
@success="loadChannelMerchant"
/>
<AlipayChannelMerchantManage
v-else-if="product === ProductEnum.ALIPAY_ISV"
v-else-if="resolvedProduct === ProductEnum.ALIPAY_ISV"
ref="alipayChannelMerchantManageRef"
@success="loadChannelMerchant"
/>
<WechatChannelMerchantManage
v-else-if="product === ProductEnum.WECHAT_ISV"
v-else-if="resolvedProduct === ProductEnum.WECHAT_ISV"
ref="wechatManageRef"
@success="loadChannelMerchant"
/>
<WechatDirectMchManage
v-else-if="product === ProductEnum.WECHAT_PAY"
v-else-if="resolvedProduct === ProductEnum.WECHAT_PAY"
ref="wechatDirectManageRef"
@success="loadChannelMerchant"
/>
<DouyinDirectMchManage
v-else-if="product === ProductEnum.DOUYIN_PAY"
v-else-if="resolvedProduct === ProductEnum.DOUYIN_PAY"
ref="douyinDirectManageRef"
@success="loadChannelMerchant"
/>
<UmsDirectMchManage v-else-if="isUmsProduct(product)" ref="umsDirectManageRef" @success="loadChannelMerchant" />
<UmsDirectMchManage v-else-if="isUmsProduct(resolvedProduct)" ref="umsDirectManageRef" @success="loadChannelMerchant" />
<LakalaMchManage
v-else-if="product === ProductEnum.LAKALA_PAY"
v-else-if="resolvedProduct === ProductEnum.LAKALA_PAY"
ref="lakalaManageRef"
@success="loadChannelMerchant"
/>
<AdapayDirectMchManage
v-else-if="product === ProductEnum.ADA_PAY"
v-else-if="resolvedProduct === ProductEnum.ADA_PAY"
ref="adapayDirectManageRef"
@success="loadChannelMerchant"
/>
<HkrtMchManage
v-else-if="product === ProductEnum.HKRT_PAY"
v-else-if="resolvedProduct === ProductEnum.HKRT_PAY"
ref="hkrtManageRef"
@success="loadChannelMerchant"
/>
<LeshuaMchManage
v-else-if="product === ProductEnum.LESHUA_PAY"
v-else-if="resolvedProduct === ProductEnum.LESHUA_PAY"
ref="leshuaManageRef"
@success="loadChannelMerchant"
/>
<DougongMchManage
v-else-if="product === ProductEnum.DOUGONG_PAY"
v-else-if="resolvedProduct === ProductEnum.DOUGONG_PAY"
ref="dougongManageRef"
@success="loadChannelMerchant"
/>
<VbillMchManage
v-else-if="product === ProductEnum.VBILL_PAY"
v-else-if="resolvedProduct === ProductEnum.VBILL_PAY"
ref="vbillManageRef"
@success="loadChannelMerchant"
/>
<FuyouMchManage
v-else-if="product === ProductEnum.FUYOU_PAY"
v-else-if="resolvedProduct === ProductEnum.FUYOU_PAY"
ref="fuyouManageRef"
@success="loadChannelMerchant"
/>
<HmpayMchManage
v-else-if="product === ProductEnum.HM_PAY"
v-else-if="resolvedProduct === ProductEnum.HM_PAY"
ref="hmpayManageRef"
@success="loadChannelMerchant"
/>
<div v-else-if="!isSupported(product)" class="flex items-center justify-center" style="min-height: 400px">
<div v-else-if="!isSupported(resolvedProduct)" class="flex items-center justify-center" style="min-height: 400px">
<a-empty :description="$t('payment.merchant.channelMerchant.detailNotSupportYet')" />
</div>
</a-spin>

View File

@@ -6,8 +6,6 @@
import { $t } from '@vben/locales';
import { IconifyIcon } from '@vben-core/icons';
import {
ChannelMerchantApi,
type ChannelMerchantResult,
@@ -62,13 +60,12 @@
return source || '-';
}
/** 进入详情(仅传 id/product,不带 mchNo */
/** 进入详情(仅传 id, product 由 detail 页反查回填 */
function handleManage(record: ChannelMerchantResult) {
router.push({
path: '/mch/channel-merchant/detail',
query: {
id: record.id!,
product: record.product || '',
},
});
}
@@ -96,11 +93,6 @@
loadList();
}
/** 创建通道商户 */
function handleCreate() {
router.push({ path: '/mch/channel-merchant/create' });
}
/** 切换启用状态 */
function handleToggleEnable(record: ChannelMerchantResult, checked: boolean) {
const isEnabling = checked;
@@ -157,14 +149,7 @@
<template>
<div class="m-4">
<a-card variant="borderless" class="rounded-xl shadow-sm">
<vxe-toolbar ref="xToolbar" custom refresh :refresh-options="{ queryMethod: loadList }">
<template #buttons>
<a-button v-if="hasPermission(PermCodes.Channel.Merchant.MANAGE)" type="primary" @click="handleCreate">
<template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
{{ $t('payment.merchant.channelMerchant.create') }}
</a-button>
</template>
</vxe-toolbar>
<vxe-toolbar ref="xToolbar" custom refresh :refresh-options="{ queryMethod: loadList }" />
<vxe-table ref="xTable" :row-config="{ keyField: 'id' }" :data="list" :loading="loading">
<vxe-column type="seq" :title="$t('common.seq')" width="60" align="center" />

View File

@@ -2,14 +2,16 @@
import type { ChannelMerchantResult } from '#/api/payment/global/channel-merchant/channel-merchant.api';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { IconifyIcon } from '@vben-core/icons';
import AlipayDirectAppDrawer from '#/views/payment/channel-merchant/detail/alipay-direct/AlipayDirectAppDrawer.vue';
import ChannelMerchantNameEditModal from '#/views/payment/channel-merchant/detail/ChannelMerchantNameEditModal.vue';
import CommonChannelMerchantBasicInfo from '#/views/payment/channel-merchant/detail/CommonChannelMerchantBasicInfo.vue';
import AlipayChannelMerchantBasicInfo from './AlipayChannelMerchantBasicInfo.vue';
import AlipayMchAppCapability from './AlipayMchAppCapability.vue';
defineOptions({ name: 'AlipayMchManage' });
@@ -17,10 +19,11 @@
(e: 'success'): void;
}>();
const router = useRouter();
const channelMchNo = ref('');
const channelMerchant = ref<ChannelMerchantResult>({});
const basicInfoRef = ref<InstanceType<typeof CommonChannelMerchantBasicInfo>>();
const alipayAppRef = ref<InstanceType<typeof AlipayDirectAppDrawer>>();
const basicInfoRef = ref<InstanceType<typeof AlipayChannelMerchantBasicInfo>>();
const capabilityRef = ref<InstanceType<typeof AlipayMchAppCapability>>();
const editNameRef = ref<InstanceType<typeof ChannelMerchantNameEditModal>>();
/** 功能卡片配置(按组分组的卡片布局) */
@@ -53,6 +56,12 @@
icon: 'ant-design:appstore-outlined',
description: $t('payment.merchant.channelMerchant.cardAppDesc'),
},
{
key: 'capabilityBinding',
title: $t('payment.merchant.alipayDirectApp.cardCapabilityBinding'),
icon: 'ant-design:api-outlined',
description: $t('payment.merchant.alipayDirectApp.cardCapabilityBindingDesc'),
},
],
},
];
@@ -86,12 +95,24 @@
function handleCardClick(card: { key: string }) {
if (card.key === 'basicInfo') {
basicInfoRef.value?.open();
return;
}
if (card.key === 'editMerchantName') {
editNameRef.value?.open();
return;
}
if (card.key === 'capabilityBinding') {
capabilityRef.value?.show(channelMchNo.value);
return;
}
if (card.key === 'mchApp') {
alipayAppRef.value?.show();
// 应用管理: 仅传 channelMchNo, 接收端反查名称/沙箱等元数据
router.push({
path: '/mch/channel-merchant/alipay-app-manage',
query: {
channelMchNo: channelMchNo.value,
},
});
}
}
@@ -144,17 +165,13 @@
</div>
</div>
<CommonChannelMerchantBasicInfo
<AlipayChannelMerchantBasicInfo
ref="basicInfoRef"
:channel-mch-no="channelMchNo"
:channel-merchant="channelMerchant"
/>
<ChannelMerchantNameEditModal ref="editNameRef" :channel-merchant="channelMerchant" @success="emit('success')" />
<AlipayDirectAppDrawer
ref="alipayAppRef"
:channel-mch-no="channelMchNo"
:sandbox="channelMerchant.sandbox ?? false"
/>
<AlipayMchAppCapability ref="capabilityRef" />
</div>
</template>

View File

@@ -0,0 +1,208 @@
<script lang="ts" setup>
import type { AlipayDirectAppResult } from '#/api/payment/alipay/alipay-direct-app.api';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { IconifyIcon } from '@vben-core/icons';
import { AlipayDirectAppApi } from '#/api/payment/alipay/alipay-direct-app.api';
import { ChannelMerchantApi } from '#/api/payment/global/channel-merchant/channel-merchant.api';
import RouteQueryMissingState from '#/components/route/RouteQueryMissingState.vue';
import { PermCodes } from '#/constants/perm-codes';
import { usePermission } from '#/hooks/usePermission';
import { useRequiredRouteQuery } from '#/hooks/useRequiredRouteQuery';
import AlipayMchAppCard from './AlipayMchAppCard.vue';
import AlipayMchAppDetail from './AlipayMchAppDetail.vue';
import AlipayMchAppEdit from './AlipayMchAppEdit.vue';
defineOptions({ name: 'AlipayMchAppManage' });
const router = useRouter();
const { hasPermission } = usePermission();
// 商户端仅需 channelMchNomchNo 由后端 PaymentContext 强制)
const routeContext = useRequiredRouteQuery({
keys: ['channelMchNo'],
messageKey: 'payment.merchant.channelMerchant.missingChannelMchNo',
fallbackPath: '/mch/channel-merchant',
});
const loading = ref(false);
const channelMchNo = ref('');
const channelMerchantName = ref('');
const channelMerchantId = ref('');
// 沙箱标识(跟随通道商户, 透传给密钥 Tab)
const sandbox = ref(false);
const appList = ref<AlipayDirectAppResult[]>([]);
const editRef = ref<InstanceType<typeof AlipayMchAppEdit>>();
const detailRef = ref<InstanceType<typeof AlipayMchAppDetail>>();
const canAdd = computed(() => hasPermission(PermCodes.Channel.App.MANAGE));
const pageTitle = computed(() => {
const base = $t('payment.merchant.alipayDirectApp.manageTitle');
return channelMerchantName.value ? `${base} (${channelMerchantName.value})` : base;
});
/** 加载应用列表 */
function loadAppList() {
if (!channelMchNo.value) {
return;
}
loading.value = true;
AlipayDirectAppApi.listByChannelMchNo(channelMchNo.value)
.then(({ data }) => {
appList.value = data || [];
})
.finally(() => {
loading.value = false;
});
}
function syncRouteState() {
if (!routeContext.isValid.value) {
return;
}
channelMchNo.value = routeContext.query.value.channelMchNo;
}
/** 反查通道商户元数据(名称/沙箱/主键), 替代 URL 透传, 保证数据实时 */
function loadMerchantMeta() {
if (!channelMchNo.value) {
return;
}
ChannelMerchantApi.findAll()
.then(({ data }) => {
const merchant = (data || []).find((m) => m.channelMchNo === channelMchNo.value);
if (merchant) {
channelMerchantName.value = merchant.channelMerchantName ?? '';
channelMerchantId.value = String(merchant.id ?? '');
sandbox.value = merchant.sandbox ?? false;
}
});
}
/** 返回通道商户详情或列表 */
function handleBack() {
if (channelMerchantId.value) {
router.push({
path: '/mch/channel-merchant/detail',
query: { id: channelMerchantId.value },
});
} else {
router.push({ path: '/mch/channel-merchant' });
}
}
function handleAdd() {
editRef.value?.show(channelMchNo.value);
}
function handleManage(record: AlipayDirectAppResult) {
detailRef.value?.show(channelMchNo.value, record);
}
function handleEdit(record: AlipayDirectAppResult) {
editRef.value?.showEdit(channelMchNo.value, record);
}
onMounted(() => {
syncRouteState();
if (!routeContext.isValid.value) {
return;
}
loadAppList();
// 反查通道商户元数据(标题/返回/sandbox)
loadMerchantMeta();
});
</script>
<template>
<RouteQueryMissingState
v-if="!routeContext.isValid"
:description="$t('payment.merchant.channelMerchant.missingChannelMchNo')"
:back-text="$t('payment.merchant.channelMerchant.back')"
@back="routeContext.goFallback"
/>
<div v-else class="m-4">
<a-card variant="borderless" class="rounded-xl shadow-sm">
<template #title>
<div class="flex w-full items-center justify-between gap-4">
<div class="flex items-center gap-2">
<a-button
type="text"
class="flex items-center justify-center rounded-full hover:bg-accent"
@click="handleBack"
>
<template #icon>
<IconifyIcon icon="ant-design:arrow-left-outlined" class="text-lg" />
</template>
</a-button>
<span class="text-lg font-bold text-foreground">{{ pageTitle }}</span>
</div>
</div>
</template>
<a-spin :spinning="loading">
<a-empty
v-if="!loading && appList.length === 0 && !canAdd"
class="py-12"
:description="$t('payment.merchant.alipayDirectApp.emptyAppList')"
/>
<div v-else class="app-card-grid">
<AlipayMchAppCard
v-for="app in appList"
:key="app.id ?? app.aliAppId"
:record="app"
@edit="handleEdit(app)"
@manage="handleManage(app)"
/>
<div
v-if="canAdd"
class="add-card flex h-full min-h-[128px] cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border border-dashed border-border bg-muted/30 text-muted-foreground transition-all hover:border-primary hover:bg-primary/5 hover:text-primary"
@click="handleAdd"
>
<IconifyIcon icon="ant-design:plus-outlined" class="text-3xl" />
<span class="text-sm font-medium">{{ $t('payment.merchant.alipayDirectApp.addCard') }}</span>
</div>
</div>
</a-spin>
</a-card>
<AlipayMchAppEdit ref="editRef" @ok="loadAppList" />
<AlipayMchAppDetail ref="detailRef" :sandbox="sandbox" @deleted="loadAppList" />
</div>
</template>
<style scoped>
.app-card-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 20px;
padding: 4px;
min-height: 120px;
}
@media (max-width: 1400px) {
.app-card-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 1024px) {
.app-card-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.app-card-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -113,12 +113,11 @@
keyConfigRef.value?.init();
}
if (card.key === 'appManage') {
const id = channelMerchant.value.id;
// 应用管理: 仅传 channelMchNo, 接收端反查名称等元数据
router.push({
path: '/mch/channel-merchant/douyin-app-manage',
query: {
channelMchNo: channelMchNo.value,
channelMerchantId: id,
},
});
}

View File

@@ -1,17 +1,17 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { IconifyIcon } from '@vben-core/icons';
import { type DouyinMchApp, DouyinMchAppApi } from '#/api/payment/channel/douyin/mch-app.api';
import { ChannelMerchantApi } from '#/api/payment/global/channel-merchant/channel-merchant.api';
import RouteQueryMissingState from '#/components/route/RouteQueryMissingState.vue';
import { PermCodes } from '#/constants/perm-codes';
import { ProductEnum } from '#/enums/payment';
import { usePermission } from '#/hooks/usePermission';
import { normalizeRouteQueryValue, useRequiredRouteQuery } from '#/hooks/useRequiredRouteQuery';
import { useRequiredRouteQuery } from '#/hooks/useRequiredRouteQuery';
import DouyinMchAppCard from './DouyinMchAppCard.vue';
import DouyinMchAppDetail from './DouyinMchAppDetail.vue';
@@ -19,7 +19,6 @@
defineOptions({ name: 'DouyinMchAppManage' });
const route = useRoute();
const router = useRouter();
const { hasPermission } = usePermission();
@@ -27,20 +26,13 @@
const routeContext = useRequiredRouteQuery({
keys: ['channelMchNo'],
messageKey: 'payment.merchant.channelMerchant.missingChannelMchNo',
fallbackPath: computed(() => {
const id = normalizeRouteQueryValue(route.query.channelMerchantId);
// product 由本页所属渠道决定(抖音直连)
const product = ProductEnum.DOUYIN_PAY;
if (id) {
return { path: '/mch/channel-merchant/detail', query: { id, product } };
}
return '/mch/channel-merchant';
}),
fallbackPath: '/mch/channel-merchant',
});
const loading = ref(false);
const channelMchNo = ref('');
const channelMerchantName = ref('');
const channelMerchantId = ref('');
const appList = ref<DouyinMchApp[]>([]);
const editRef = ref<InstanceType<typeof DouyinMchAppEdit>>();
const detailRef = ref<InstanceType<typeof DouyinMchAppDetail>>();
@@ -71,17 +63,28 @@
return;
}
channelMchNo.value = routeContext.query.value.channelMchNo!;
channelMerchantName.value = normalizeRouteQueryValue(route.query.channelMerchantName);
}
/** 反查通道商户元数据(名称/主键), 替代 URL 透传, 保证数据实时 */
function loadMerchantMeta() {
if (!channelMchNo.value) {
return;
}
ChannelMerchantApi.findAll()
.then(({ data }) => {
const merchant = (data || []).find((m) => m.channelMchNo === channelMchNo.value);
if (merchant) {
channelMerchantName.value = merchant.channelMerchantName ?? '';
channelMerchantId.value = String(merchant.id ?? '');
}
});
}
function handleBack() {
const id = normalizeRouteQueryValue(route.query.channelMerchantId);
// product 由本页所属渠道决定(抖音直连)
const product = ProductEnum.DOUYIN_PAY;
if (id) {
if (channelMerchantId.value) {
router.push({
path: '/mch/channel-merchant/detail',
query: { id, product },
query: { id: channelMerchantId.value },
});
} else {
router.push({ path: '/mch/channel-merchant' });
@@ -106,6 +109,8 @@
return;
}
loadAppList();
// 反查通道商户元数据(标题/返回)
loadMerchantMeta();
});
</script>