feat(payment): 新增微信域名验证文件管理端(平台全局视图/商户工作台/JSON上传/编辑弹窗/归属只读)

This commit is contained in:
DaxPay Dev
2026-07-09 12:40:06 +08:00
parent c909719801
commit 90ab8fee4c
12 changed files with 1213 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
import type { BaseEntity, PageResult, Result } from '#/types/web';
import { defHttp } from '#/api/request';
/**
* 微信域名验证文件 API平台级路径前缀 /admin/platform/wx-verify
* 运营在「支付配置」菜单管理所有验证文件(平台 + 商户),平台页为全局视图
*/
export const PlatformWxDomainVerifyApi = {
/**
* 分页查询平台级验证文件
*/
page(
params: PlatformWxDomainVerifyQuery & { current: number; size: number },
): Promise<Result<PlatformWxDomainVerifyPageResult>> {
return defHttp.get({ url: '/admin/platform/wx-verify/page', params });
},
/**
* 根据 id 查询详情
*/
get(id: string): Promise<Result<PlatformWxDomainVerifyVo>> {
return defHttp.get({ url: '/admin/platform/wx-verify/get', params: { id } });
},
/**
* 修改备注等元数据(备注)
*/
update(data: PlatformWxDomainVerifyParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/platform/wx-verify/update', data });
},
/**
* 删除
*/
delete(id: string): Promise<Result<void>> {
return defHttp.post({ url: '/admin/platform/wx-verify/delete', params: { id } });
},
/**
* 上传单个验证文件JSON 提交 fileName + fileContent
* @param data 文件名 + 文件内容 + 可选元数据
*/
upload(data: PlatformWxDomainVerifyUploadData): Promise<Result<PlatformWxDomainVerifyVo>> {
return defHttp.post({ url: '/admin/platform/wx-verify/upload', data });
},
};
/**
* 平台级验证文件查询参数
*/
export interface PlatformWxDomainVerifyQuery {
/** 文件名 */
fileName?: string;
/** 验证码 */
verifyCode?: string;
/** 归属筛选true-平台 / false-商户,不传=全部) */
platform?: boolean;
/** 商户号 */
mchNo?: string;
}
/**
* 平台级验证文件上传数据(前端读 .txt 内容后 JSON 提交)
*/
export interface PlatformWxDomainVerifyUploadData {
/** 文件名(如 MP_verify_xxx.txt */
fileName: string;
/** 文件内容(纯文本) */
fileContent: string;
/** 备注 */
remark?: string;
}
/**
* 平台级验证文件修改参数
*/
export interface PlatformWxDomainVerifyParam {
/** 主键 */
id: string;
/** 备注 */
remark?: string;
}
/**
* 平台级验证文件结果
*/
export interface PlatformWxDomainVerifyVo extends BaseEntity {
/** 商户号(平台级为空) */
mchNo?: string;
/** 是否平台级 */
platform?: boolean;
/** 文件名(如 MP_verify_xxx.txt */
fileName?: string;
/** 验证码(从文件名或内容解析) */
verifyCode?: string;
/** 文件内容 */
fileContent?: string;
/** 备注 */
remark?: string;
}
/**
* 平台级验证文件分页结果
*/
export type PlatformWxDomainVerifyPageResult = PageResult<PlatformWxDomainVerifyVo>;

View File

