feat(hkrt): 海科融通服务商配置与通道商户管理(管理端)

This commit is contained in:
DaxPay Dev
2026-07-07 21:02:30 +08:00
parent cc4f127886
commit c392c1a31e
11 changed files with 980 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -0,0 +1,3 @@
/** 海科融通渠道 barrel */
export * from './channel-merchant.api';
export * from './pay-config.api';

View File

@@ -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;
}

View File

@@ -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"
}
}

View File

@@ -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终端号"
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

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 { 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>