mirror of
https://gitee.com/bootx/dax-pay-ui
synced 2026-08-14 09:25:39 +08:00
feat(hkrt): 海科融通服务商配置与通道商户管理(管理端)
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import type { Result } from '#/types/web';
|
||||
|
||||
import { defHttp } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 海科融通通道商户绑定 API
|
||||
*/
|
||||
export const HkrtChannelMerchantApi = {
|
||||
/**
|
||||
* 根据通道商户号查询海科融通通道商户配置
|
||||
*/
|
||||
findByChannelMchNo(channelMchNo: string): Promise<Result<HkrtIsvChannelMerchant>> {
|
||||
return defHttp.get({
|
||||
url: '/admin/hkrt/isv-channel-merchant/find-by-channel-mch-no',
|
||||
params: { channelMchNo },
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 创建海科融通通道商户
|
||||
*/
|
||||
create(data: HkrtIsvChannelMerchantCreateParam): Promise<Result<void>> {
|
||||
return defHttp.post({ url: '/admin/hkrt/isv-channel-merchant/create', data });
|
||||
},
|
||||
/** 更新 SAAS 终端号 */
|
||||
updatePn(channelMchNo: string, pn: string): Promise<Result<void>> {
|
||||
return defHttp.post({
|
||||
url: '/admin/hkrt/isv-channel-merchant/update-pn',
|
||||
params: { channelMchNo, pn },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/** 海科融通通道商户绑定 */
|
||||
export interface HkrtIsvChannelMerchant {
|
||||
/** 主键 */
|
||||
id?: string;
|
||||
/** 通道商户号 */
|
||||
channelMchNo?: string;
|
||||
/** 所属支付产品 */
|
||||
product?: string;
|
||||
/** 海科商户编号 */
|
||||
merchNo?: string;
|
||||
/** SAAS 终端号 */
|
||||
pn?: string;
|
||||
}
|
||||
|
||||
/** 海科融通通道商户创建参数 */
|
||||
export interface HkrtIsvChannelMerchantCreateParam {
|
||||
/** 商户号 */
|
||||
mchNo: string;
|
||||
/** 通道商户名称 */
|
||||
channelMerchantName: string;
|
||||
/** 所属支付产品 */
|
||||
product: string;
|
||||
/** 海科商户编号 */
|
||||
merchNo: string;
|
||||
}
|
||||
3
apps/daxpay-admin/src/api/payment/channel/hkrt/index.ts
Normal file
3
apps/daxpay-admin/src/api/payment/channel/hkrt/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
/** 海科融通渠道 barrel */
|
||||
export * from './channel-merchant.api';
|
||||
export * from './pay-config.api';
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Result } from '#/types/web';
|
||||
|
||||
import { defHttp } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 海科融通服务商密钥配置 API
|
||||
*/
|
||||
export const HkrtPayConfigApi = {
|
||||
/**
|
||||
* 查询海科融通服务商密钥配置
|
||||
* 平台为唯一服务商,密钥全局唯一
|
||||
*/
|
||||
findConfig(product: string): Promise<Result<HkrtIsvKeyConfig>> {
|
||||
return defHttp.get({ url: '/admin/hkrt/isv-key-config/find-config', params: { product } });
|
||||
},
|
||||
/**
|
||||
* 保存海科融通服务商密钥配置
|
||||
*/
|
||||
saveConfig(data: HkrtIsvKeyConfig): Promise<Result<void>> {
|
||||
return defHttp.post({ url: '/admin/hkrt/isv-key-config/save-config', data });
|
||||
},
|
||||
};
|
||||
|
||||
/** 海科融通服务商密钥配置 */
|
||||
export interface HkrtIsvKeyConfig {
|
||||
/** 主键 */
|
||||
id?: string;
|
||||
/** 产品编码 */
|
||||
product?: string;
|
||||
/** 服务商编号 */
|
||||
agentNo?: string;
|
||||
/** 接入机构标识 */
|
||||
accessId?: string;
|
||||
/** 签名密钥(MD5 大写签名用) */
|
||||
accessKey?: string;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"title": "HKRT ISV Configuration",
|
||||
"groupConfig": "Configuration",
|
||||
"configTitle": "HKRT ISV Key Configuration",
|
||||
"manageTitle": "HKRT Merchant Management",
|
||||
"basicConfig": "Basic Configuration",
|
||||
"keyConfig": "Key Configuration",
|
||||
"cardMchKey": "ISV Key",
|
||||
"cardMchKeyDesc": "Configure HKRT agent number, access ID and signature key",
|
||||
"agentNo": "Agent Number",
|
||||
"agentNoPlaceholder": "Please enter agent number",
|
||||
"accessId": "Access Institution ID",
|
||||
"accessIdPlaceholder": "Please enter access institution ID",
|
||||
"accessKey": "Signature Key",
|
||||
"accessKeyPlaceholder": "Please enter signature key (for MD5 uppercase signing)",
|
||||
"groupMchConfig": "Merchant Config",
|
||||
"cardMchConfig": "Merchant Config",
|
||||
"cardMchConfigDesc": "Configure SAAS terminal number for channel merchant",
|
||||
"mchConfigTitle": "Merchant Config",
|
||||
"merchNo": "HKRT Merchant No",
|
||||
"merchNoPlaceholder": "Please enter HKRT merchant number",
|
||||
"pn": "SAAS Terminal No",
|
||||
"pnPlaceholder": "Please enter SAAS terminal number",
|
||||
"validation": {
|
||||
"agentNo": "Please enter agent number",
|
||||
"accessId": "Please enter access institution ID",
|
||||
"accessKey": "Please enter signature key",
|
||||
"merchNo": "Please enter HKRT merchant number",
|
||||
"pn": "Please enter SAAS terminal number"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"title": "海科融通服务商配置",
|
||||
"groupConfig": "配置管理",
|
||||
"configTitle": "海科融通服务商密钥配置",
|
||||
"manageTitle": "海科融通商户管理",
|
||||
"basicConfig": "基础配置",
|
||||
"keyConfig": "密钥配置",
|
||||
"cardMchKey": "服务商密钥",
|
||||
"cardMchKeyDesc": "配置海科融通服务商编号、接入标识、签名密钥",
|
||||
"agentNo": "服务商编号",
|
||||
"agentNoPlaceholder": "请输入服务商编号",
|
||||
"accessId": "接入机构标识",
|
||||
"accessIdPlaceholder": "请输入接入机构标识",
|
||||
"accessKey": "签名密钥",
|
||||
"accessKeyPlaceholder": "请输入签名密钥(MD5 大写签名用)",
|
||||
"groupMchConfig": "商户配置",
|
||||
"cardMchConfig": "商户配置",
|
||||
"cardMchConfigDesc": "配置通道商户SAAS终端号",
|
||||
"mchConfigTitle": "商户配置",
|
||||
"merchNo": "海科商户编号",
|
||||
"merchNoPlaceholder": "请输入海科商户编号",
|
||||
"pn": "SAAS终端号",
|
||||
"pnPlaceholder": "请输入SAAS终端号",
|
||||
"validation": {
|
||||
"agentNo": "请输入服务商编号",
|
||||
"accessId": "请输入接入机构标识",
|
||||
"accessKey": "请输入签名密钥",
|
||||
"merchNo": "请输入海科商户编号",
|
||||
"pn": "请输入SAAS终端号"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
type HkrtIsvKeyConfig,
|
||||
HkrtPayConfigApi,
|
||||
} from '#/api/payment/channel/hkrt/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: 'HkrtIsvConfigEdit' });
|
||||
|
||||
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<HkrtIsvKeyConfig>({} as HkrtIsvKeyConfig);
|
||||
let rawForm: Record<string, any> = {};
|
||||
|
||||
const canEdit = computed(() => hasPermission(PermCodes.Payment.Hkrt.MANAGE));
|
||||
|
||||
const drawerTitle = $t('payment.channel.hkrtIsv.configTitle');
|
||||
|
||||
const rules = {
|
||||
agentNo: [{ required: true, message: $t('payment.channel.hkrtIsv.validation.agentNo') }],
|
||||
accessId: [{ required: true, message: $t('payment.channel.hkrtIsv.validation.accessId') }],
|
||||
accessKey: [{ required: true, message: $t('payment.channel.hkrtIsv.validation.accessKey') }],
|
||||
};
|
||||
|
||||
/** 打开抽屉并加载海科融通服务商密钥配置(平台为唯一服务商,密钥全局唯一) */
|
||||
function init() {
|
||||
visible.value = true;
|
||||
resetForm();
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
confirmLoading.value = true;
|
||||
HkrtPayConfigApi.findConfig(ProductEnum.HKRT_PAY)
|
||||
.then(({ data }) => {
|
||||
rawForm = { ...data };
|
||||
form.value = {
|
||||
product: ProductEnum.HKRT_PAY,
|
||||
...data,
|
||||
} as HkrtIsvKeyConfig;
|
||||
})
|
||||
.finally(() => {
|
||||
confirmLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function handleOk() {
|
||||
formRef.value?.validate().then(() => {
|
||||
confirmLoading.value = true;
|
||||
HkrtPayConfigApi.saveConfig({
|
||||
...form.value,
|
||||
...diffForm(
|
||||
rawForm,
|
||||
form.value,
|
||||
'accessKey',
|
||||
),
|
||||
product: ProductEnum.HKRT_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.hkrtIsv.basicConfig') }}</a-divider>
|
||||
|
||||
<!-- 国际化: 服务商编号 -->
|
||||
<a-form-item :label="$t('payment.channel.hkrtIsv.agentNo')" name="agentNo">
|
||||
<a-input
|
||||
v-model:value="form.agentNo"
|
||||
:disabled="!canEdit"
|
||||
:placeholder="$t('payment.channel.hkrtIsv.agentNoPlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 国际化: 接入机构标识 -->
|
||||
<a-form-item :label="$t('payment.channel.hkrtIsv.accessId')" name="accessId">
|
||||
<a-input
|
||||
v-model:value="form.accessId"
|
||||
:disabled="!canEdit"
|
||||
:placeholder="$t('payment.channel.hkrtIsv.accessIdPlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">{{ $t('payment.channel.hkrtIsv.keyConfig') }}</a-divider>
|
||||
|
||||
<!-- 国际化: 签名密钥 -->
|
||||
<a-form-item :label="$t('payment.channel.hkrtIsv.accessKey')" name="accessKey">
|
||||
<a-textarea
|
||||
v-model:value="form.accessKey"
|
||||
:disabled="!canEdit"
|
||||
:rows="3"
|
||||
:placeholder="$t('payment.channel.hkrtIsv.accessKeyPlaceholder')"
|
||||
/>
|
||||
</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,169 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { IconifyIcon } from '@vben-core/icons';
|
||||
|
||||
import { HkrtChannelMerchantApi, type HkrtIsvChannelMerchantCreateParam } from '#/api/payment/channel/hkrt/channel-merchant.api';
|
||||
import ChannelLogo from '#/components/channel/ChannelLogo.vue';
|
||||
import { productI18nMap, productNameMap } from '#/enums/payment';
|
||||
import { useMessage } from '#/hooks/useMessage';
|
||||
|
||||
defineOptions({ name: 'HkrtMchCreateConfig' });
|
||||
|
||||
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: '',
|
||||
merchNo: '',
|
||||
});
|
||||
|
||||
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') }],
|
||||
merchNo: [{ required: true, message: $t('payment.channel.hkrtIsv.validation.merchNo') }],
|
||||
}));
|
||||
|
||||
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> {
|
||||
// param 已由 handleSubmit 组装完整(mchNo/product/form 各字段), 直接透传
|
||||
return HkrtChannelMerchantApi.create(param as HkrtIsvChannelMerchantCreateParam);
|
||||
}
|
||||
|
||||
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.hkrtIsv.merchNo')" name="merchNo">
|
||||
<a-input
|
||||
v-model:value="form.merchNo"
|
||||
:placeholder="$t('payment.channel.hkrtIsv.merchNoPlaceholder')"
|
||||
/>
|
||||
</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 HkrtIsvConfigEdit from '#/views/payment/channel/hkrt/config/HkrtIsvConfigEdit.vue';
|
||||
|
||||
defineOptions({ name: 'HkrtManage' });
|
||||
|
||||
const mchKeyEditRef = ref<InstanceType<typeof HkrtIsvConfigEdit> | null>(null);
|
||||
|
||||
/**
|
||||
* 功能卡片配置
|
||||
* 海科融通为聚合服务商模式, 密钥全局唯一, 不区分应用/能力绑定, 仅一个密钥配置入口
|
||||
*/
|
||||
const functionCards = computed(() => [
|
||||
{
|
||||
group: $t('payment.channel.hkrtIsv.groupConfig'),
|
||||
color: 'blue',
|
||||
cards: [
|
||||
{
|
||||
key: 'mchKey',
|
||||
title: $t('payment.channel.hkrtIsv.cardMchKey'),
|
||||
icon: 'ant-design:key-outlined',
|
||||
description: $t('payment.channel.hkrtIsv.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>
|
||||
<HkrtIsvConfigEdit 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,126 @@
|
||||
<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 {
|
||||
HkrtChannelMerchantApi,
|
||||
type HkrtIsvChannelMerchant,
|
||||
} from '#/api/payment/channel/hkrt/channel-merchant.api';
|
||||
import { productI18nMap, productNameMap } from '#/enums/payment';
|
||||
|
||||
defineOptions({ name: 'HkrtChannelMerchantBasicInfo' });
|
||||
|
||||
const props = defineProps<{
|
||||
channelMchNo: string;
|
||||
channelMerchant: ChannelMerchantResult;
|
||||
}>();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
// 海科融通通道商户专属配置(海科商户编号 / SAAS 终端号)
|
||||
const hkrtConfig = ref<HkrtIsvChannelMerchant>({});
|
||||
|
||||
// 支付产品展示名称(优先取国际化映射, 兜底产品编码)
|
||||
const productDisplayName = computed(() => {
|
||||
const product = props.channelMerchant.product;
|
||||
if (!product) return '-';
|
||||
const i18nKey = productI18nMap[product];
|
||||
if (i18nKey) {
|
||||
return $t(i18nKey);
|
||||
}
|
||||
return productNameMap[product] || product;
|
||||
});
|
||||
|
||||
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 || '-';
|
||||
});
|
||||
|
||||
/** 加载海科融通通道商户专属配置 */
|
||||
function loadConfig() {
|
||||
if (!props.channelMchNo) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
hkrtConfig.value = {};
|
||||
HkrtChannelMerchantApi.findByChannelMchNo(props.channelMchNo)
|
||||
.then(({ data }) => {
|
||||
hkrtConfig.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.selectedProduct')">
|
||||
{{ productDisplayName }}
|
||||
</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.hkrtIsv.merchNo')">
|
||||
{{ hkrtConfig.merchNo || '-' }}
|
||||
</a-descriptions-item>
|
||||
<!-- 国际化: SAAS 终端号 -->
|
||||
<a-descriptions-item :label="$t('payment.channel.hkrtIsv.pn')">
|
||||
{{ hkrtConfig.pn || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</template>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { HkrtChannelMerchantApi } from '#/api/payment/channel/hkrt/channel-merchant.api';
|
||||
import { useMessage } from '#/hooks/useMessage';
|
||||
|
||||
defineOptions({ name: 'HkrtMchConfigEdit' });
|
||||
|
||||
const emit = defineEmits(['ok']);
|
||||
|
||||
const { message } = useMessage();
|
||||
|
||||
const formRef = ref();
|
||||
const visible = ref(false);
|
||||
const confirmLoading = ref(false);
|
||||
const channelMchNo = ref('');
|
||||
|
||||
const formState = ref({
|
||||
pn: '',
|
||||
});
|
||||
|
||||
const title = $t('payment.channel.hkrtIsv.mchConfigTitle');
|
||||
|
||||
const formRules = {
|
||||
pn: [{ required: true, message: $t('payment.channel.hkrtIsv.validation.pn') }],
|
||||
};
|
||||
|
||||
/** 打开配置弹窗 */
|
||||
function show(mchChannelNo: string, currentPn?: string) {
|
||||
channelMchNo.value = mchChannelNo;
|
||||
formState.value.pn = currentPn ?? '';
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleOk() {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
// 校验失败: 表单已显示错误提示
|
||||
return;
|
||||
}
|
||||
confirmLoading.value = true;
|
||||
HkrtChannelMerchantApi.updatePn(channelMchNo.value, formState.value.pn)
|
||||
.then(() => {
|
||||
message.success($t('common.saveSuccess'));
|
||||
handleCancel();
|
||||
emit('ok');
|
||||
})
|
||||
.finally(() => {
|
||||
confirmLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({ show });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="title"
|
||||
:width="480"
|
||||
:confirm-loading="confirmLoading"
|
||||
:destroy-on-hidden="true"
|
||||
:mask-closable="false"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form ref="formRef" :model="formState" :rules="formRules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<!-- 国际化: SAAS 终端号 -->
|
||||
<a-form-item :label="$t('payment.channel.hkrtIsv.pn')" name="pn">
|
||||
<a-input
|
||||
v-model:value="formState.pn"
|
||||
:placeholder="$t('payment.channel.hkrtIsv.pnPlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
@@ -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 { HkrtChannelMerchantApi } from '#/api/payment/channel/hkrt/channel-merchant.api';
|
||||
import ChannelMerchantNameEditModal from '#/views/payment/merchant/channel-merchant/detail/ChannelMerchantNameEditModal.vue';
|
||||
|
||||
import HkrtChannelMerchantBasicInfo from './HkrtChannelMerchantBasicInfo.vue';
|
||||
import HkrtMchConfigEdit from './HkrtMchConfigEdit.vue';
|
||||
|
||||
defineOptions({ name: 'HkrtMchManage' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'success'): void;
|
||||
}>();
|
||||
|
||||
const mchNo = ref('');
|
||||
const channelMchNo = ref('');
|
||||
const channelMerchant = ref<ChannelMerchantResult>({});
|
||||
const basicInfoRef = ref<InstanceType<typeof HkrtChannelMerchantBasicInfo>>();
|
||||
const configEditRef = ref<InstanceType<typeof HkrtMchConfigEdit>>();
|
||||
const editNameRef = ref<InstanceType<typeof ChannelMerchantNameEditModal>>();
|
||||
|
||||
/**
|
||||
* 功能卡片配置
|
||||
* 海科融通服务商模式, 商户配置(SAAS 终端号)
|
||||
*/
|
||||
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: 'mchConfig',
|
||||
// 国际化: 商户配置
|
||||
title: $t('payment.channel.hkrtIsv.cardMchConfig'),
|
||||
icon: 'ant-design:setting-outlined',
|
||||
description: $t('payment.channel.hkrtIsv.cardMchConfigDesc'),
|
||||
},
|
||||
{
|
||||
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') {
|
||||
basicInfoRef.value?.open();
|
||||
return;
|
||||
}
|
||||
if (card.key === 'editMerchantName') {
|
||||
editNameRef.value?.open();
|
||||
return;
|
||||
}
|
||||
if (card.key === 'mchConfig') {
|
||||
// 先查询当前 SAAS 终端号, 再打开配置弹窗
|
||||
HkrtChannelMerchantApi.findByChannelMchNo(channelMchNo.value).then(({ data }) => {
|
||||
configEditRef.value?.show(channelMchNo.value, data?.pn);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<HkrtChannelMerchantBasicInfo
|
||||
ref="basicInfoRef"
|
||||
:channel-mch-no="channelMchNo"
|
||||
:channel-merchant="channelMerchant"
|
||||
/>
|
||||
|
||||
<ChannelMerchantNameEditModal ref="editNameRef" :channel-merchant="channelMerchant" @success="emit('success')" />
|
||||
|
||||
<HkrtMchConfigEdit ref="configEditRef" />
|
||||
</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