@@ -0,0 +1,103 @@
import type { MchEntity, PageResult, Result } from '#/types/web';
import { defHttp } from '#/api/request';
/**
* 微信域名验证文件 API商户级路径前缀 /admin/mch/wx-verify
* 运营在商户工作台代指定商户管理upload/page 均需传 mchNo
*/
export const MchWxDomainVerifyApi = {
/**
* 分页查询指定商户的验证文件
*/
page(
params: MchWxDomainVerifyQuery & { current: number; size: number },
): Promise<Result<MchWxDomainVerifyPageResult>> {
return defHttp.get({ url: '/admin/mch/wx-verify/page', params });
},
/**
* 根据 id 查询详情
*/
get(id: string): Promise<Result<MchWxDomainVerifyVo>> {
return defHttp.get({ url: '/admin/mch/wx-verify/get', params: { id } });
},
/**
* 修改备注等元数据(备注)
*/
update(data: MchWxDomainVerifyParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/mch/wx-verify/update', data });
},
/**
* 删除
*/
delete(id: string): Promise<Result<void>> {
return defHttp.post({ url: '/admin/mch/wx-verify/delete', params: { id } });
},
/**
* 上传单个验证文件JSON 提交 fileName + fileContent必须传 mchNo
* @param data 文件名 + 文件内容 + 可选元数据
* @param mchNo 商户号(必填,走 URL query
*/
upload(data: MchWxDomainVerifyUploadData, mchNo: string): Promise<Result<MchWxDomainVerifyVo>> {
return defHttp.post({ url: '/admin/mch/wx-verify/upload', params: { mchNo }, data });
},
};
/**
* 商户级验证文件查询参数mchNo 必填)
*/
export interface MchWxDomainVerifyQuery {
/** 商户号(必填) */
mchNo: string;
/** 文件名 */
fileName?: string;
/** 验证码 */
verifyCode?: string;
}
/**
* 商户级验证文件上传数据(前端读 .txt 内容后 JSON 提交)
*/
export interface MchWxDomainVerifyUploadData {
/** 文件名(如 MP_verify_xxx.txt */
fileName: string;
/** 文件内容(纯文本) */
fileContent: string;
/** 备注 */
remark?: string;
}
/**
* 商户级验证文件修改参数
*/
export interface MchWxDomainVerifyParam {
/** 主键 */
id: string;
/** 备注 */
remark?: string;
}
/**
* 商户级验证文件结果
*/
export interface MchWxDomainVerifyVo extends MchEntity {
/** 是否平台级 */
platform?: boolean;
/** 文件名(如 MP_verify_xxx.txt */
fileName?: string;
/** 验证码(从文件名或内容解析) */
verifyCode?: string;
/** 文件内容 */
fileContent?: string;
/** 备注 */
remark?: string;
}
/**
* 商户级验证文件分页结果
*/
export type MchWxDomainVerifyPageResult = PageResult<MchWxDomainVerifyVo>;

View File

