feat(admin): 码牌预览二维码并精简编辑/新增流程

列表预览改为素二维码+扫码地址;编辑展示商户名称、隐藏关联应用;去掉单条新增入口。
This commit is contained in:
DaxPay Dev
2026-07-10 23:06:14 +08:00
parent d573a03a12
commit 960bf77765
5 changed files with 62 additions and 116 deletions

View File

@@ -22,13 +22,6 @@ export const DeviceQrCodeApi = {
return defHttp.get({ url: '/admin/device/qrcode/get', params: { id } });
},
/**
* 新增码牌(快捷绑定商户)
*/
add(data: DeviceQrCodeParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/qrcode/add', data });
},
/**
* 批量创建空白码牌
*/
@@ -77,6 +70,13 @@ export const DeviceQrCodeApi = {
changeStatus(id: string, status: string): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/qrcode/change-status', params: { id, status } });
},
/**
* 获取码牌扫码链接(完整 H5 地址)
*/
getCodeLink(code: string): Promise<Result<string>> {
return defHttp.get({ url: '/admin/device/qrcode/get-code-link', params: { code } });
},
};
/** 支付码牌查询参数 */
@@ -95,16 +95,14 @@ export interface DeviceQrCodeQuery {
status?: string;
}
/** 支付码牌参数 */
/** 支付码牌参数(编辑) */
export interface DeviceQrCodeParam {
/** 主键 */
id?: string;
/** 码牌名称 */
name?: string;
/** 商户号 */
/** 商户号(只读展示, 不提交归属变更) */
mchNo?: string;
/** 关联应用号(空=商户默认应用) */
appId?: string;
/** 金额类型 random/fixed */
amountType?: string;
/** 固定金额(分) */

View File

@@ -52,6 +52,7 @@
"viewCode": "View Code",
"qrCodeTitle": "QR Code",
"qrCodeTip": "Scan to enter the payment page",
"copyLink": "Copy Link",
"download": "Download QR Code",
"amountLabel": "Amount"
}

View File

@@ -52,6 +52,7 @@
"viewCode": "查看码牌",
"qrCodeTitle": "码牌二维码",
"qrCodeTip": "扫码即可进入支付页面",
"copyLink": "复制链接",
"download": "下载二维码",
"amountLabel": "金额"
}

View File

