mirror of
https://gitee.com/bootx/dax-pay-ui
synced 2026-08-14 17:35:36 +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