@@ -79,6 +79,11 @@ export const PermCodes = {
VIEW: 'merchant:user:view',
MANAGE: 'merchant:user:manage',
},
/** 微信域名验证文件 menuCode=merchant:wx_verify */
WxDomainVerify: {
VIEW: 'merchant:wx_verify:view',
MANAGE: 'merchant:wx_verify:manage',
},
},
/** 渠道管理(独立顶级域) */
@@ -164,6 +169,13 @@ export const PermCodes = {
VIEW: 'payment:config:product_config:view',
MANAGE: 'payment:config:product_config:manage',
},
/** 微信域名验证文件 menuCode=payment:config:wx_verify */
Config: {
WxDomainVerify: {
VIEW: 'payment:config:wx_verify:view',
MANAGE: 'payment:config:wx_verify:manage',
},
},
/** 普通支付业务订单 menuCode=payment:order */
Order: {
VIEW: 'payment:order:view',

View File

@@ -18,6 +18,8 @@
"cardAppDesc": "Merchant app creation and management",
"cardCredentialConfig": "Credential Config",
"cardCredentialConfigDesc": "Merchant public key, communication key, platform public key and other credential configuration",
"cardWxDomainVerify": "WeChat Domain Verify File",
"cardWxDomainVerifyDesc": "Upload and manage WeChat Official Account / Mini Program business domain verification files",
"cardUser": "User Management",
"cardUserDesc": "User account management and permission assignment",
"cardStore": "Store Management",

View File

@@ -0,0 +1,23 @@
{
"title": "WeChat Domain Verify File",
"adminTitle": "WeChat Domain Verify File",
"upload": "Upload Verify File",
"uploadTip": "Select the WeChat domain verification .txt file",
"uploadSuccess": "Upload succeeded",
"uploadFail": "Upload failed",
"confirmDelete": "Are you sure to delete this verification file?",
"editTitle": "Edit Verify File",
"field": {
"fileName": "File Name",
"mchNo": "Merchant No.",
"belong": "Belong",
"verifyCode": "Verify Content",
"remark": "Remark",
"createTime": "Create Time",
"fileContent": "File Content"
},
"belong": {
"platform": "Platform",
"merchant": "Merchant"
}
}

View File

@@ -18,6 +18,8 @@
"cardAppDesc": "商户应用创建与管理",
"cardCredentialConfig": "对接配置",
"cardCredentialConfigDesc": "商户公钥、通信密钥、平台公钥等对接密钥配置",
"cardWxDomainVerify": "微信域名验证文件",
"cardWxDomainVerifyDesc": "上传与管理微信公众号 / 小程序业务域名校验文件",
"cardUser": "用户管理",
"cardUserDesc": "用户账号管理与权限分配",
"cardStore": "门店管理",

View File

@@ -0,0 +1,23 @@
{
"title": "微信域名验证文件",
"adminTitle": "微信域名验证文件",
"upload": "上传验证文件",
"uploadTip": "请选择微信下载的域名验证 .txt 文件",
"uploadSuccess": "上传成功",
"uploadFail": "上传失败",
"confirmDelete": "确定删除该验证文件吗?",
"editTitle": "编辑验证文件",
"field": {
"fileName": "文件名",
"mchNo": "商户号",
"belong": "归属",
"verifyCode": "验证内容",
"remark": "备注",
"createTime": "创建时间",
"fileContent": "文件内容"
},
"belong": {
"platform": "平台",
"merchant": "商户"
}
}

View File

@@ -0,0 +1,166 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { $t } from '@vben/locales';
import {
PlatformWxDomainVerifyApi,
type PlatformWxDomainVerifyParam,
type PlatformWxDomainVerifyVo,
} from '#/api/payment/config/wx-domain-verify.api';
import { FormEditType } from '#/enums/formEditType';
import { useFormEdit } from '#/hooks/useFormEdit';
import { useMessage } from '#/hooks/useMessage';
const emit = defineEmits(['ok']);
const { message } = useMessage();
const formRef = ref();
const { visible, confirmLoading, title, initFormEditType, handleCancel, showable } = useFormEdit();
// 表单数据
const formState = ref<PlatformWxDomainVerifyParam & { fileName?: string; verifyCode?: string; platform?: boolean }>({
id: '',
remark: '',
fileName: '',
verifyCode: '',
platform: false,
});
/**
* 重置表单
*/
function resetForm() {
formState.value = {
id: '',
remark: '',
fileName: '',
verifyCode: '',
platform: false,
};
formRef.value?.resetFields();
}
/**
* 加载详情数据并填充表单
*/
async function fillForm(record: PlatformWxDomainVerifyVo) {
confirmLoading.value = true;
try {
const { data } = await PlatformWxDomainVerifyApi.get(record.id!);
const row = data || record;
formState.value = {
id: row.id!,
remark: row.remark,
fileName: row.fileName,
verifyCode: row.verifyCode,
platform: row.platform,
};
} finally {
confirmLoading.value = false;
}
}
/**
* 打开编辑弹窗
*/
async function showEdit(record: PlatformWxDomainVerifyVo) {
initFormEditType(FormEditType.Edit);
resetForm();
await fillForm(record);
}
/**
* 打开查看弹窗
*/
async function showView(record: PlatformWxDomainVerifyVo) {
initFormEditType(FormEditType.Show);
resetForm();
await fillForm(record);
}
/**
* 保存
*/
async function handleOk() {
try {
await formRef.value?.validate();
} catch {
// 校验失败:表单已显示错误提示
return;
}
confirmLoading.value = true;
try {
await PlatformWxDomainVerifyApi.update({
id: formState.value.id,
remark: formState.value.remark,
});
message.success($t('common.saveSuccess'));
handleCancel();
emit('ok');
} finally {
confirmLoading.value = false;
}
}
defineExpose({ showEdit, showView });
</script>
<template>
<a-modal
v-model:open="visible"
:title="title"
:width="520"
:destroy-on-hidden="true"
:mask-closable="showable"
@cancel="handleCancel"
>
<a-spin :spinning="confirmLoading">
<a-form
ref="formRef"
:model="formState"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
class="form-compact"
>
<!-- 国际化文件名只读展示便于确认当前编辑的文件 -->
<a-form-item :label="$t('payment.wxVerify.field.fileName')">
<a-input :value="formState.fileName" disabled />
</a-form-item>
<!-- 国际化验证码只读展示 -->
<a-form-item :label="$t('payment.wxVerify.field.verifyCode')">
<a-input :value="formState.verifyCode" disabled />
</a-form-item>
<!-- 国际化归属只读不可更改 -->
<a-form-item :label="$t('payment.wxVerify.field.belong')">
<a-tag v-if="formState.platform" color="blue">
{{ $t('payment.wxVerify.belong.platform') }}
</a-tag>
<a-tag v-else color="green">
{{ $t('payment.wxVerify.belong.merchant') }}
</a-tag>
</a-form-item>
<!-- 国际化备注 -->
<a-form-item :label="$t('payment.wxVerify.field.remark')" name="remark">
<a-textarea
v-model:value="formState.remark"
:rows="3"
:disabled="showable"
:placeholder="$t('common.pleaseInput')"
/>
</a-form-item>
</a-form>
</a-spin>
<template #footer>
<a-space>
<a-button @click="handleCancel">{{ showable ? $t('common.close') : $t('common.cancel') }}</a-button>
<a-button v-if="!showable" type="primary" :loading="confirmLoading" @click="handleOk">
{{ $t('common.save') }}
</a-button>
</a-space>
</template>
</a-modal>
</template>

View File

@@ -0,0 +1,300 @@
<script lang="ts" setup>
import type { VxeTableInstance, VxeToolbarInstance } from 'vxe-table';
import { computed, onMounted, ref } from 'vue';
import { $t } from '@vben/locales';
import { IconifyIcon } from '@vben-core/icons';
import {
PlatformWxDomainVerifyApi,
type PlatformWxDomainVerifyQuery,
type PlatformWxDomainVerifyVo,
} from '#/api/payment/config/wx-domain-verify.api';
import { BQuery, type QueryField } from '#/components/query';
import { PermCodes } from '#/constants/perm-codes';
import { useMessage } from '#/hooks/useMessage';
import { usePermission } from '#/hooks/usePermission';
import PlatformWxDomainVerifyEdit from './PlatformWxDomainVerifyEdit.vue';
defineOptions({ name: 'PlatformWxDomainVerifyList' });
const { confirm, message } = useMessage();
const { hasPermission } = usePermission();
const loading = ref(false);
// 上传中状态
const uploading = ref(false);
const xTable = ref<VxeTableInstance>();
const xToolbar = ref<VxeToolbarInstance>();
// 查询条件
const queryForm = ref<PlatformWxDomainVerifyQuery>({});
const pageConfig = ref({
currentPage: 1,
pageSize: 10,
total: 0,
});
const tableData = ref<PlatformWxDomainVerifyVo[]>([]);
// 编辑弹窗
const editRef = ref();
// 查询字段(全局视图:可按归属 / 商户号筛选)
const queryFields = computed<QueryField[]>(() => [
{
type: 'string',
field: 'fileName',
name: $t('payment.wxVerify.field.fileName'),
placeholder: $t('common.pleaseInput'),
},
{
type: 'string',
field: 'verifyCode',
name: $t('payment.wxVerify.field.verifyCode'),
placeholder: $t('common.pleaseInput'),
},
{
type: 'list',
field: 'platform',
name: $t('payment.wxVerify.field.belong'),
placeholder: $t('common.pleaseSelect'),
selectList: [
{ label: $t('payment.wxVerify.belong.platform'), value: true },
{ label: $t('payment.wxVerify.belong.merchant'), value: false },
],
},
{
type: 'string',
field: 'mchNo',
name: $t('payment.wxVerify.field.mchNo'),
placeholder: $t('common.pleaseInput'),
},
]);
/**
* 分页查询平台级验证文件列表
*/
function queryPage() {
loading.value = true;
return PlatformWxDomainVerifyApi.page({
current: pageConfig.value.currentPage,
size: pageConfig.value.pageSize,
...queryForm.value,
})
.then((res) => {
tableData.value = res.data?.records || [];
pageConfig.value.total = Number(res.data?.total) || 0;
loading.value = false;
})
.catch(() => {
loading.value = false;
});
}
function resetQuery() {
queryForm.value = {};
pageConfig.value.currentPage = 1;
queryPage();
}
function handlePageChange({ currentPage, pageSize }: { currentPage: number; pageSize: number }) {
pageConfig.value.currentPage = currentPage;
pageConfig.value.pageSize = pageSize;
queryPage();
}
/**
* a-upload 选择文件回调(单选),读取文件内容后 JSON 提交上传
*/
async function handleBeforeUpload(file: File) {
uploading.value = true;
try {
const fileContent = await file.text();
await PlatformWxDomainVerifyApi.upload({ fileName: file.name, fileContent });
message.success($t('payment.wxVerify.uploadSuccess'));
queryPage();
} catch {
// 错误消息已由全局响应拦截器统一提示
} finally {
uploading.value = false;
}
// 返回 false 阻止 a-upload 自动上传
return false;
}
/**
* 编辑
*/
function handleEdit(row: PlatformWxDomainVerifyVo) {
editRef.value?.showEdit(row);
}
/**
* 查看
*/
function handleView(row: PlatformWxDomainVerifyVo) {
editRef.value?.showView(row);
}
/**
* 删除验证文件(危险操作,二次确认)
*/
function handleDelete(row: PlatformWxDomainVerifyVo) {
confirm({
// 国际化:确认
title: $t('common.confirm'),
content: $t('payment.wxVerify.confirmDelete'),
okText: $t('common.delete'),
cancelText: $t('common.cancel'),
onOk() {
return PlatformWxDomainVerifyApi.delete(row.id!).then(() => {
message.success($t('common.deleteSuccess'));
queryPage();
});
},
});
}
onMounted(() => {
xTable.value?.connectToolbar(xToolbar.value as VxeToolbarInstance);
queryPage();
});
</script>
<template>
<div class="m-3 p-3 bg-background rounded-lg list-page-compact">
<a-card>
<BQuery :fields="queryFields" :query-params="queryForm" @query="queryPage" @reset="resetQuery" />
</a-card>
<div class="mt-4">
<a-card>
<vxe-toolbar ref="xToolbar" custom refresh :refresh-options="{ queryMethod: queryPage }">
<template #buttons>
<a-upload
:show-upload-list="false"
accept=".txt"
:before-upload="handleBeforeUpload"
>
<a-button
v-if="hasPermission(PermCodes.Payment.Config.WxDomainVerify.MANAGE)"
type="primary"
:loading="uploading"
>
<template #icon>
<IconifyIcon icon="ant-design:upload-outlined" />
</template>
<!-- 国际化上传验证文件 -->
{{ $t('payment.wxVerify.upload') }}
</a-button>
</a-upload>
</template>
</vxe-toolbar>
<vxe-table ref="xTable" :row-config="{ keyField: 'id' }" :data="tableData" :loading="loading">
<vxe-column type="seq" :title="$t('common.seq')" width="60" align="center" />
<!-- 国际化文件名 -->
<vxe-column field="fileName" :title="$t('payment.wxVerify.field.fileName')" :min-width="220">
<template #default="{ row }">
<a
v-if="hasPermission(PermCodes.Payment.Config.WxDomainVerify.VIEW)"
href="javascript:"
class="vben-link"
@click="handleView(row)"
>{{ row.fileName }}</a
>
<span v-else>{{ row.fileName }}</span>
</template>
</vxe-column>
<!-- 国际化归属 -->
<vxe-column
field="platform"
:title="$t('payment.wxVerify.field.belong')"
:width="100"
align="center"
>
<template #default="{ row }">
<a-tag v-if="row.platform" color="blue">
{{ $t('payment.wxVerify.belong.platform') }}
</a-tag>
<a-tag v-else color="green">
{{ $t('payment.wxVerify.belong.merchant') }}
</a-tag>
</template>
</vxe-column>
<!-- 国际化商户号 -->
<vxe-column
field="mchNo"
:title="$t('payment.wxVerify.field.mchNo')"
:min-width="140"
show-overflow
>
<template #default="{ row }">
{{ row.platform ? '-' : row.mchNo }}
</template>
</vxe-column>
<!-- 国际化验证码 -->
<vxe-column
field="verifyCode"
:title="$t('payment.wxVerify.field.verifyCode')"
:min-width="200"
show-overflow
/>
<!-- 国际化备注 -->
<vxe-column
field="remark"
:title="$t('payment.wxVerify.field.remark')"
:min-width="160"
show-overflow
/>
<!-- 国际化创建时间 -->
<vxe-column
field="createTime"
:title="$t('payment.wxVerify.field.createTime')"
:min-width="180"
formatter="formatDateTime"
/>
<vxe-column fixed="right" :width="140" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">
<a-space :size="2">
<template #separator>
<a-divider type="vertical" />
</template>
<a-button
v-if="hasPermission(PermCodes.Payment.Config.WxDomainVerify.MANAGE)"
type="link"
size="small"
@click="handleEdit(row)"
>{{ $t('common.edit') }}</a-button
>
<a-button
v-if="hasPermission(PermCodes.Payment.Config.WxDomainVerify.MANAGE)"
type="link"
size="small"
danger
@click="handleDelete(row)"
>{{ $t('common.delete') }}</a-button
>
</a-space>
</template>
</vxe-column>
</vxe-table>
<vxe-pager
size="medium"
:loading="loading"
:current-page="pageConfig.currentPage"
:page-size="pageConfig.pageSize"
:total="pageConfig.total"
@page-change="handlePageChange"
/>
</a-card>
</div>
<!-- 编辑弹窗 -->
<PlatformWxDomainVerifyEdit ref="editRef" @ok="queryPage" />
</div>
</template>

View File

@@ -101,6 +101,15 @@
description: $t('payment.merchant.workbench.workbench.cardCredentialConfigDesc'),
route: '/payment/merchant/manage/credential',
},
{
key: 'wxDomainVerify',
// 国际化:微信域名验证文件
title: $t('payment.merchant.workbench.workbench.cardWxDomainVerify'),
icon: 'ant-design:safety-certificate-outlined',
// 国际化:上传与管理微信公众号 / 小程序业务域名校验文件
description: $t('payment.merchant.workbench.workbench.cardWxDomainVerifyDesc'),
route: '/payment/merchant/manage/wx-verify',
},
],
},
]);