@@ -7,6 +7,7 @@
import { DeviceQrCodeApi, type DeviceQrCodeResult } from '#/api/payment/device/qrcode.api';
import { BQuery, type QueryField } from '#/components/query';
import { QrCode } from '#/components/qrcode';
import { PermCodes } from '#/constants/perm-codes';
import { useMessage } from '#/hooks/useMessage';
import { usePermission } from '#/hooks/usePermission';
@@ -88,7 +89,7 @@
// 查看码牌弹窗
const codeVisible = ref(false);
const currentCode = ref('');
const qrCodeUrl = ref('');
/**
* 同步勾选行
@@ -137,10 +138,6 @@
queryPage();
});
function handleAdd() {
editRef.value?.show();
}
function handleBatchCreate() {
batchCreateRef.value?.show();
}
@@ -223,19 +220,27 @@
}
/**
* 查看码牌编码
* 查看码牌二维码(拉取完整扫码链接后弹窗展示)
*/
function handleViewCode(row: DeviceQrCodeResult) {
currentCode.value = row.code || '';
codeVisible.value = true;
if (!row.code) {
return;
}
DeviceQrCodeApi.getCodeLink(row.code).then((res) => {
qrCodeUrl.value = res.data || '';
codeVisible.value = true;
});
}
/**
* 复制码牌编码
* 复制码牌扫码链接
*/
async function handleCopyCode() {
if (!qrCodeUrl.value) {
return;
}
try {
await navigator.clipboard.writeText(currentCode.value);
await navigator.clipboard.writeText(qrCodeUrl.value);
message.success($t('common.operationSuccess'));
} catch {
message.error($t('common.operationFail'));
@@ -262,9 +267,6 @@
@click="handleBatchCreate"
>{{ $t('payment.device.qrcode.batchCreate') }}</a-button
>
<a-button v-if="hasPermission(PermCodes.Device.QrCode.MANAGE)" @click="handleAdd">{{
$t('common.add')
}}</a-button>
<a-button
v-if="hasPermission(PermCodes.Device.QrCode.MANAGE)"
:disabled="selectedRows.length === 0"
@@ -402,7 +404,7 @@
<DeviceQrCodeBatchCreate ref="batchCreateRef" @ok="queryPage" />
<DeviceQrCodeBindMerchant ref="bindMerchantRef" @ok="queryPage" />
<!-- 查看码牌弹窗 -->
<!-- 查看码牌弹窗: 素二维码 + 下方扫码地址 -->
<a-modal
v-model:open="codeVisible"
:title="$t('payment.device.qrcode.qrCodeTitle')"
@@ -412,9 +414,12 @@
>
<div class="code-modal">
<p class="code-tip">{{ $t('payment.device.qrcode.qrCodeTip') }}</p>
<div class="code-value">{{ currentCode }}</div>
<a-button type="primary" block @click="handleCopyCode">
{{ $t('common.copy') }}
<div v-if="qrCodeUrl" class="qr-card">
<QrCode :value="qrCodeUrl" :width="250" :margin="0" />
</div>
<div class="code-url">{{ qrCodeUrl }}</div>
<a-button type="primary" block :disabled="!qrCodeUrl" @click="handleCopyCode">
{{ $t('payment.device.qrcode.copyLink') }}
</a-button>
</div>
</a-modal>
@@ -427,7 +432,7 @@
flex-direction: column;
align-items: center;
gap: 16px;
padding: 8px 0;
padding: 8px 0 4px;
}
.code-tip {
@@ -436,14 +441,20 @@
font-size: 13px;
}
.code-value {
width: 100%;
.qr-card {
padding: 12px;
background: var(--background-color);
background: #fff;
border: 1px solid var(--border-color, #e8e8e8);
border-radius: 8px;
}
.code-url {
width: 100%;
padding: 0 8px;
color: var(--text-color-secondary);
font-size: 12px;
line-height: 1.5;
text-align: center;
font-family: monospace;
font-size: 15px;
word-break: break-all;
}
</style>

View File

@@ -1,13 +1,9 @@
<script lang="ts" setup>
import type { LabelValue } from '#/types/web';
import { computed, ref } from 'vue';
import { $t } from '@vben/locales';
import { DeviceQrCodeApi, type DeviceQrCodeParam, type DeviceQrCodeResult } from '#/api/payment/device/qrcode.api';
import { MchAppInfoApi, type MchAppInfoResult } from '#/api/payment/merchant/mch-app-info.api';
import { MerchantApi } from '#/api/payment/merchant/merchant.api';
import { FormEditType } from '#/enums/formEditType';
import { useFormEdit } from '#/hooks/useFormEdit';
import { useMessage } from '#/hooks/useMessage';
@@ -18,20 +14,13 @@
const formRef = ref();
const { visible, confirmLoading, title, initFormEditType, handleCancel, formEditType } = useFormEdit();
const { visible, confirmLoading, title, initFormEditType, handleCancel } = useFormEdit();
// 是否编辑模式(编辑时商户/应用只读展示)
const isEdit = computed(() => formEditType.value === FormEditType.Edit);
// 商户下拉选项
const mchOptions = ref<LabelValue[]>([]);
// 应用下拉选项(根据商户联动)
const appOptions = ref<MchAppInfoResult[]>([]);
// 编辑态只读展示用商户名称(不参与提交)
const editMchName = ref('');
const formState = ref<DeviceQrCodeParam>({
name: '',
mchNo: '',
amountType: 'random',
});
@@ -44,11 +33,6 @@
name: [{ required: true, message: $t('payment.device.qrcode.validateName') }],
amountType: [{ required: true, message: $t('payment.device.qrcode.validateAmountType') }],
};
// 新增时商户必填; 编辑不改归属
if (!isEdit.value) {
rules.mchNo = [{ required: true, message: $t('payment.device.qrcode.validateMchNo') }];
}
// 固定金额类型时金额必填
if (isFixedAmount.value) {
rules.fixedAmount = [
{ required: true, message: $t('payment.device.qrcode.validateFixedAmount') },
@@ -62,49 +46,19 @@
return rules;
});
/**
* 加载商户下拉
*/
async function loadMchOptions() {
const { data } = await MerchantApi.dropdown();
mchOptions.value = data || [];
}
/**
* 商户切换时重新加载应用列表并清空已选应用
*/
async function handleMchChange() {
formState.value.appId = undefined;
appOptions.value = [];
if (formState.value.mchNo) {
const { data } = await MchAppInfoApi.page({ mchNo: formState.value.mchNo, size: 999 });
appOptions.value = data?.records || [];
}
}
/**
* 重置表单
*/
function resetForm() {
formState.value = {
name: '',
mchNo: '',
amountType: 'random',
fixedAmount: undefined,
};
appOptions.value = [];
editMchName.value = '';
formRef.value?.resetFields();
}
/**
* 打开新增弹窗
*/
function show() {
initFormEditType(FormEditType.Add);
resetForm();
loadMchOptions();
}
/**
* 打开编辑弹窗(归属只读)
*/
@@ -119,12 +73,12 @@
id: row.id!,
name: row.name,
mchNo: row.mchNo,
appId: row.appId,
amountType: row.amountType,
// 分转元展示
fixedAmount: row.fixedAmount ? row.fixedAmount / 100 : undefined,
remark: row.remark,
};
editMchName.value = row.mchName || '';
} finally {
confirmLoading.value = false;
}
@@ -144,13 +98,16 @@
try {
// 金额元转分, 在构造时一次性赋值避免对对象立即修改
const payload: DeviceQrCodeParam = {
...formState.value,
id: formState.value.id,
name: formState.value.name,
amountType: formState.value.amountType,
fixedAmount:
isFixedAmount.value && formState.value.fixedAmount
? Math.round(formState.value.fixedAmount * 100)
: undefined,
remark: formState.value.remark,
};
await (formEditType.value === FormEditType.Edit ? DeviceQrCodeApi.update(payload) : DeviceQrCodeApi.add(payload));
await DeviceQrCodeApi.update(payload);
message.success($t('common.operationSuccess'));
handleCancel();
emit('ok');
@@ -160,7 +117,7 @@
}
// import 放最后避免循环依赖(参照项目惯例)
defineExpose({ show, showEdit });
defineExpose({ showEdit });
</script>
<template>
@@ -183,36 +140,14 @@
:wrapper-col="{ span: 16 }"
class="form-compact"
>
<!-- 新增: 可选商户/应用; 编辑: 只读展示归属 -->
<a-form-item v-if="!isEdit" :label="$t('payment.device.qrcode.field.mchNo')" name="mchNo">
<a-select
v-model:value="formState.mchNo"
:options="mchOptions"
:placeholder="$t('payment.device.qrcode.pleaseSelectMch')"
show-search
option-filter-prop="label"
@change="handleMchChange"
/>
</a-form-item>
<a-form-item v-if="!isEdit" :label="$t('payment.device.qrcode.field.appId')">
<a-select
v-model:value="formState.appId"
:options="appOptions"
:field-names="{ label: 'appName', value: 'appId' }"
:placeholder="$t('payment.device.qrcode.pleaseSelectApp')"
allow-clear
/>
</a-form-item>
<a-form-item v-if="isEdit" :label="$t('payment.device.qrcode.field.mchNo')">
<a-tag v-if="formState.mchNo" color="blue">{{ formState.mchNo }}</a-tag>
<!-- 归属只读 -->
<a-form-item :label="$t('payment.device.qrcode.field.mchName')">
<a-tag v-if="formState.mchNo" color="blue">{{ editMchName || formState.mchNo }}</a-tag>
<a-tag v-else color="default">{{ $t('payment.device.qrcode.unbound') }}</a-tag>
</a-form-item>
<a-form-item v-if="isEdit" :label="$t('payment.device.qrcode.field.appId')">
<a-tag v-if="formState.appId" color="cyan">{{ formState.appId }}</a-tag>
<span v-else-if="formState.mchNo" style="color: var(--text-color-placeholder)">
{{ $t('payment.device.qrcode.defaultApp') }}
</span>
<span v-else style="color: var(--text-color-placeholder)">-</span>
<a-form-item :label="$t('payment.device.qrcode.field.mchNo')">
<a-tag v-if="formState.mchNo" color="blue">{{ formState.mchNo }}</a-tag>
<a-tag v-else color="default">{{ $t('payment.device.qrcode.unbound') }}</a-tag>
</a-form-item>
<!-- 码牌名称 -->
<a-form-item :label="$t('payment.device.qrcode.field.name')" name="name">