feat(ums): 银联商务通道商户管理(管理端)

- 新增银联商务通道商户管理页(umsManage)与创建配置页调整
- api 补充通道商户接口, 新增 umsManage locale 中英文
- ChannelMerchantDetailDispatch 分发银联商务系列产品至管理页
This commit is contained in:
DaxPay Dev
2026-07-05 23:58:09 +08:00
parent 95f8b6b7ec
commit f291d82285
8 changed files with 755 additions and 170 deletions

View File

@@ -1,15 +1,119 @@
import type { Result } from '#/types/web';
import type { MchEntity, Result } from '#/types/web';
import { defHttp } from '#/api/request';
/**
* 银联商务通道商户创建 API
* 银联商务直连通道商户 API
*/
export const ChannelMerchantUmsApi = {
export const UmsDirectChannelMerchantApi = {
/**
* 创建银联商务通道商户
* 创建银联商务直连通道商户(同时保存密钥配置)
*/
create(data: Record<string, any>): Promise<Result<void>> {
return defHttp.post({ url: '/admin/ums/channel-merchant/create', data });
create(data: UmsDirectChannelMerchantCreateParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/ums/direct-channel-merchant/create', data });
},
/**
* 根据通道商户号查询银联商务直连通道商户配置
*/
findByChannelMchNo(channelMchNo: string): Promise<Result<UmsDirectChannelMerchantConfig>> {
return defHttp.get({
url: '/admin/ums/direct-channel-merchant/find-by-channel-mch-no',
params: { channelMchNo },
});
},
/**
* 根据通道商户号查询密钥配置
*/
findKeyConfig(channelMchNo: string): Promise<Result<UmsDirectKeyConfig>> {
return defHttp.get({
url: '/admin/ums/direct-channel-merchant/find-key-config',
params: { channelMchNo },
});
},
/**
* 保存密钥配置
*/
saveKeyConfig(data: UmsDirectKeyConfigParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/ums/direct-channel-merchant/save-key-config', data });
},
};
/**
* 银联商务直连通道商户配置
*/
export interface UmsDirectChannelMerchantConfig extends MchEntity {
/** 通道商户号(系统生成雪花号) */
channelMchNo?: string;
/** 所属支付产品 */
product?: string;
/** 银联商务商户号(mid) */
merchantNo?: string;
/** 终端号(tid) */
terminalNo?: string;
/** 订单号前缀 */
orderPrefix?: string;
/** 是否沙箱环境 */
sandbox?: boolean;
}
/**
* 银联商务直连密钥配置
*/
export interface UmsDirectKeyConfig {
/** 通道商户号 */
channelMchNo?: string;
/** 商户号 */
mchNo?: string;
/** 银联商务应用 AppId */
umsAppId?: string;
/** 应用密钥(HmacSHA256 签名密钥) */
appKey?: string;
/** 通讯密钥(回调验签密钥) */
secretKey?: string;
/** 应用密钥是否已配置 */
appKeyConfigured?: boolean;
/** 通讯密钥是否已配置 */
secretKeyConfigured?: boolean;
}
/**
* 银联商务直连密钥配置保存参数
*/
export interface UmsDirectKeyConfigParam {
/** 通道商户号 */
channelMchNo: string;
/** 商户号 */
mchNo?: string;
/** 银联商务应用 AppId */
umsAppId?: string;
/** 应用密钥 */
appKey?: string;
/** 通讯密钥 */
secretKey?: string;
}
/**
* 银联商务直连通道商户创建参数
*/
export interface UmsDirectChannelMerchantCreateParam {
/** 商户号 */
mchNo: string;
/** 通道商户名称 */
channelMerchantName: string;
/** 所属支付产品 */
product: string;
/** 银联商务商户号 */
merchantNo: string;
/** 终端号 */
terminalNo?: string;
/** 订单号前缀 */
orderPrefix?: string;
/** 是否沙箱环境 */
sandbox?: boolean;
/** 银联商务应用 AppId */
umsAppId: string;
/** 应用密钥 */
appKey: string;
/** 通讯密钥 */
secretKey: string;
}

View File

@@ -0,0 +1,9 @@
{
"manageTitle": "UMS Direct Merchant Management",
"cardDirectKeyConfig": "Key Config",
"cardDirectKeyConfigDesc": "Configure UMS merchant-level App ID and keys",
"directKeyConfigTitle": "UMS Key Configuration",
"keyConfigSection": "Merchant Key Config",
"appKeyConfigured": "Configured (enter new value to change)",
"secretKeyConfigured": "Configured (enter new value to change)"
}

View File

@@ -0,0 +1,9 @@
{
"manageTitle": "银联商务直连商户管理",
"cardDirectKeyConfig": "密钥配置",
"cardDirectKeyConfigDesc": "配置银联商务商户维度的应用ID和密钥",
"directKeyConfigTitle": "银联商务密钥配置",
"keyConfigSection": "商户密钥配置",
"appKeyConfigured": "已配置(如需修改请直接输入新值)",
"secretKeyConfigured": "已配置(如需修改请直接输入新值)"
}

View File

@@ -1,195 +1,187 @@
<script lang="ts" setup>
import { computed, nextTick, ref } from 'vue';
import type { UmsDirectChannelMerchantCreateParam } from '#/api/payment/channel/ums/channel-merchant.api';
import { $t } from '@vben/locales';
import { computed, nextTick, ref } from 'vue';
import { IconifyIcon } from '@vben-core/icons';
import { $t } from '@vben/locales';
import { ChannelMerchantUmsApi } from '#/api/payment/channel/ums/channel-merchant.api';
import { useMessage } from '#/hooks/useMessage';
import { IconifyIcon } from '@vben-core/icons';
defineOptions({ name: 'UmsMchCreateConfig' });
import { UmsDirectChannelMerchantApi } from '#/api/payment/channel/ums/channel-merchant.api';
import { useMessage } from '#/hooks/useMessage';
const emit = defineEmits<{
(e: 'prev'): void;
(e: 'close'): void;
}>();
defineOptions({ name: 'UmsMchCreateConfig' });
const { message } = useMessage();
const emit = defineEmits<{
(e: 'prev'): void;
(e: 'close'): void;
}>();
const mchNo = ref('');
const productCode = ref('');
const productDisplayName = ref('');
const formRef = ref();
const form = ref({
channelMerchantName: '',
sandbox: false,
umsAppId: '',
appKey: '',
merchantNo: '',
terminalNo: '',
orderPrefix: '',
secretKey: '',
});
const { message } = useMessage();
const visible = ref(false);
const createSuccess = ref(false);
const submitLoading = ref(false);
const rules = computed(() => ({
channelMerchantName: [{ required: true, message: $t('payment.merchant.channelMerchant.channelMerchantNameRequired') }],
umsAppId: [{ required: true, message: $t('payment.channel.ums.validation.umsAppId') }],
appKey: [{ required: true, message: $t('payment.channel.ums.validation.appKey') }],
merchantNo: [{ required: true, message: $t('payment.channel.ums.validation.merchantNo') }],
terminalNo: [{ required: true, message: $t('payment.channel.ums.validation.terminalNo') }],
orderPrefix: [{ required: true, message: $t('payment.channel.ums.validation.orderPrefix') }],
secretKey: [{ required: true, message: $t('payment.channel.ums.validation.secretKey') }],
}));
function init(no: string, product: string, displayName: string) {
mchNo.value = no;
productCode.value = product;
productDisplayName.value = displayName;
visible.value = true;
createSuccess.value = false;
resetForm();
}
function validate(): boolean {
let valid = false;
formRef.value?.validate((errors: any) => {
valid = !errors;
const mchNo = ref('');
const productCode = ref('');
const productDisplayName = ref('');
const formRef = ref();
const form = ref({
channelMerchantName: '',
sandbox: false,
umsAppId: '',
appKey: '',
merchantNo: '',
terminalNo: '',
orderPrefix: '',
secretKey: '',
});
return valid;
}
function getData(): Record<string, any> {
return { ...form.value };
}
const visible = ref(false);
const createSuccess = ref(false);
const submitLoading = ref(false);
function submit(param: Record<string, any>): Promise<any> {
return ChannelMerchantUmsApi.create({
...param,
...form.value,
});
}
const rules = computed(() => ({
channelMerchantName: [
{ required: true, message: $t('payment.merchant.channelMerchant.channelMerchantNameRequired') },
],
umsAppId: [{ required: true, message: $t('payment.channel.ums.validation.umsAppId') }],
appKey: [{ required: true, message: $t('payment.channel.ums.validation.appKey') }],
merchantNo: [{ required: true, message: $t('payment.channel.ums.validation.merchantNo') }],
terminalNo: [{ required: true, message: $t('payment.channel.ums.validation.terminalNo') }],
orderPrefix: [{ required: true, message: $t('payment.channel.ums.validation.orderPrefix') }],
secretKey: [{ required: true, message: $t('payment.channel.ums.validation.secretKey') }],
}));
function handlePrev() {
emit('prev');
}
function handleSubmit() {
formRef.value?.validate().then(() => {
submitLoading.value = true;
const param = {
mchNo: mchNo.value,
product: productCode.value,
channel: 'ums',
...form.value,
};
submit(param)
.then(() => {
createSuccess.value = true;
message.success($t('payment.merchant.channelMerchant.createSuccess'));
})
.finally(() => {
submitLoading.value = false;
});
}).catch(() => {});
function init(no: string, product: string, displayName: string) {
mchNo.value = no;
productCode.value = product;
productDisplayName.value = displayName;
visible.value = true;
createSuccess.value = false;
resetForm();
}
function resetForm() {
nextTick(() => {
formRef.value?.resetFields();
});
}
function validate(): boolean {
let valid = false;
formRef.value?.validate((errors: any) => {
valid = !errors;
});
return valid;
}
defineExpose({ init, validate, getData, submit });
function getData(): Record<string, any> {
return { ...form.value };
}
function submit(param: Record<string, any>): Promise<any> {
return UmsDirectChannelMerchantApi.create({
...param,
...form.value,
} as UmsDirectChannelMerchantCreateParam);
}
function handlePrev() {
emit('prev');
}
function handleSubmit() {
formRef.value
?.validate()
.then(() => {
submitLoading.value = true;
const param = {
mchNo: mchNo.value,
product: productCode.value,
channel: 'ums',
...form.value,
};
submit(param)
.then(() => {
createSuccess.value = true;
message.success($t('payment.merchant.channelMerchant.createSuccess'));
})
.finally(() => {
submitLoading.value = false;
});
})
.catch(() => {});
}
function resetForm() {
nextTick(() => {
formRef.value?.resetFields();
});
}
defineExpose({ init, validate, getData, submit });
</script>
<template>
<div v-if="visible">
<div v-if="!createSuccess">
<a-spin :spinning="submitLoading">
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
:validate-trigger="['blur', 'change']"
>
<!-- 国际化商户名称 -->
<a-form-item :label="$t('payment.merchant.channelMerchant.channelMerchantName')" name="channelMerchantName">
<a-input
v-model:value="form.channelMerchantName"
:placeholder="$t('payment.merchant.channelMerchant.pleaseInputName')"
/>
</a-form-item>
<!-- 国际化所属支付产品 -->
<a-form-item :label="$t('payment.merchant.channelMerchant.selectedProduct')">
<a-input :value="productDisplayName" disabled />
</a-form-item>
<!-- 国际化沙箱环境 -->
<a-form-item :label="$t('payment.channel.ums.sandbox')" name="sandbox">
<a-switch
v-model:checked="form.sandbox"
:checked-children="$t('common.yes')"
:un-checked-children="$t('common.no')"
/>
</a-form-item>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
:validate-trigger="['blur', 'change']"
>
<!-- 国际化商户名称 -->
<a-form-item :label="$t('payment.merchant.channelMerchant.channelMerchantName')" name="channelMerchantName">
<a-input
v-model:value="form.channelMerchantName"
:placeholder="$t('payment.merchant.channelMerchant.pleaseInputName')"
/>
</a-form-item>
<!-- 国际化所属支付产品 -->
<a-form-item :label="$t('payment.merchant.channelMerchant.selectedProduct')">
<a-input :value="productDisplayName" disabled />
</a-form-item>
<!-- 国际化沙箱环境 -->
<a-form-item :label="$t('payment.channel.ums.sandbox')" name="sandbox">
<a-switch
v-model:checked="form.sandbox"
:checked-children="$t('common.yes')"
:un-checked-children="$t('common.no')"
/>
</a-form-item>
<a-form-item :label="$t('payment.channel.ums.umsAppId')" name="umsAppId">
<a-input
v-model:value="form.umsAppId"
:placeholder="$t('payment.channel.ums.umsAppIdPlaceholder')"
/>
</a-form-item>
<a-form-item :label="$t('payment.channel.ums.umsAppId')" name="umsAppId">
<a-input v-model:value="form.umsAppId" :placeholder="$t('payment.channel.ums.umsAppIdPlaceholder')" />
</a-form-item>
<a-form-item :label="$t('payment.channel.ums.appKey')" name="appKey">
<a-input-password
v-model:value="form.appKey"
:placeholder="$t('payment.channel.ums.appKeyPlaceholder')"
/>
</a-form-item>
<a-form-item :label="$t('payment.channel.ums.appKey')" name="appKey">
<a-input-password v-model:value="form.appKey" :placeholder="$t('payment.channel.ums.appKeyPlaceholder')" />
</a-form-item>
<a-form-item :label="$t('payment.channel.ums.merchantNo')" name="merchantNo">
<a-input
v-model:value="form.merchantNo"
:placeholder="$t('payment.channel.ums.merchantNoPlaceholder')"
/>
</a-form-item>
<a-form-item :label="$t('payment.channel.ums.merchantNo')" name="merchantNo">
<a-input v-model:value="form.merchantNo" :placeholder="$t('payment.channel.ums.merchantNoPlaceholder')" />
</a-form-item>
<a-form-item :label="$t('payment.channel.ums.terminalNo')" name="terminalNo">
<a-input
v-model:value="form.terminalNo"
:placeholder="$t('payment.channel.ums.terminalNoPlaceholder')"
/>
</a-form-item>
<a-form-item :label="$t('payment.channel.ums.terminalNo')" name="terminalNo">
<a-input v-model:value="form.terminalNo" :placeholder="$t('payment.channel.ums.terminalNoPlaceholder')" />
</a-form-item>
<a-form-item :label="$t('payment.channel.ums.orderPrefix')" name="orderPrefix">
<a-input
v-model:value="form.orderPrefix"
:placeholder="$t('payment.channel.ums.orderPrefixPlaceholder')"
/>
</a-form-item>
<a-form-item :label="$t('payment.channel.ums.orderPrefix')" name="orderPrefix">
<a-input v-model:value="form.orderPrefix" :placeholder="$t('payment.channel.ums.orderPrefixPlaceholder')" />
</a-form-item>
<a-form-item :label="$t('payment.channel.ums.secretKey')" name="secretKey">
<a-input-password
v-model:value="form.secretKey"
:placeholder="$t('payment.channel.ums.secretKeyPlaceholder')"
/>
</a-form-item>
<a-form-item :label="$t('payment.channel.ums.secretKey')" name="secretKey">
<a-input-password
v-model:value="form.secretKey"
:placeholder="$t('payment.channel.ums.secretKeyPlaceholder')"
/>
</a-form-item>
<div class="flex justify-center gap-4 mt-8 pt-6 border-t border-border">
<a-button @click="handlePrev">
{{ $t('payment.merchant.channelMerchant.prevStep') }}
</a-button>
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
{{ $t('payment.merchant.channelMerchant.create') }}
</a-button>
</div>
</a-form>
<div class="flex justify-center gap-4 mt-8 pt-6 border-t border-border">
<a-button @click="handlePrev">
{{ $t('payment.merchant.channelMerchant.prevStep') }}
</a-button>
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
{{ $t('payment.merchant.channelMerchant.create') }}
</a-button>
</div>
</a-form>
</a-spin>
</div>

View File

@@ -0,0 +1,119 @@
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { $t } from '@vben/locales';
import { type ChannelMerchantResult } from '#/api/payment/channel/channel-merchant.api';
import {
UmsDirectChannelMerchantApi,
type UmsDirectChannelMerchantConfig,
} from '#/api/payment/channel/ums/channel-merchant.api';
defineOptions({ name: 'UmsDirectChannelMerchantBasicInfo' });
const props = defineProps<{
channelMchNo: string;
channelMerchant: ChannelMerchantResult;
}>();
const visible = ref(false);
const loading = ref(false);
const config = ref<UmsDirectChannelMerchantConfig>({});
const enableLabel = computed(() =>
props.channelMerchant.enable
? $t('payment.merchant.channelMerchant.enableStatus')
: $t('payment.merchant.channelMerchant.disableStatus'),
);
const sourceLabel = computed(() => {
if (props.channelMerchant.source === 'manual') {
return $t('payment.merchant.channelMerchant.sourceManual');
}
return props.channelMerchant.source || '-';
});
const sandboxLabel = computed(() => (config.value.sandbox ? $t('common.yes') : $t('common.no')));
function loadConfig() {
if (!props.channelMchNo) {
return;
}
loading.value = true;
config.value = {};
UmsDirectChannelMerchantApi.findByChannelMchNo(props.channelMchNo)
.then(({ data }) => {
config.value = data || {};
})
.finally(() => {
loading.value = false;
});
}
function open() {
visible.value = true;
loadConfig();
}
function close() {
visible.value = false;
}
watch(
() => props.channelMchNo,
() => {
if (visible.value) {
loadConfig();
}
},
);
defineExpose({ open, close });
</script>
<template>
<a-drawer
v-model:open="visible"
:title="$t('payment.merchant.channelMerchant.basicInfoDrawerTitle')"
:size="640"
destroy-on-hidden
>
<a-spin :spinning="loading">
<a-descriptions bordered :column="1" size="small">
<!-- 国际化: 通道商户号 -->
<a-descriptions-item :label="$t('payment.merchant.channelMerchant.channelMerchantNo')">
{{ channelMerchant.channelMchNo || '-' }}
</a-descriptions-item>
<!-- 国际化: 通道商户名称 -->
<a-descriptions-item :label="$t('payment.merchant.channelMerchant.channelMerchantName')">
{{ channelMerchant.channelMerchantName || '-' }}
</a-descriptions-item>
<!-- 国际化: 是否启用 -->
<a-descriptions-item :label="$t('payment.merchant.channelMerchant.enable')">
{{ enableLabel }}
</a-descriptions-item>
<!-- 国际化: 来源 -->
<a-descriptions-item :label="$t('payment.merchant.channelMerchant.source')">
{{ sourceLabel }}
</a-descriptions-item>
<!-- 国际化: 银联商务商户号 -->
<a-descriptions-item :label="$t('payment.channel.ums.merchantNo')">
{{ config.merchantNo || '-' }}
</a-descriptions-item>
<!-- 国际化: 终端号 -->
<a-descriptions-item :label="$t('payment.channel.ums.terminalNo')">
{{ config.terminalNo || '-' }}
</a-descriptions-item>
<!-- 国际化: 订单号前缀 -->
<a-descriptions-item :label="$t('payment.channel.ums.orderPrefix')">
{{ config.orderPrefix || '-' }}
</a-descriptions-item>
<!-- 国际化: 沙箱环境 -->
<a-descriptions-item :label="$t('payment.channel.ums.sandbox')">
{{ sandboxLabel }}
</a-descriptions-item>
</a-descriptions>
</a-spin>
</a-drawer>
</template>

View File

@@ -0,0 +1,159 @@
<script lang="ts" setup>
import { computed, nextTick, ref } from 'vue';
import { $t } from '@vben/locales';
import { UmsDirectChannelMerchantApi, type UmsDirectKeyConfig } from '#/api/payment/channel/ums/channel-merchant.api';
import { PermCodes } from '#/constants/perm-codes';
import { useFormEdit } from '#/hooks/useFormEdit';
import { useMessage } from '#/hooks/useMessage';
import { usePermission } from '#/hooks/usePermission';
defineOptions({ name: 'UmsDirectKeyConfigEdit' });
const props = defineProps<{
channelMchNo: string;
}>();
const emit = defineEmits<{
(e: 'saved'): void;
}>();
const { labelCol, wrapperCol, confirmLoading, visible, handleCancel, diffForm } = useFormEdit();
const { message } = useMessage();
const { hasPermission } = usePermission();
const formRef = ref();
const form = ref<UmsDirectKeyConfig>({} as UmsDirectKeyConfig);
let rawForm: Record<string, any> = {};
const canEdit = computed(() => hasPermission(PermCodes.Channel.Merchant.MANAGE));
const drawerTitle = $t('payment.channel.umsManage.directKeyConfigTitle');
const rules = {
umsAppId: [{ required: true, message: $t('payment.channel.ums.validation.umsAppId') }],
appKey: [{ required: true, message: $t('payment.channel.ums.validation.appKey') }],
secretKey: [{ required: true, message: $t('payment.channel.ums.validation.secretKey') }],
};
/** 打开抽屉并加载密钥配置 */
function init() {
visible.value = true;
resetForm();
loadConfig();
}
function loadConfig() {
if (!props.channelMchNo) return;
confirmLoading.value = true;
UmsDirectChannelMerchantApi.findKeyConfig(props.channelMchNo)
.then(({ data }) => {
rawForm = { ...data };
form.value = { ...data } as UmsDirectKeyConfig;
})
.finally(() => {
confirmLoading.value = false;
});
}
function handleOk() {
formRef.value
?.validate()
.then(() => {
confirmLoading.value = true;
UmsDirectChannelMerchantApi.saveKeyConfig({
...form.value,
...diffForm(rawForm, form.value, 'appKey', 'secretKey'),
channelMchNo: props.channelMchNo,
})
.then(() => {
message.success($t('common.saveSuccess'));
handleCancel();
emit('saved');
})
.finally(() => {
confirmLoading.value = false;
});
})
.catch(() => {});
}
function resetForm() {
nextTick(() => {
formRef.value?.resetFields();
});
}
defineExpose({ init });
</script>
<template>
<a-drawer
v-model:open="visible"
:title="drawerTitle"
size="large"
:styles="{ footer: { textAlign: 'right' } }"
:mask-closable="false"
destroy-on-hidden
@close="handleCancel"
>
<a-spin :spinning="confirmLoading">
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="labelCol"
:wrapper-col="wrapperCol"
:validate-trigger="['blur', 'change']"
>
<a-divider orientation="left">{{ $t('payment.channel.umsManage.keyConfigSection') }}</a-divider>
<!-- 国际化: 应用ID -->
<a-form-item :label="$t('payment.channel.ums.umsAppId')" name="umsAppId">
<a-input
v-model:value="form.umsAppId"
:disabled="!canEdit"
:placeholder="$t('payment.channel.ums.umsAppIdPlaceholder')"
/>
</a-form-item>
<!-- 国际化: 应用密钥 -->
<a-form-item :label="$t('payment.channel.ums.appKey')" name="appKey">
<a-input-password
v-model:value="form.appKey"
:disabled="!canEdit"
:placeholder="
form.appKeyConfigured
? $t('payment.channel.umsManage.appKeyConfigured')
: $t('payment.channel.ums.appKeyPlaceholder')
"
/>
</a-form-item>
<!-- 国际化: 通讯密钥 -->
<a-form-item :label="$t('payment.channel.ums.secretKey')" name="secretKey">
<a-input-password
v-model:value="form.secretKey"
:disabled="!canEdit"
:placeholder="
form.secretKeyConfigured
? $t('payment.channel.umsManage.secretKeyConfigured')
: $t('payment.channel.ums.secretKeyPlaceholder')
"
/>
</a-form-item>
</a-form>
</a-spin>
<template #footer>
<a-space>
<a-button @click="handleCancel">{{ $t('common.cancel') }}</a-button>
<a-button v-if="canEdit" type="primary" :loading="confirmLoading" @click="handleOk">
{{ $t('common.save') }}
</a-button>
</a-space>
</template>
</a-drawer>
</template>

View File

@@ -0,0 +1,171 @@
<script lang="ts" setup>
import type { ChannelMerchantResult } from '#/api/payment/channel/channel-merchant.api';
import { computed, ref } from 'vue';
import { $t } from '@vben/locales';
import { IconifyIcon } from '@vben-core/icons';
import UmsDirectChannelMerchantBasicInfo from './UmsDirectChannelMerchantBasicInfo.vue';
import UmsDirectKeyConfigEdit from './UmsDirectKeyConfigEdit.vue';
defineOptions({ name: 'UmsDirectMchManage' });
const loading = ref(false);
const mchNo = ref('');
const channelMchNo = ref('');
const channelMerchant = ref<ChannelMerchantResult>({});
const basicInfoRef = ref<InstanceType<typeof UmsDirectChannelMerchantBasicInfo>>();
const keyConfigRef = ref<InstanceType<typeof UmsDirectKeyConfigEdit>>();
/** 功能卡片配置(银联商务首期仅基本信息与密钥配置, 无应用管理) */
const functionCards = computed(() => [
{
// 国际化: 基础管理
group: $t('payment.merchant.channelMerchant.groupBasic'),
color: 'blue',
cards: [
{
key: 'basicInfo',
// 国际化: 基本信息
title: $t('payment.merchant.channelMerchant.cardBasicInfo'),
icon: 'ant-design:info-circle-outlined',
// 国际化: 编辑通道商户基本信息
description: $t('payment.merchant.channelMerchant.cardBasicInfoDesc'),
},
{
key: 'keyConfig',
// 国际化: 密钥配置
title: $t('payment.channel.umsManage.cardDirectKeyConfig'),
icon: 'ant-design:key-outlined',
// 国际化: 配置通道商户密钥
description: $t('payment.channel.umsManage.cardDirectKeyConfigDesc'),
},
],
},
]);
/** 获取组主题颜色(底条) */
function getGroupColorClass(color: string) {
const map: Record<string, string> = {
blue: 'bg-blue-500',
green: 'bg-emerald-500',
purple: 'bg-purple-500',
};
return map[color] || 'bg-gray-500';
}
/** 获取图标背景颜色 */
function getIconBgClass(color: string) {
const map: Record<string, string> = {
blue: 'bg-primary/10 text-primary',
green: 'bg-success/10 text-success',
purple: 'bg-purple-500/10 text-purple-500',
};
return map[color] || 'bg-muted text-muted-foreground';
}
function init(no: string, cMchNo: string, summary: ChannelMerchantResult) {
mchNo.value = no;
channelMchNo.value = cMchNo;
channelMerchant.value = summary;
}
function handleCardClick(card: { key: string }) {
if (card.key === 'basicInfo') {
basicInfoRef.value?.open();
}
if (card.key === 'keyConfig') {
keyConfigRef.value?.init();
}
}
defineExpose({ init });
</script>
<template>
<div>
<a-spin :spinning="loading">
<div class="space-y-12 py-4">
<div v-for="group in functionCards" :key="group.group">
<div class="mb-6 flex items-center gap-3 px-2">
<div class="h-6 w-1.5 rounded-full shadow-sm" :class="getGroupColorClass(group.color)"></div>
<span class="text-xl font-extrabold tracking-tight text-foreground">{{ group.group }}</span>
</div>
<div class="card-grid">
<a-card
v-for="card in group.cards"
:key="card.key"
hoverable
class="isv-card group relative overflow-hidden rounded-2xl border-none bg-card shadow-md transition-all duration-300 hover:-translate-y-1.5 hover:shadow-xl"
:styles="{ body: { padding: '24px 20px' } }"
@click="handleCardClick(card)"
>
<div class="flex flex-col items-center text-center">
<div
class="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl shadow-sm transition-all duration-300 group-hover:scale-110 group-hover:shadow-md"
:class="getIconBgClass(group.color)"
>
<IconifyIcon :icon="card.icon" class="h-7 w-7" />
</div>
<div
class="mb-1.5 text-base font-bold text-foreground group-hover:text-primary transition-colors duration-300"
>
{{ card.title }}
</div>
<a-tooltip :title="card.description" placement="bottom">
<div
class="card-desc line-clamp-1 text-xs leading-relaxed text-muted-foreground group-hover:text-foreground transition-colors duration-300"
>
{{ card.description }}
</div>
</a-tooltip>
</div>
<div
class="absolute bottom-0 left-0 h-1.5 w-0 transition-all duration-300 group-hover:w-full"
:class="getGroupColorClass(group.color)"
></div>
</a-card>
</div>
</div>
</div>
</a-spin>
<UmsDirectChannelMerchantBasicInfo
ref="basicInfoRef"
:channel-mch-no="channelMchNo"
:channel-merchant="channelMerchant"
/>
<UmsDirectKeyConfigEdit ref="keyConfigRef" :channel-mch-no="channelMchNo" />
</div>
</template>
<style scoped>
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, 220px);
gap: 24px;
justify-content: center;
}
.isv-card {
max-height: 200px;
}
.card-desc {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.line-clamp-1 {
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
</style>

View File

@@ -13,6 +13,7 @@
import AlipayChannelMerchantManage from '#/views/payment/channel/alipay/manage/mch/AlipayChannelMerchantManage.vue';
import AlipayMchManage from '#/views/payment/channel/alipay/manage/mch/AlipayMchManage.vue';
import DouyinDirectMchManage from '#/views/payment/channel/douyin/manage/DouyinDirectMchManage.vue';
import UmsDirectMchManage from '#/views/payment/channel/ums/manage/UmsDirectMchManage.vue';
import WechatChannelMerchantManage from '#/views/payment/channel/wechat/manage/mch/WechatChannelMerchantManage.vue';
import WechatDirectMchManage from '#/views/payment/channel/wechat/manage/mch/WechatDirectMchManage.vue';
@@ -49,6 +50,19 @@
const wechatManageRef = ref<InstanceType<typeof WechatChannelMerchantManage>>();
const wechatDirectManageRef = ref<InstanceType<typeof WechatDirectMchManage>>();
const douyinDirectManageRef = ref<InstanceType<typeof DouyinDirectMchManage>>();
const umsDirectManageRef = ref<InstanceType<typeof UmsDirectMchManage>>();
/** 是否为银联商务系列产品 */
function isUmsProduct(p: string) {
return (
p === ProductEnum.UMS_QRCODE ||
p === ProductEnum.UMS_JSAPI ||
p === ProductEnum.UMS_APP ||
p === ProductEnum.UMS_MINI ||
p === ProductEnum.UMS_H5 ||
p === ProductEnum.UMS_BARCODE
);
}
/** 是否为已支持的通道产品 */
function isSupported(p: string) {
@@ -57,7 +71,8 @@
p === ProductEnum.ALIPAY_ISV ||
p === ProductEnum.WECHAT_ISV ||
p === ProductEnum.WECHAT_PAY ||
p === ProductEnum.DOUYIN_PAY
p === ProductEnum.DOUYIN_PAY ||
isUmsProduct(p)
);
}
@@ -78,6 +93,9 @@
if (productCode === ProductEnum.DOUYIN_PAY) {
return $t('payment.channel.douyinManage.manageTitle');
}
if (isUmsProduct(productCode)) {
return $t('payment.channel.umsManage.manageTitle');
}
return $t('payment.merchant.channelMerchant.manageTitleDefault');
}
@@ -129,6 +147,9 @@
if (product.value === ProductEnum.DOUYIN_PAY) {
douyinDirectManageRef.value?.init(mchNo.value, channelMchNo, channelMerchant.value);
}
if (isUmsProduct(product.value)) {
umsDirectManageRef.value?.init(mchNo.value, channelMchNo, channelMerchant.value);
}
}
/** 从路由同步 query 至本地状态 */
@@ -212,6 +233,7 @@
<WechatChannelMerchantManage v-else-if="product === ProductEnum.WECHAT_ISV" ref="wechatManageRef" />
<WechatDirectMchManage v-else-if="product === ProductEnum.WECHAT_PAY" ref="wechatDirectManageRef" />
<DouyinDirectMchManage v-else-if="product === ProductEnum.DOUYIN_PAY" ref="douyinDirectManageRef" />
<UmsDirectMchManage v-else-if="isUmsProduct(product)" ref="umsDirectManageRef" />
<div v-else-if="!isSupported(product)" class="flex items-center justify-center" style="min-height: 400px">
<a-empty :description="$t('payment.merchant.channelMerchant.detailNotSupportYet')" />
</div>