View File

@@ -0,0 +1,166 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { $t } from '@vben/locales';
import {
MchWxDomainVerifyApi,
type MchWxDomainVerifyParam,
type MchWxDomainVerifyVo,
} from '#/api/payment/merchant/mch-wx-domain-verify.api';
import { FormEditType } from '#/enums/formEditType';
import { useFormEdit } from '#/hooks/useFormEdit';
import { useMessage } from '#/hooks/useMessage';
const emit = defineEmits(['ok']);
const { message } = useMessage();
const formRef = ref();
const { visible, confirmLoading, title, initFormEditType, handleCancel, showable } = useFormEdit();
// 表单数据
const formState = ref<MchWxDomainVerifyParam & { fileName?: string; verifyCode?: string; platform?: boolean }>({
id: '',
remark: '',
fileName: '',
verifyCode: '',
platform: false,
});
/**
* 重置表单
*/
function resetForm() {
formState.value = {
id: '',
remark: '',
fileName: '',
verifyCode: '',
platform: false,
};
formRef.value?.resetFields();
}
/**
* 加载详情数据并填充表单
*/
async function fillForm(record: MchWxDomainVerifyVo) {
confirmLoading.value = true;
try {
const { data } = await MchWxDomainVerifyApi.get(record.id!);
const row = data || record;
formState.value = {
id: row.id!,
remark: row.remark,
fileName: row.fileName,
verifyCode: row.verifyCode,
platform: row.platform,
};
} finally {
confirmLoading.value = false;
}
}
/**
* 打开编辑弹窗
*/
async function showEdit(record: MchWxDomainVerifyVo) {
initFormEditType(FormEditType.Edit);
resetForm();
await fillForm(record);
}
/**
* 打开查看弹窗
*/
async function showView(record: MchWxDomainVerifyVo) {
initFormEditType(FormEditType.Show);
resetForm();
await fillForm(record);
}
/**
* 保存
*/
async function handleOk() {
try {
await formRef.value?.validate();
} catch {
// 校验失败:表单已显示错误提示
return;
}
confirmLoading.value = true;
try {
await MchWxDomainVerifyApi.update({
id: formState.value.id,
remark: formState.value.remark,
});
message.success($t('common.saveSuccess'));
handleCancel();
emit('ok');
} finally {
confirmLoading.value = false;
}
}
defineExpose({ showEdit, showView });
</script>
<template>
<a-modal
v-model:open="visible"
:title="title"
:width="520"
:destroy-on-hidden="true"
:mask-closable="showable"
@cancel="handleCancel"
>
<a-spin :spinning="confirmLoading">
<a-form
ref="formRef"
:model="formState"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
class="form-compact"
>
<!-- 国际化文件名只读展示便于确认当前编辑的文件 -->
<a-form-item :label="$t('payment.wxVerify.field.fileName')">
<a-input :value="formState.fileName" disabled />
</a-form-item>
<!-- 国际化验证码只读展示 -->
<a-form-item :label="$t('payment.wxVerify.field.verifyCode')">
<a-input :value="formState.verifyCode" disabled />
</a-form-item>
<!-- 国际化归属只读不可更改 -->
<a-form-item :label="$t('payment.wxVerify.field.belong')">
<a-tag v-if="formState.platform" color="blue">
{{ $t('payment.wxVerify.belong.platform') }}
</a-tag>
<a-tag v-else color="green">
{{ $t('payment.wxVerify.belong.merchant') }}
</a-tag>
</a-form-item>
<!-- 国际化备注 -->
<a-form-item :label="$t('payment.wxVerify.field.remark')" name="remark">
<a-textarea
v-model:value="formState.remark"
:rows="3"
:disabled="showable"
:placeholder="$t('common.pleaseInput')"
/>
</a-form-item>
</a-form>
</a-spin>
<template #footer>
<a-space>
<a-button @click="handleCancel">{{ showable ? $t('common.close') : $t('common.cancel') }}</a-button>
<a-button v-if="!showable" type="primary" :loading="confirmLoading" @click="handleOk">
{{ $t('common.save') }}
</a-button>
</a-space>
</template>
</a-modal>
</template>

