mirror of
https://gitee.com/bootx/dax-pay-ui
synced 2026-08-10 23:01:59 +08:00
feat(vbill): 随行付通道管理端(服务商配置/商户进件 API与视图 + i18n)
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import type { Result } from '#/types/web';
|
||||
|
||||
import { defHttp } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 随行付(天阙科技)通道商户绑定 API
|
||||
*/
|
||||
export const VbillChannelMerchantApi = {
|
||||
/**
|
||||
* 根据通道商户号查询随行付通道商户配置
|
||||
*/
|
||||
findByChannelMchNo(channelMchNo: string): Promise<Result<VbillIsvChannelMerchant>> {
|
||||
return defHttp.get({
|
||||
url: '/admin/vbill/isv-channel-merchant/find-by-channel-mch-no',
|
||||
params: { channelMchNo },
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 创建随行付通道商户
|
||||
*/
|
||||
create(data: VbillIsvChannelMerchantCreateParam): Promise<Result<void>> {
|
||||
return defHttp.post({ url: '/admin/vbill/isv-channel-merchant/create', data });
|
||||
},
|
||||
};
|
||||
|
||||
/** 随行付通道商户绑定 */
|
||||
export interface VbillIsvChannelMerchant {
|
||||
/** 主键 */
|
||||
id?: string;
|
||||
/** 通道商户号 */
|
||||
channelMchNo?: string;
|
||||
/** 所属支付产品 */
|
||||
product?: string;
|
||||
/** 天阙商户号(mno) */
|
||||
vbillMchNo?: string;
|
||||
}
|
||||
|
||||
/** 随行付通道商户创建参数 */
|
||||
export interface VbillIsvChannelMerchantCreateParam {
|
||||
/** 商户号 */
|
||||
mchNo: string;
|
||||
/** 通道商户名称 */
|
||||
channelMerchantName: string;
|
||||
/** 所属支付产品 */
|
||||
product: string;
|
||||
/** 天阙商户号(mno) */
|
||||
vbillMchNo: string;
|
||||
}
|
||||
3
apps/daxpay-admin/src/api/payment/channel/vbill/index.ts
Normal file
3
apps/daxpay-admin/src/api/payment/channel/vbill/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
/** 随行付(天阙科技)渠道 barrel */
|
||||
export * from './channel-merchant.api';
|
||||
export * from './pay-config.api';
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Result } from '#/types/web';
|
||||
|
||||
import { defHttp } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 随行付(天阙科技)服务商密钥配置 API
|
||||
*/
|
||||
export const VbillPayConfigApi = {
|
||||
/**
|
||||
* 查询随行付服务商密钥配置
|
||||
* 平台为唯一服务商,密钥全局唯一
|
||||
*/
|
||||
findConfig(product: string): Promise<Result<VbillIsvKeyConfig>> {
|
||||
return defHttp.get({ url: '/admin/vbill/isv-key-config/find-config', params: { product } });
|
||||
},
|
||||
/**
|
||||
* 保存随行付服务商密钥配置
|
||||
*/
|
||||
saveConfig(data: VbillIsvKeyConfig): Promise<Result<void>> {
|
||||
return defHttp.post({ url: '/admin/vbill/isv-key-config/save-config', data });
|
||||
},
|
||||
};
|
||||
|
||||
/** 随行付服务商密钥配置 */
|
||||
export interface VbillIsvKeyConfig {
|
||||
/** 主键 */
|
||||
id?: string;
|
||||
/** 产品编码 */
|
||||
product?: string;
|
||||
/** 天阙合作机构ID(orgId) */
|
||||
orgId?: string;
|
||||
/** 天阙RSA公钥(X509 Base64, 回调验签用) */
|
||||
publicKey?: string;
|
||||
/** 商户RSA私钥(PKCS8 Base64, SHA1withRSA签名) */
|
||||
privateKey?: string;
|
||||
/** 是否沙箱环境 */
|
||||
sandbox?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"title": "VBill ISV Configuration",
|
||||
"groupConfig": "Configuration",
|
||||
"configTitle": "VBill ISV Key Configuration",
|
||||
"manageTitle": "VBill Merchant Management",
|
||||
"basicConfig": "Basic Config",
|
||||
"keyConfig": "Key Config",
|
||||
"cardMchKey": "ISV Key",
|
||||
"cardMchKeyDesc": "Configure VBill org ID, RSA public/private keys",
|
||||
"orgId": "Tianque Org ID",
|
||||
"orgIdPlaceholder": "Please enter Tianque partner org ID (orgId)",
|
||||
"publicKey": "Tianque RSA Public Key",
|
||||
"publicKeyTooltip": "Tianque RSA public key (X509 Base64), for response/callback signature verification",
|
||||
"privateKey": "Merchant RSA Private Key",
|
||||
"privateKeyTooltip": "Merchant RSA private key (PKCS8 Base64), SHA1withRSA signing",
|
||||
"sandbox": "Sandbox",
|
||||
"groupMchConfig": "Merchant Config",
|
||||
"cardMchConfig": "Merchant Config",
|
||||
"cardMchConfigDesc": "View Tianque merchant number",
|
||||
"mchConfigTitle": "Merchant Config",
|
||||
"vbillMchNo": "Tianque Merchant No",
|
||||
"vbillMchNoPlaceholder": "Please enter Tianque merchant No (mno)",
|
||||
"validation": {
|
||||
"orgId": "Please enter Tianque org ID",
|
||||
"publicKey": "Please enter Tianque RSA public key",
|
||||
"privateKey": "Please enter merchant RSA private key",
|
||||
"vbillMchNo": "Please enter Tianque merchant No"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"title": "随行付服务商配置",
|
||||
"groupConfig": "配置管理",
|
||||
"configTitle": "随行付服务商密钥配置",
|
||||
"manageTitle": "随行付商户管理",
|
||||
"basicConfig": "基础配置",
|
||||
"keyConfig": "密钥配置",
|
||||
"cardMchKey": "服务商密钥",
|
||||
"cardMchKeyDesc": "配置随行付天阙机构ID、RSA公私钥",
|
||||
"orgId": "天阙机构ID",
|
||||
"orgIdPlaceholder": "请输入天阙合作机构ID(orgId)",
|
||||
"publicKey": "天阙RSA公钥",
|
||||
"publicKeyTooltip": "天阙平台RSA公钥(X509 Base64), 用于响应/回调验签",
|
||||
"privateKey": "商户RSA私钥",
|
||||
"privateKeyTooltip": "商户RSA私钥(PKCS8 Base64), SHA1withRSA 签名",
|
||||
"sandbox": "沙箱环境",
|
||||
"groupMchConfig": "商户配置",
|
||||
"cardMchConfig": "商户配置",
|
||||
"cardMchConfigDesc": "查看天阙商户号",
|
||||
"mchConfigTitle": "商户配置",
|
||||
"vbillMchNo": "天阙商户号",
|
||||
"vbillMchNoPlaceholder": "请输入天阙商户号(mno)",
|
||||
"validation": {
|
||||
"orgId": "请输入天阙机构ID",
|
||||
"publicKey": "请输入天阙RSA公钥",
|
||||
"privateKey": "请输入商户RSA私钥",
|
||||
"vbillMchNo": "请输入天阙商户号"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
type VbillIsvKeyConfig,
|
||||
VbillPayConfigApi,
|
||||
} from '#/api/payment/channel/vbill/pay-config.api';
|
||||
import { PermCodes } from '#/constants/perm-codes';
|
||||
import { ProductEnum } from '#/enums/payment/productEnum';
|
||||
import { useFormEdit } from '#/hooks/useFormEdit';
|
||||
import { useMessage } from '#/hooks/useMessage';
|
||||
import { usePermission } from '#/hooks/usePermission';
|
||||
|
||||
defineOptions({ name: 'VbillIsvConfigEdit' });
|
||||
|
||||
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<VbillIsvKeyConfig>({} as VbillIsvKeyConfig);
|
||||
let rawForm: Record<string, any> = {};
|
||||
|
||||
const canEdit = computed(() => hasPermission(PermCodes.Payment.Vbill.MANAGE));
|
||||
|
||||
const drawerTitle = $t('payment.channel.vbillIsv.configTitle');
|
||||
|
||||
const rules = {
|
||||
orgId: [{ required: true, message: $t('payment.channel.vbillIsv.validation.orgId') }],
|
||||
// 国际化: 脱敏字段始终必填, 编辑时预填脱敏值, 未修改由 diffForm 比对跳过更新
|
||||
publicKey: [{ required: true, message: $t('payment.channel.vbillIsv.validation.publicKey') }],
|
||||
privateKey: [{ required: true, message: $t('payment.channel.vbillIsv.validation.privateKey') }],
|
||||
};
|
||||
|
||||
/** 打开抽屉并加载随行付服务商密钥配置(平台为唯一服务商,密钥全局唯一) */
|
||||
function init() {
|
||||
visible.value = true;
|
||||
resetForm();
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
confirmLoading.value = true;
|
||||
VbillPayConfigApi.findConfig(ProductEnum.VBILL_PAY)
|
||||
.then(({ data }) => {
|
||||
rawForm = { ...data };
|
||||
form.value = {
|
||||
product: ProductEnum.VBILL_PAY,
|
||||
...data,
|
||||
} as VbillIsvKeyConfig;
|
||||
})
|
||||
.finally(() => {
|
||||
confirmLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function handleOk() {
|
||||
formRef.value?.validate().then(() => {
|
||||
confirmLoading.value = true;
|
||||
VbillPayConfigApi.saveConfig({
|
||||
...form.value,
|
||||
...diffForm(
|
||||
rawForm,
|
||||
form.value,
|
||||
'privateKey',
|
||||
'publicKey',
|
||||
),
|
||||
product: ProductEnum.VBILL_PAY,
|
||||
})
|
||||
.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.vbillIsv.basicConfig') }}</a-divider>
|
||||
|
||||
<!-- 国际化: 天阙机构ID -->
|
||||
<a-form-item :label="$t('payment.channel.vbillIsv.orgId')" name="orgId">
|
||||
<a-input
|
||||
v-model:value="form.orgId"
|
||||
:disabled="!canEdit"
|
||||
:placeholder="$t('payment.channel.vbillIsv.orgIdPlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">{{ $t('payment.channel.vbillIsv.keyConfig') }}</a-divider>
|
||||
|
||||
<!-- 国际化: 天阙RSA公钥 -->
|
||||
<a-form-item
|
||||
:label="$t('payment.channel.vbillIsv.publicKey')"
|
||||
name="publicKey"
|
||||
:tooltip="$t('payment.channel.vbillIsv.publicKeyTooltip')"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="form.publicKey"
|
||||
:disabled="!canEdit"
|
||||
:rows="3"
|
||||
:placeholder="$t('payment.channel.vbillIsv.publicKeyTooltip')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 国际化: 商户RSA私钥 -->
|
||||
<a-form-item
|
||||
:label="$t('payment.channel.vbillIsv.privateKey')"
|
||||
name="privateKey"
|
||||
:tooltip="$t('payment.channel.vbillIsv.privateKeyTooltip')"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="form.privateKey"
|
||||
:disabled="!canEdit"
|
||||
:rows="3"
|
||||
:placeholder="$t('payment.channel.vbillIsv.privateKeyTooltip')"
|
||||
/>
|
||||
</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>
|
||||
@@ -0,0 +1,168 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { IconifyIcon } from '@vben-core/icons';
|
||||
|
||||
import { VbillChannelMerchantApi, type VbillIsvChannelMerchantCreateParam } from '#/api/payment/channel/vbill/channel-merchant.api';
|
||||
import ChannelLogo from '#/components/channel/ChannelLogo.vue';
|
||||
import { productI18nMap, productNameMap } from '#/enums/payment';
|
||||
import { useMessage } from '#/hooks/useMessage';
|
||||
|
||||
defineOptions({ name: 'VbillMchCreateConfig' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'prev'): void;
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const { message } = useMessage();
|
||||
|
||||
const mchNo = ref('');
|
||||
const productCode = ref('');
|
||||
const channelCode = ref('');
|
||||
const formRef = ref();
|
||||
const form = ref({
|
||||
channelMerchantName: '',
|
||||
vbillMchNo: '',
|
||||
});
|
||||
|
||||
const visible = ref(false);
|
||||
const createSuccess = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
|
||||
/** 支付产品展示名称 */
|
||||
const productDisplayName = computed(() => {
|
||||
const product = productCode.value;
|
||||
if (!product) return '-';
|
||||
const i18nKey = productI18nMap[product];
|
||||
if (i18nKey) {
|
||||
return $t(i18nKey);
|
||||
}
|
||||
return productNameMap[product] || product;
|
||||
});
|
||||
|
||||
const rules = computed(() => ({
|
||||
channelMerchantName: [{ required: true, message: $t('payment.merchant.channelMerchant.channelMerchantNameRequired') }],
|
||||
vbillMchNo: [{ required: true, message: $t('payment.channel.vbillIsv.validation.vbillMchNo') }],
|
||||
}));
|
||||
|
||||
function init(no: string, product: string, channel: string) {
|
||||
mchNo.value = no;
|
||||
productCode.value = product;
|
||||
channelCode.value = channel;
|
||||
visible.value = true;
|
||||
createSuccess.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
function validate(): boolean {
|
||||
let valid = false;
|
||||
formRef.value?.validate((errors: any) => {
|
||||
valid = !errors;
|
||||
});
|
||||
return valid;
|
||||
}
|
||||
|
||||
function getData(): Record<string, any> {
|
||||
return { ...form.value };
|
||||
}
|
||||
|
||||
function submit(param: Record<string, any>): Promise<any> {
|
||||
return VbillChannelMerchantApi.create(param as VbillIsvChannelMerchantCreateParam);
|
||||
}
|
||||
|
||||
function handlePrev() {
|
||||
emit('prev');
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
formRef.value?.validate().then(() => {
|
||||
submitLoading.value = true;
|
||||
const param = {
|
||||
mchNo: mchNo.value,
|
||||
product: productCode.value,
|
||||
...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.product')">
|
||||
<div class="flex items-center gap-2 h-8">
|
||||
<ChannelLogo v-if="channelCode" :channel="channelCode" :size="24" />
|
||||
<span class="text-foreground">{{ productDisplayName }}</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<!-- 国际化: 商户名称 -->
|
||||
<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.channel.vbillIsv.vbillMchNo')" name="vbillMchNo">
|
||||
<a-input
|
||||
v-model:value="form.vbillMchNo"
|
||||
:placeholder="$t('payment.channel.vbillIsv.vbillMchNoPlaceholder')"
|
||||
/>
|
||||
</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>
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col items-center justify-center py-12">
|
||||
<div class="w-20 h-20 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center mb-6">
|
||||
<IconifyIcon icon="ant-design:check-circle-filled" class="text-4xl text-green-500" />
|
||||
</div>
|
||||
<div class="text-xl font-bold text-foreground mb-2">{{
|
||||
$t('payment.merchant.channelMerchant.createSuccess')
|
||||
}}</div>
|
||||
<div class="text-sm text-muted-foreground mb-8">{{
|
||||
$t('payment.merchant.channelMerchant.createSuccessDesc')
|
||||
}}</div>
|
||||
<a-button type="primary" @click="emit('close')">
|
||||
{{ $t('payment.merchant.channelMerchant.closePage') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,114 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { IconifyIcon } from '@vben-core/icons';
|
||||
|
||||
import VbillIsvConfigEdit from '#/views/payment/channel/vbill/config/VbillIsvConfigEdit.vue';
|
||||
|
||||
defineOptions({ name: 'VbillManage' });
|
||||
|
||||
const mchKeyEditRef = ref<InstanceType<typeof VbillIsvConfigEdit> | null>(null);
|
||||
|
||||
/**
|
||||
* 功能卡片配置
|
||||
* 随行付为聚合服务商模式, 密钥全局唯一, 不区分应用/能力绑定, 仅一个密钥配置入口
|
||||
*/
|
||||
const functionCards = computed(() => [
|
||||
{
|
||||
group: $t('payment.channel.vbillIsv.groupConfig'),
|
||||
color: 'blue',
|
||||
cards: [
|
||||
{
|
||||
key: 'mchKey',
|
||||
title: $t('payment.channel.vbillIsv.cardMchKey'),
|
||||
icon: 'ant-design:key-outlined',
|
||||
description: $t('payment.channel.vbillIsv.cardMchKeyDesc'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
function getIconBgClass(color: string) {
|
||||
const map: Record<string, string> = {
|
||||
green: 'bg-success/10 text-success',
|
||||
blue: 'bg-primary/10 text-primary',
|
||||
};
|
||||
return map[color] || 'bg-muted text-muted-foreground';
|
||||
}
|
||||
|
||||
function getGroupColorClass(color: string) {
|
||||
const map: Record<string, string> = {
|
||||
green: 'bg-emerald-500',
|
||||
blue: 'bg-blue-500',
|
||||
};
|
||||
return map[color] || 'bg-gray-500';
|
||||
}
|
||||
|
||||
/** 初始化(由分发页调用, 平台为唯一服务商, 无需服务商号) */
|
||||
function init() {
|
||||
}
|
||||
|
||||
function handleCardClick(card: { key: string }) {
|
||||
if (card.key === 'mchKey') {
|
||||
mchKeyEditRef.value?.init();
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ init });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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="line-clamp-1 text-xs leading-relaxed text-muted-foreground">{{ 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>
|
||||
<VbillIsvConfigEdit ref="mchKeyEditRef" />
|
||||
</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;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,164 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ChannelMerchantResult } from '#/api/payment/channel/channel-merchant.api';
|
||||
import type { VbillIsvChannelMerchant } from '#/api/payment/channel/vbill/channel-merchant.api';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { IconifyIcon } from '@vben-core/icons';
|
||||
|
||||
import { VbillChannelMerchantApi } from '#/api/payment/channel/vbill/channel-merchant.api';
|
||||
import ChannelMerchantNameEditModal from '#/views/payment/merchant/channel-merchant/detail/ChannelMerchantNameEditModal.vue';
|
||||
import CommonChannelMerchantBasicInfo from '#/views/payment/merchant/channel-merchant/detail/CommonChannelMerchantBasicInfo.vue';
|
||||
|
||||
defineOptions({ name: 'VbillMchManage' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'success'): void;
|
||||
}>();
|
||||
|
||||
const mchNo = ref('');
|
||||
const channelMchNo = ref('');
|
||||
const channelMerchant = ref<ChannelMerchantResult>({});
|
||||
const channelConfig = ref<VbillIsvChannelMerchant>({});
|
||||
const basicInfoRef = ref<InstanceType<typeof CommonChannelMerchantBasicInfo>>();
|
||||
const editNameRef = ref<InstanceType<typeof ChannelMerchantNameEditModal>>();
|
||||
|
||||
/** 通道专属字段(基本信息抽屉展示) */
|
||||
const extraFields = computed(() => [
|
||||
{ label: $t('payment.channel.vbillIsv.vbillMchNo'), value: channelConfig.value.vbillMchNo || '-' },
|
||||
]);
|
||||
|
||||
/**
|
||||
* 功能卡片配置
|
||||
* 随行付服务商模式, 商户仅有天阙商户号(mno), 创建后无需额外编辑
|
||||
*/
|
||||
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: 'editMerchantName',
|
||||
// 国际化: 修改商户名称
|
||||
title: $t('payment.merchant.channelMerchant.cardEditMerchantName'),
|
||||
icon: 'ant-design:edit-outlined',
|
||||
description: $t('payment.merchant.channelMerchant.cardEditMerchantNameDesc'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
function getGroupColorClass(color: string) {
|
||||
const map: Record<string, string> = {
|
||||
blue: 'bg-blue-500',
|
||||
green: 'bg-emerald-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',
|
||||
};
|
||||
return map[color] || 'bg-muted text-muted-foreground';
|
||||
}
|
||||
|
||||
/** 初始化(由 DetailDispatch 调用) */
|
||||
function init(no: string, mchChannelNo: string, summary: ChannelMerchantResult) {
|
||||
mchNo.value = no;
|
||||
channelMchNo.value = mchChannelNo;
|
||||
channelMerchant.value = summary;
|
||||
}
|
||||
|
||||
function handleCardClick(card: { key: string }) {
|
||||
if (card.key === 'basicInfo') {
|
||||
// 先加载随行付专属配置, 再打开基本信息抽屉
|
||||
VbillChannelMerchantApi.findByChannelMchNo(channelMchNo.value).then(({ data }) => {
|
||||
channelConfig.value = data || {};
|
||||
basicInfoRef.value?.open();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (card.key === 'editMerchantName') {
|
||||
editNameRef.value?.open();
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ init });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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="line-clamp-1 text-xs leading-relaxed text-muted-foreground">{{ 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>
|
||||
|
||||
<CommonChannelMerchantBasicInfo
|
||||
ref="basicInfoRef"
|
||||
:channel-mch-no="channelMchNo"
|
||||
:channel-merchant="channelMerchant"
|
||||
:extra-fields="extraFields"
|
||||
/>
|
||||
|
||||
<ChannelMerchantNameEditModal ref="editNameRef" :channel-merchant="channelMerchant" @success="emit('success')" />
|
||||
</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;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user