View File

@@ -0,0 +1,301 @@
<script lang="ts" setup>
import type { VxeTableInstance, VxeToolbarInstance } from 'vxe-table';
import { computed, onMounted, ref } from 'vue';
import { $t } from '@vben/locales';
import { IconifyIcon } from '@vben-core/icons';
import { MerchantApi, type MerchantInfo } from '#/api/payment/merchant/merchant.api';
import {
MchWxDomainVerifyApi,
type MchWxDomainVerifyQuery,
type MchWxDomainVerifyVo,
} from '#/api/payment/merchant/mch-wx-domain-verify.api';
import { BQuery, type QueryField } from '#/components/query';
import RouteQueryMissingState from '#/components/route/RouteQueryMissingState.vue';
import { PermCodes } from '#/constants/perm-codes';
import { useMessage } from '#/hooks/useMessage';
import { usePermission } from '#/hooks/usePermission';
import { useRequiredRouteQuery } from '#/hooks/useRequiredRouteQuery';
import MchWxDomainVerifyEdit from './MchWxDomainVerifyEdit.vue';
defineOptions({ name: 'MchWxDomainVerifyList' });
const { confirm, message } = useMessage();
const { hasPermission } = usePermission();
// 必填路由参数守卫:缺少 mchNo 时提示并回退
const routeContext = useRequiredRouteQuery({
keys: ['mchNo'],
messageKey: 'payment.common.route.missingMchNo',
fallbackPath: '/payment/merchant',
});
const mchNo = computed(() => routeContext.query.value.mchNo);
const loading = ref(false);
// 上传中状态
const uploading = ref(false);
const xTable = ref<VxeTableInstance>();
const xToolbar = ref<VxeToolbarInstance>();
const merchantInfo = ref<MerchantInfo>({});
// 查询条件mchNo 固定来自路由)
const queryForm = ref<Omit<MchWxDomainVerifyQuery, 'mchNo'>>({});
const pageConfig = ref({
currentPage: 1,
pageSize: 10,
total: 0,
});
const tableData = ref<MchWxDomainVerifyVo[]>([]);
// 编辑弹窗
const editRef = ref();
// 查询字段(不含商户号,固定单商户)
const queryFields = computed<QueryField[]>(() => [
{
type: 'string',
field: 'fileName',
name: $t('payment.wxVerify.field.fileName'),
placeholder: $t('common.pleaseInput'),
},
{
type: 'string',
field: 'verifyCode',
name: $t('payment.wxVerify.field.verifyCode'),
placeholder: $t('common.pleaseInput'),
},
]);
/**
* 加载商户信息(展示商户名)
*/
async function loadMerchantInfo() {
if (!mchNo.value) return;
const { data } = await MerchantApi.findByMchNo(mchNo.value);
merchantInfo.value = data || {};
}
/**
* 分页查询指定商户的验证文件列表
*/
function queryPage() {
if (!mchNo.value) return Promise.resolve();
loading.value = true;
return MchWxDomainVerifyApi.page({
current: pageConfig.value.currentPage,
size: pageConfig.value.pageSize,
mchNo: mchNo.value,
...queryForm.value,
})
.then((res) => {
tableData.value = res.data?.records || [];
pageConfig.value.total = Number(res.data?.total) || 0;
loading.value = false;
})
.catch(() => {
loading.value = false;
});
}
function resetQuery() {
queryForm.value = {};
pageConfig.value.currentPage = 1;
queryPage();
}
function handlePageChange({ currentPage, pageSize }: { currentPage: number; pageSize: number }) {
pageConfig.value.currentPage = currentPage;
pageConfig.value.pageSize = pageSize;
queryPage();
}
/**
* a-upload 选择文件回调(单选),读取文件内容后 JSON 提交上传
*/
async function handleBeforeUpload(file: File) {
if (!mchNo.value) {
return false;
}
uploading.value = true;
try {
const fileContent = await file.text();
await MchWxDomainVerifyApi.upload({ fileName: file.name, fileContent }, mchNo.value);
message.success($t('payment.wxVerify.uploadSuccess'));
queryPage();
} catch {
// 错误消息已由全局响应拦截器统一提示
} finally {
uploading.value = false;
}
// 返回 false 阻止 a-upload 自动上传
return false;
}
/**
* 编辑
*/
function handleEdit(row: MchWxDomainVerifyVo) {
editRef.value?.showEdit(row);
}
/**
* 查看
*/
function handleView(row: MchWxDomainVerifyVo) {
editRef.value?.showView(row);
}
/**
* 删除验证文件(危险操作,二次确认)
*/
function handleDelete(row: MchWxDomainVerifyVo) {
confirm({
// 国际化:确认
title: $t('common.confirm'),
content: $t('payment.wxVerify.confirmDelete'),
okText: $t('common.delete'),
cancelText: $t('common.cancel'),
onOk() {
return MchWxDomainVerifyApi.delete(row.id!).then(() => {
message.success($t('common.deleteSuccess'));
queryPage();
});
},
});
}
onMounted(() => {
if (!routeContext.isValid.value) {
return;
}
loadMerchantInfo();
xTable.value?.connectToolbar(xToolbar.value as VxeToolbarInstance);
queryPage();
});
</script>
<template>
<RouteQueryMissingState
v-if="!routeContext.isValid"
:description="$t('payment.common.route.missingMchNo')"
:back-text="$t('payment.merchant.workbench.workbench.backToList')"
@back="routeContext.goFallback"
/>
<div v-else class="m-4">
<a-card variant="borderless" class="rounded-xl shadow-sm">
<template #title>
<div class="flex items-center gap-2">
<span class="text-lg font-bold text-foreground">{{ $t('payment.wxVerify.title') }}</span>
<span v-if="merchantInfo.mchName" class="text-sm text-muted-foreground"
>({{ merchantInfo.mchName }})</span
>
</div>
</template>
<BQuery :fields="queryFields" :query-params="queryForm" @query="queryPage" @reset="resetQuery" />
<div class="mt-4">
<vxe-toolbar ref="xToolbar" custom refresh :refresh-options="{ queryMethod: queryPage }">
<template #buttons>
<a-upload
:show-upload-list="false"
accept=".txt"
:before-upload="handleBeforeUpload"
>
<a-button
v-if="hasPermission(PermCodes.Merchant.WxDomainVerify.MANAGE)"
type="primary"
:loading="uploading"
>
<template #icon>
<IconifyIcon icon="ant-design:upload-outlined" />
</template>
<!-- 国际化上传验证文件 -->
{{ $t('payment.wxVerify.upload') }}
</a-button>
</a-upload>
</template>
</vxe-toolbar>
<vxe-table ref="xTable" :row-config="{ keyField: 'id' }" :data="tableData" :loading="loading">
<vxe-column type="seq" :title="$t('common.seq')" width="60" align="center" />
<!-- 国际化文件名 -->
<vxe-column field="fileName" :title="$t('payment.wxVerify.field.fileName')" :min-width="220">
<template #default="{ row }">
<a
v-if="hasPermission(PermCodes.Merchant.WxDomainVerify.VIEW)"
href="javascript:"
class="vben-link"
@click="handleView(row)"
>{{ row.fileName }}</a
>
<span v-else>{{ row.fileName }}</span>
</template>
</vxe-column>
<!-- 国际化验证码 -->
<vxe-column
field="verifyCode"
:title="$t('payment.wxVerify.field.verifyCode')"
:min-width="200"
show-overflow
/>
<!-- 国际化备注 -->
<vxe-column
field="remark"
:title="$t('payment.wxVerify.field.remark')"
:min-width="160"
show-overflow
/>
<!-- 国际化创建时间 -->
<vxe-column
field="createTime"
:title="$t('payment.wxVerify.field.createTime')"
:min-width="180"
formatter="formatDateTime"
/>
<vxe-column fixed="right" :width="140" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">
<a-space :size="2">
<template #separator>
<a-divider type="vertical" />
</template>
<a-button
v-if="hasPermission(PermCodes.Merchant.WxDomainVerify.MANAGE)"
type="link"
size="small"
@click="handleEdit(row)"
>{{ $t('common.edit') }}</a-button
>
<a-button
v-if="hasPermission(PermCodes.Merchant.WxDomainVerify.MANAGE)"
type="link"
size="small"
danger
@click="handleDelete(row)"
>{{ $t('common.delete') }}</a-button
>
</a-space>
</template>
</vxe-column>
</vxe-table>
<vxe-pager
size="medium"
:loading="loading"
:current-page="pageConfig.currentPage"
:page-size="pageConfig.pageSize"
:total="pageConfig.total"
@page-change="handlePageChange"
/>
</div>
</a-card>
<!-- 编辑弹窗 -->
<MchWxDomainVerifyEdit ref="editRef" @ok="queryPage" />
</div>
</template>