refactor(admin): 开源版移除云音箱/云打印/厂商设备管理

硬件对接归商业版; 保留码牌管理与相关权限。
This commit is contained in:
DaxPay Dev
2026-07-11 10:15:50 +08:00
parent 960bf77765
commit 27b21bfccb
21 changed files with 1 additions and 2229 deletions

View File

@@ -1,123 +0,0 @@
import type { BaseEntity, PageResult, Result } from '#/types/web';
import { defHttp } from '#/api/request';
/**
* 云打印设备 API
*/
export const DevicePrinterApi = {
/**
* 分页查询云打印设备
*/
page(
params: DevicePrinterQuery & { current?: number; size?: number },
): Promise<Result<PageResult<DevicePrinterResult>>> {
return defHttp.get({ url: '/admin/device/printer/page', params });
},
/**
* 根据 id 查询云打印设备
*/
get(id: string): Promise<Result<DevicePrinterResult>> {
return defHttp.get({ url: '/admin/device/printer/get', params: { id } });
},
/**
* 新增云打印设备
*/
add(data: DevicePrinterParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/printer/add', data });
},
/**
* 修改云打印设备
*/
update(data: DevicePrinterParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/printer/update', data });
},
/**
* 删除云打印设备
*/
delete(id: string): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/printer/delete', params: { id } });
},
/**
* 绑定云打印设备(首期仅更新本地状态, 真实设备对接由独立服务完成)
*/
bind(id: string): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/printer/bind', params: { id } });
},
/**
* 解绑云打印设备
*/
unbind(id: string): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/printer/unbind', params: { id } });
},
};
/** 云打印设备查询参数 */
export interface DevicePrinterQuery {
/** 商户号 */
mchNo?: string;
/** 厂商代码 */
vendorCode?: string;
/** 设备序列号 */
deviceSn?: string;
/** 设备名称 */
deviceName?: string;
/** 厂商门店ID */
shopId?: string;
/** 设备状态 unbound/online/offline/fault */
status?: string;
}
/** 云打印设备参数 */
export interface DevicePrinterParam {
/** 主键 */
id?: string;
/** 商户号 */
mchNo?: string;
/** 厂商代码 */
vendorCode?: string;
/** 厂商配置ID */
vendorConfigId?: string;
/** 设备序列号 */
deviceSn?: string;
/** 设备IMEI */
imei?: string;
/** 厂商门店ID */
shopId?: string;
/** 设备名称 */
deviceName?: string;
/** 备注 */
remark?: string;
}
/** 云打印设备结果 */
export interface DevicePrinterResult extends BaseEntity {
/** 商户号 */
mchNo?: string;
/** 厂商代码 */
vendorCode?: string;
/** 厂商配置ID */
vendorConfigId?: string;
/** 设备序列号 */
deviceSn?: string;
/** 设备IMEI */
imei?: string;
/** 厂商门店ID */
shopId?: string;
/** 设备名称 */
deviceName?: string;
/** 设备状态 */
status?: string;
/** 绑定时间 */
bindTime?: string;
/** 最后在线时间 */
lastOnlineTime?: string;
/** 备注 */
remark?: string;
}

View File

@@ -1,123 +0,0 @@
import type { BaseEntity, PageResult, Result } from '#/types/web';
import { defHttp } from '#/api/request';
/**
* 云音箱设备 API
*/
export const DeviceSpeakerApi = {
/**
* 分页查询云音箱设备
*/
page(
params: DeviceSpeakerQuery & { current?: number; size?: number },
): Promise<Result<PageResult<DeviceSpeakerResult>>> {
return defHttp.get({ url: '/admin/device/speaker/page', params });
},
/**
* 根据 id 查询云音箱设备
*/
get(id: string): Promise<Result<DeviceSpeakerResult>> {
return defHttp.get({ url: '/admin/device/speaker/get', params: { id } });
},
/**
* 新增云音箱设备
*/
add(data: DeviceSpeakerParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/speaker/add', data });
},
/**
* 修改云音箱设备
*/
update(data: DeviceSpeakerParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/speaker/update', data });
},
/**
* 删除云音箱设备
*/
delete(id: string): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/speaker/delete', params: { id } });
},
/**
* 绑定云音箱设备(首期仅更新本地状态, 真实设备对接由独立服务完成)
*/
bind(id: string): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/speaker/bind', params: { id } });
},
/**
* 解绑云音箱设备
*/
unbind(id: string): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/speaker/unbind', params: { id } });
},
};
/** 云音箱设备查询参数 */
export interface DeviceSpeakerQuery {
/** 商户号 */
mchNo?: string;
/** 厂商代码 */
vendorCode?: string;
/** 设备序列号 */
deviceSn?: string;
/** 设备名称 */
deviceName?: string;
/** 厂商门店ID */
shopId?: string;
/** 设备状态 unbound/online/offline/fault */
status?: string;
}
/** 云音箱设备参数 */
export interface DeviceSpeakerParam {
/** 主键 */
id?: string;
/** 商户号 */
mchNo?: string;
/** 厂商代码 */
vendorCode?: string;
/** 厂商配置ID */
vendorConfigId?: string;
/** 设备序列号 */
deviceSn?: string;
/** 设备IMEI */
imei?: string;
/** 厂商门店ID */
shopId?: string;
/** 设备名称 */
deviceName?: string;
/** 备注 */
remark?: string;
}
/** 云音箱设备结果 */
export interface DeviceSpeakerResult extends BaseEntity {
/** 商户号 */
mchNo?: string;
/** 厂商代码 */
vendorCode?: string;
/** 厂商配置ID */
vendorConfigId?: string;
/** 设备序列号 */
deviceSn?: string;
/** 设备IMEI */
imei?: string;
/** 厂商门店ID */
shopId?: string;
/** 设备名称 */
deviceName?: string;
/** 设备状态 */
status?: string;
/** 绑定时间 */
bindTime?: string;
/** 最后在线时间 */
lastOnlineTime?: string;
/** 备注 */
remark?: string;
}

View File

@@ -1,112 +0,0 @@
import type { BaseEntity, PageResult, Result } from '#/types/web';
import { defHttp } from '#/api/request';
/**
* 设备厂商配置 API
*/
export const DeviceVendorConfigApi = {
/**
* 分页查询厂商配置
*/
page(
params: DeviceVendorConfigQuery & { current?: number; size?: number },
): Promise<Result<PageResult<DeviceVendorConfigResult>>> {
return defHttp.get({ url: '/admin/device/vendor-config/page', params });
},
/**
* 根据 id 查询厂商配置
*/
get(id: string): Promise<Result<DeviceVendorConfigResult>> {
return defHttp.get({ url: '/admin/device/vendor-config/get', params: { id } });
},
/**
* 新增厂商配置
*/
add(data: DeviceVendorConfigParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/vendor-config/add', data });
},
/**
* 修改厂商配置
*/
update(data: DeviceVendorConfigParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/vendor-config/update', data });
},
/**
* 删除厂商配置
*/
delete(id: string): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/vendor-config/delete', params: { id } });
},
/**
* 查询指定设备类型和厂商的启用配置列表(设备下拉用)
*/
listEnabledByVendor(
deviceType: string,
vendorCode: string,
): Promise<Result<DeviceVendorConfigResult[]>> {
return defHttp.get({
url: '/admin/device/vendor-config/list-enabled-by-vendor',
params: { deviceType, vendorCode },
});
},
};
/** 厂商配置查询参数 */
export interface DeviceVendorConfigQuery {
/** 设备类型(顶层厂商配置页可不传, 按厂商聚合查询) */
deviceType?: string;
/** 厂商代码 */
vendorCode?: string;
/** 配置名称 */
configName?: string;
/** 是否启用 */
enable?: boolean;
}
/** 厂商配置参数 */
export interface DeviceVendorConfigParam {
/** 主键 */
id?: string;
/** 设备类型 */
deviceType?: string;
/** 厂商代码 */
vendorCode?: string;
/** 配置名称 */
configName?: string;
/** 厂商应用ID */
appId?: string;
/** 厂商应用密钥 */
appSecret?: string;
/** 是否启用 */
enable?: boolean;
/** 扩展参数(JSON) */
extParam?: string;
/** 备注 */
remark?: string;
}
/** 厂商配置结果 */
export interface DeviceVendorConfigResult extends BaseEntity {
/** 设备类型 */
deviceType?: string;
/** 厂商代码 */
vendorCode?: string;
/** 配置名称 */
configName?: string;
/** 厂商应用ID */
appId?: string;
/** 厂商应用密钥(脱敏) */
appSecret?: string;
/** 是否启用 */
enable?: boolean;
/** 扩展参数(JSON) */
extParam?: string;
/** 备注 */
remark?: string;
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64"><circle cx="32" cy="32" r="30" fill="#52c41a"/><text x="32" y="32" font-size="28" font-weight="bold" fill="#fff" text-anchor="middle" dominant-baseline="central" font-family="sans-serif"></text></svg>

Before

Width:  |  Height:  |  Size: 286 B

View File

@@ -1 +0,0 @@
<svg t="1782370182964" class="icon" viewBox="0 0 5152 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6267" width="200" height="200"><path d="M1897.987261 0v704.407643c0 71.745223-52.178344 130.44586-123.923567 130.44586h-247.847133c-65.22293 0-123.923567-58.700637-123.923567-130.44586V0h-247.847134v769.630573c0 143.490446 110.878981 254.369427 247.847134 254.369427h482.649681c136.968153 0 247.847134-117.401274 247.847134-254.369427V0h-234.802548zM737.019108 417.426752H352.203822c-58.700637 0-104.356688-52.178344-104.356688-110.878981 0-58.700637 45.656051-110.878981 104.356688-110.878981H952.254777V0H247.847134C110.878981 0 0 117.401274 0 254.369427v97.834395c0 143.490446 110.878981 254.369427 247.847134 254.369426h384.815286c58.700637 0 104.356688 52.178344 104.356688 110.878981s-45.656051 110.878981-104.356688 110.878981H32.611465v189.146497h710.929936c136.968153 0 247.847134-117.401274 247.847134-254.369427v-97.834395c-13.044586-136.968153-117.401274-247.847134-254.369427-247.847133zM3065.477707 0h-482.649682c-136.968153 0-247.847134 117.401274-247.847133 254.369427v769.630573h247.847133V319.592357c0-71.745223 52.178344-130.44586 123.923567-130.44586h247.847134c65.22293 0 123.923567 58.700637 123.923567 130.44586v704.407643H3326.369427V254.369427C3313.324841 117.401274 3202.44586 0 3065.477707 0zM4904.764331 0H5152.611465v1024h-247.847134zM3985.121019 384.815287h247.847134v639.184713h-247.847134z" fill="#FF6000" p-id="6268"></path><path d="M4474.292994 0H3743.796178c-136.968153 0-247.847134 117.401274-247.847133 254.369427v769.630573h247.847133V319.592357c0-71.745223 52.178344-130.44586 123.923567-130.44586h489.171975c65.22293 0 123.923567 58.700637 123.923567 130.44586v704.407643h247.847133V254.369427C4722.140127 117.401274 4611.261146 0 4474.292994 0z" fill="#FF6000" p-id="6269"></path></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64"><circle cx="32" cy="32" r="30" fill="#1677ff"/><text x="32" y="32" font-size="28" font-weight="bold" fill="#fff" text-anchor="middle" dominant-baseline="central" font-family="sans-serif"></text></svg>

Before

Width:  |  Height:  |  Size: 286 B

View File

@@ -1,65 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { vendorLogoMap } from '#/enums/payment/deviceEnum';
defineOptions({ name: 'VendorLogo' });
const props = withDefaults(
defineProps<{
vendor: string;
size?: number;
}>(),
{
size: 32,
},
);
/** 厂商 SVG logo 的 URL */
const logoSrc = computed(() => {
const fileName = vendorLogoMap[props.vendor];
if (!fileName) {
return undefined;
}
try {
return new URL(`/src/assets/device-vendor/${fileName}.svg`, import.meta.url).href;
} catch {
return undefined;
}
});
</script>
<template>
<span class="vendor-logo" :style="{ width: `${size}px`, height: `${size}px` }">
<img v-if="logoSrc" :src="logoSrc" :width="size" :height="size" :alt="vendor" class="logo-img" />
<span
v-else
class="fallback-icon"
:style="{ width: `${size}px`, height: `${size}px`, fontSize: `${Math.max(size! * 0.5, 14)}px` }"
>
🔈
</span>
</span>
</template>
<style scoped>
.vendor-logo {
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
}
.logo-img {
display: block;
object-fit: contain;
}
.fallback-icon {
display: inline-flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
border-radius: 4px;
}
</style>

View File

@@ -211,28 +211,13 @@ export const PermCodes = {
},
},
/** 设备管理(独立命名空间) */
/** 设备管理(独立命名空间;开源仅码牌,硬件对接归商业版 */
Device: {
/** 云音箱 menuCode=device:speaker */
Speaker: {
VIEW: 'device:speaker:view',
MANAGE: 'device:speaker:manage',
},
/** 码牌 menuCode=device:qrcode */
QrCode: {
VIEW: 'device:qrcode:view',
MANAGE: 'device:qrcode:manage',
},
/** 云打印 menuCode=device:printer */
Printer: {
VIEW: 'device:printer:view',
MANAGE: 'device:printer:manage',
},
/** 厂商配置 menuCode=device:vendor_config */
VendorConfig: {
VIEW: 'device:vendor_config:view',
MANAGE: 'device:vendor_config:manage',
},
},
/** 系统域 */

View File

@@ -1,68 +0,0 @@
/**
* 设备类型枚举
*/
export enum DeviceType {
/** 云音箱 */
SPEAKER = 'speaker',
/** 云打印 */
PRINTER = 'printer',
/** 码牌 */
QRCODE = 'qrcode',
}
/**
* 设备厂商枚举
*/
export enum DeviceVendor {
/** 商米 */
SUNMI = 'sunmi',
/** 智谷联 */
ZHIGULIAN = 'zhigulian',
/** 博实结 */
BOSHIJIE = 'boshijie',
}
/**
* 厂商国际化 Key 映射
*/
export const vendorI18nMap: Record<string, string> = {
[DeviceVendor.SUNMI]: 'payment.device.vendor.vendorSunmi',
[DeviceVendor.ZHIGULIAN]: 'payment.device.vendor.vendorZhigulian',
[DeviceVendor.BOSHIJIE]: 'payment.device.vendor.vendorBoshijie',
};
/**
* 厂商 Logo 映射(SVG 文件名, 对应 src/assets/device-vendor/ 下的文件)
*/
export const vendorLogoMap: Record<string, string> = {
[DeviceVendor.SUNMI]: 'sunmi',
[DeviceVendor.ZHIGULIAN]: 'zhigulian',
[DeviceVendor.BOSHIJIE]: 'boshijie',
};
/**
* 设备类型 → 支持的厂商列表
*
* 每种设备类型支持的厂商不同, 在此维护映射关系。
*/
export const deviceVendorMap: Record<string, DeviceVendor[]> = {
[DeviceType.SPEAKER]: [DeviceVendor.SUNMI, DeviceVendor.ZHIGULIAN, DeviceVendor.BOSHIJIE],
[DeviceType.PRINTER]: [DeviceVendor.SUNMI],
};
/**
* 设备类型国际化 Key 映射(用于厂商配置页设备类型列展示)
*/
export const deviceTypeI18nMap: Record<string, string> = {
[DeviceType.SPEAKER]: 'payment.device.vendor.deviceTypeLabel.speaker',
[DeviceType.PRINTER]: 'payment.device.vendor.deviceTypeLabel.printer',
[DeviceType.QRCODE]: 'payment.device.vendor.deviceTypeLabel.qrcode',
};
/**
* 根据厂商代码反查其支持的设备类型列表(基于 deviceVendorMap 反向查询)
*/
export function getDeviceTypesByVendor(vendorCode: string): DeviceType[] {
return (Object.keys(deviceVendorMap) as DeviceType[])
.filter((dt) => deviceVendorMap[dt]?.includes(vendorCode as DeviceVendor));
}

View File

@@ -1,35 +0,0 @@
{
"title": "Cloud Printer Device",
"field": {
"mchNo": "Merchant No.",
"vendorCode": "Vendor",
"vendorConfigId": "Vendor Config",
"deviceSn": "Device Serial No.",
"imei": "Device IMEI",
"shopId": "Vendor Shop ID",
"deviceName": "Device Name",
"status": "Status",
"bindTime": "Bind Time",
"lastOnlineTime": "Last Online Time",
"remark": "Remark",
"createTime": "Create Time"
},
"status": {
"unbound": "Unbound",
"online": "Online",
"offline": "Offline",
"fault": "Fault"
},
"validateMchNo": "Please enter merchant no.",
"validateDeviceSn": "Please enter device serial no.",
"bind": "Bind",
"unbind": "Unbind",
"confirmBind": "Are you sure to bind this device?",
"confirmUnbind": "Are you sure to unbind this device? Printing will stop after unbind.",
"confirmDelete": "Are you sure to delete this device?",
"validateVendorCode": "Please select a vendor",
"validateVendorConfig": "Please select a vendor config",
"pleaseSelectVendor": "Please select a vendor",
"pleaseSelectVendorConfig": "Please select a config",
"unassigned": "Unassigned"
}

View File

@@ -1,35 +0,0 @@
{
"title": "Cloud Speaker Device",
"field": {
"mchNo": "Merchant No.",
"vendorCode": "Vendor",
"vendorConfigId": "Vendor Config",
"deviceSn": "Device Serial No.",
"imei": "Device IMEI",
"shopId": "Vendor Shop ID",
"deviceName": "Device Name",
"status": "Status",
"bindTime": "Bind Time",
"lastOnlineTime": "Last Online Time",
"remark": "Remark",
"createTime": "Create Time"
},
"status": {
"unbound": "Unbound",
"online": "Online",
"offline": "Offline",
"fault": "Fault"
},
"validateMchNo": "Please enter merchant no.",
"validateDeviceSn": "Please enter device serial no.",
"bind": "Bind",
"unbind": "Unbind",
"confirmBind": "Are you sure to bind this device?",
"confirmUnbind": "Are you sure to unbind this device? Broadcast will stop after unbind.",
"confirmDelete": "Are you sure to delete this device?",
"validateVendorCode": "Please select a vendor",
"validateVendorConfig": "Please select a vendor config",
"pleaseSelectVendor": "Please select a vendor",
"pleaseSelectVendorConfig": "Please select a config",
"unassigned": "Unassigned"
}

View File

@@ -1,42 +0,0 @@
{
"title": "Device Vendor Management",
"vendorSunmi": "SUNMI",
"vendorZhigulian": "Zhigulian",
"vendorBoshijie": "Boshijie",
"field": {
"deviceType": "Device Type",
"vendorCode": "Vendor",
"configName": "Config Name",
"appId": "App ID",
"appSecret": "App Secret",
"enable": "Enabled",
"extParam": "Ext Params",
"remark": "Remark",
"createTime": "Created At"
},
"action": {
"addConfig": "Add Config",
"editConfig": "Edit Config"
},
"placeholder": {
"configName": "Please enter config name",
"appId": "Please enter App ID",
"appSecret": "Please enter App Secret",
"extParam": "Please enter ext params (JSON)"
},
"hint": {
"appSecretKeepEmpty": "Leave empty to keep unchanged",
"clickToManage": "Click to manage vendor configs"
},
"enable": "Enabled",
"disable": "Disabled",
"confirmDelete": "Are you sure to delete this vendor config?",
"validateConfigName": "Please enter config name",
"validateAppId": "Please enter App ID",
"validateAppSecret": "Please enter App Secret",
"deviceTypeLabel": {
"speaker": "Cloud Speaker",
"printer": "Cloud Printer",
"qrcode": "QR Code"
}
}

View File

@@ -1,35 +0,0 @@
{
"title": "云打印设备",
"field": {
"mchNo": "商户号",
"vendorCode": "厂商",
"vendorConfigId": "厂商配置",
"deviceSn": "设备序列号",
"imei": "设备IMEI",
"shopId": "厂商门店ID",
"deviceName": "设备名称",
"status": "设备状态",
"bindTime": "绑定时间",
"lastOnlineTime": "最后在线时间",
"remark": "备注",
"createTime": "创建时间"
},
"status": {
"unbound": "未绑定",
"online": "在线",
"offline": "离线",
"fault": "故障"
},
"validateMchNo": "请输入商户号",
"validateDeviceSn": "请输入设备序列号",
"bind": "绑定",
"unbind": "解绑",
"confirmBind": "确定要绑定该设备吗?",
"confirmUnbind": "确定要解绑该设备吗?解绑后将停止打印。",
"confirmDelete": "确定要删除该设备吗?",
"validateVendorCode": "请选择厂商",
"validateVendorConfig": "请选择厂商配置",
"pleaseSelectVendor": "请选择厂商",
"pleaseSelectVendorConfig": "请选择配置",
"unassigned": "未分配"
}

View File

@@ -1,35 +0,0 @@
{
"title": "云音箱设备",
"field": {
"mchNo": "商户号",
"vendorCode": "厂商",
"vendorConfigId": "厂商配置",
"deviceSn": "设备序列号",
"imei": "设备IMEI",
"shopId": "厂商门店ID",
"deviceName": "设备名称",
"status": "设备状态",
"bindTime": "绑定时间",
"lastOnlineTime": "最后在线时间",
"remark": "备注",
"createTime": "创建时间"
},
"status": {
"unbound": "未绑定",
"online": "在线",
"offline": "离线",
"fault": "故障"
},
"validateMchNo": "请输入商户号",
"validateDeviceSn": "请输入设备序列号",
"bind": "绑定",
"unbind": "解绑",
"confirmBind": "确定要绑定该设备吗?",
"confirmUnbind": "确定要解绑该设备吗?解绑后将停止播报。",
"confirmDelete": "确定要删除该设备吗?",
"validateVendorCode": "请选择厂商",
"validateVendorConfig": "请选择厂商配置",
"pleaseSelectVendor": "请选择厂商",
"pleaseSelectVendorConfig": "请选择配置",
"unassigned": "未分配"
}

View File

@@ -1,42 +0,0 @@
{
"title": "设备厂商管理",
"vendorSunmi": "商米",
"vendorZhigulian": "智谷联",
"vendorBoshijie": "博实结",
"field": {
"deviceType": "设备类型",
"vendorCode": "厂商",
"configName": "配置名称",
"appId": "应用ID",
"appSecret": "应用密钥",
"enable": "是否启用",
"extParam": "扩展参数",
"remark": "备注",
"createTime": "创建时间"
},
"action": {
"addConfig": "新增配置",
"editConfig": "编辑配置"
},
"placeholder": {
"configName": "请输入配置名称",
"appId": "请输入应用ID",
"appSecret": "请输入应用密钥",
"extParam": "请输入扩展参数(JSON格式)"
},
"hint": {
"appSecretKeepEmpty": "留空不修改",
"clickToManage": "点击管理厂商配置"
},
"enable": "启用",
"disable": "停用",
"confirmDelete": "确定要删除该厂商配置吗?",
"validateConfigName": "请输入配置名称",
"validateAppId": "请输入应用ID",
"validateAppSecret": "请输入应用密钥",
"deviceTypeLabel": {
"speaker": "云音箱",
"printer": "云打印",
"qrcode": "码牌"
}
}

View File

@@ -1,200 +0,0 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { $t } from '@vben/locales';
import { DevicePrinterApi, type DevicePrinterParam, type DevicePrinterResult } from '#/api/payment/device/printer.api';
import { DeviceVendorConfigApi, type DeviceVendorConfigResult } from '#/api/payment/device/vendor-config.api';
import { DeviceType, deviceVendorMap, vendorI18nMap } from '#/enums/payment/deviceEnum';
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, formEditType } = useFormEdit();
// 厂商选项(从设备类型→厂商映射获取)
const vendorOptions = computed(() => {
const vendorList = deviceVendorMap[DeviceType.PRINTER] || [];
return vendorList.map((v) => ({
label: $t(vendorI18nMap[v] || v),
value: v,
}));
});
// 厂商配置选项(根据厂商动态加载)
const configOptions = ref<DeviceVendorConfigResult[]>([]);
const formState = ref<DevicePrinterParam>({
vendorCode: '',
deviceSn: '',
deviceName: '',
});
const formRules = computed(() => ({
vendorCode: [{ required: true, message: $t('payment.device.printer.validateVendorCode') }],
vendorConfigId: [{ required: true, message: $t('payment.device.printer.validateVendorConfig') }],
deviceSn: [{ required: true, message: $t('payment.device.printer.validateDeviceSn') }],
}));
/**
* 加载厂商配置选项
*/
async function loadConfigOptions(vendorCode?: string) {
if (!vendorCode) {
configOptions.value = [];
return;
}
const { data } = await DeviceVendorConfigApi.listEnabledByVendor(DeviceType.PRINTER, vendorCode);
configOptions.value = data || [];
}
/**
* 厂商切换时清空配置并重新加载
*/
function handleVendorChange() {
formState.value.vendorConfigId = undefined;
loadConfigOptions(formState.value.vendorCode);
}
/**
* 重置表单
*/
function resetForm() {
formState.value = {
vendorCode: '',
deviceSn: '',
deviceName: '',
};
configOptions.value = [];
formRef.value?.resetFields();
}
/**
* 打开新增弹窗
*/
function show() {
initFormEditType(FormEditType.Add);
resetForm();
}
/**
* 打开编辑弹窗
*/
async function showEdit(record: DevicePrinterResult) {
initFormEditType(FormEditType.Edit);
resetForm();
confirmLoading.value = true;
try {
const { data } = await DevicePrinterApi.get(record.id!);
const row = data || record;
formState.value = {
id: row.id!,
mchNo: row.mchNo,
vendorCode: row.vendorCode,
vendorConfigId: row.vendorConfigId,
deviceSn: row.deviceSn,
imei: row.imei,
shopId: row.shopId,
deviceName: row.deviceName,
remark: row.remark,
};
// 加载已有厂商的配置列表
if (row.vendorCode) {
await loadConfigOptions(row.vendorCode);
}
} finally {
confirmLoading.value = false;
}
}
/**
* 保存
*/
async function handleOk() {
try {
await formRef.value?.validate();
} catch {
// 校验失败:表单已显示错误提示
return;
}
confirmLoading.value = true;
try {
const payload: DevicePrinterParam = { ...formState.value };
await (formEditType.value === FormEditType.Edit
? DevicePrinterApi.update(payload)
: DevicePrinterApi.add(payload));
message.success($t('common.operationSuccess'));
handleCancel();
emit('ok');
} finally {
confirmLoading.value = false;
}
}
defineExpose({ show, showEdit });
</script>
<template>
<a-modal
v-model:open="visible"
:title="title"
:width="640"
:confirm-loading="confirmLoading"
:destroy-on-hidden="true"
:mask-closable="false"
@ok="handleOk"
@cancel="handleCancel"
>
<a-spin :spinning="confirmLoading">
<a-form
ref="formRef"
:model="formState"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
class="form-compact"
>
<!-- 厂商 -->
<a-form-item :label="$t('payment.device.printer.field.vendorCode')" name="vendorCode">
<a-select
v-model:value="formState.vendorCode"
:options="vendorOptions"
:placeholder="$t('payment.device.printer.pleaseSelectVendor')"
@change="handleVendorChange"
/>
</a-form-item>
<!-- 厂商配置 -->
<a-form-item :label="$t('payment.device.printer.field.vendorConfigId')" name="vendorConfigId">
<a-select
v-model:value="formState.vendorConfigId"
:options="configOptions"
:field-names="{ label: 'configName', value: 'id' }"
:placeholder="$t('payment.device.printer.pleaseSelectVendorConfig')"
/>
</a-form-item>
<!-- 设备序列号 -->
<a-form-item :label="$t('payment.device.printer.field.deviceSn')" name="deviceSn">
<a-input v-model:value="formState.deviceSn" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
<!-- 设备名称 -->
<a-form-item :label="$t('payment.device.printer.field.deviceName')">
<a-input v-model:value="formState.deviceName" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
<!-- 设备IMEI -->
<a-form-item :label="$t('payment.device.printer.field.imei')">
<a-input v-model:value="formState.imei" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
<!-- 备注 -->
<a-form-item :label="$t('payment.device.printer.field.remark')">
<a-textarea v-model:value="formState.remark" :rows="2" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>

View File

@@ -1,295 +0,0 @@
<script lang="ts" setup>
import type { VxeTableInstance, VxeToolbarInstance } from 'vxe-table';
import { computed, onMounted, ref } from 'vue';
import { $t } from '@vben/locales';
import { DevicePrinterApi, type DevicePrinterResult } from '#/api/payment/device/printer.api';
import { DeviceType, deviceVendorMap, vendorI18nMap } from '#/enums/payment/deviceEnum';
import { BQuery, type QueryField } from '#/components/query';
import { PermCodes } from '#/constants/perm-codes';
import { useMessage } from '#/hooks/useMessage';
import { usePermission } from '#/hooks/usePermission';
import DevicePrinterEdit from './DevicePrinterEdit.vue';
defineOptions({ name: 'DevicePrinterList' });
const { confirm, message } = useMessage();
const { hasPermission } = usePermission();
const loading = ref(false);
const xTable = ref<VxeTableInstance>();
const xToolbar = ref<VxeToolbarInstance>();
const editRef = ref<InstanceType<typeof DevicePrinterEdit>>();
const queryForm = ref<Record<string, any>>({});
// 厂商选项(查询用)
const vendorOptions = computed(() => {
const vendorList = deviceVendorMap[DeviceType.PRINTER] || [];
return vendorList.map((v) => ({
label: $t(vendorI18nMap[v] || v),
value: v,
}));
});
const queryFields = computed<QueryField[]>(() => [
{
type: 'string',
field: 'mchNo',
name: $t('payment.device.printer.field.mchNo'),
placeholder: $t('common.pleaseInput'),
},
{
type: 'list',
field: 'vendorCode',
name: $t('payment.device.printer.field.vendorCode'),
placeholder: $t('common.pleaseSelect'),
selectList: vendorOptions.value,
},
{
type: 'string',
field: 'deviceSn',
name: $t('payment.device.printer.field.deviceSn'),
placeholder: $t('common.pleaseInput'),
},
{
type: 'string',
field: 'deviceName',
name: $t('payment.device.printer.field.deviceName'),
placeholder: $t('common.pleaseInput'),
},
{
type: 'list',
field: 'status',
name: $t('payment.device.printer.field.status'),
placeholder: $t('common.pleaseSelect'),
selectList: [
{ label: $t('payment.device.printer.status.unbound'), value: 'unbound' },
{ label: $t('payment.device.printer.status.online'), value: 'online' },
{ label: $t('payment.device.printer.status.offline'), value: 'offline' },
{ label: $t('payment.device.printer.status.fault'), value: 'fault' },
],
},
]);
const pageConfig = ref({
currentPage: 1,
pageSize: 10,
total: 0,
});
const tableData = ref<DevicePrinterResult[]>([]);
/**
* 分页查询云打印设备列表
*/
function queryPage() {
loading.value = true;
DevicePrinterApi.page({
current: pageConfig.value.currentPage,
size: pageConfig.value.pageSize,
...queryForm.value,
})
.then((res: any) => {
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 }: any) {
pageConfig.value.currentPage = currentPage;
pageConfig.value.pageSize = pageSize;
queryPage();
}
onMounted(() => {
xTable.value?.connectToolbar(xToolbar.value as VxeToolbarInstance);
queryPage();
});
function handleAdd() {
editRef.value?.show();
}
function handleEdit(row: DevicePrinterResult) {
editRef.value?.showEdit(row);
}
/**
* 删除设备
*/
function handleDelete(row: DevicePrinterResult) {
confirm({
content: $t('payment.device.printer.confirmDelete'),
onOk() {
return DevicePrinterApi.delete(row.id!).then(() => {
message.success($t('common.operationSuccess'));
queryPage();
});
},
});
}
/**
* 绑定设备(首期仅更新本地状态)
*/
function handleBind(row: DevicePrinterResult) {
confirm({
content: $t('payment.device.printer.confirmBind'),
onOk() {
return DevicePrinterApi.bind(row.id!).then(() => {
message.success($t('common.operationSuccess'));
queryPage();
});
},
});
}
/**
* 解绑设备
*/
function handleUnbind(row: DevicePrinterResult) {
confirm({
content: $t('payment.device.printer.confirmUnbind'),
onOk() {
return DevicePrinterApi.unbind(row.id!).then(() => {
message.success($t('common.operationSuccess'));
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-space>
<a-button
v-if="hasPermission(PermCodes.Device.Printer.MANAGE)"
type="primary"
@click="handleAdd"
>{{ $t('common.add') }}</a-button
>
</a-space>
</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="deviceSn" :title="$t('payment.device.printer.field.deviceSn')" :min-width="180" />
<vxe-column field="deviceName" :title="$t('payment.device.printer.field.deviceName')" :min-width="140" />
<vxe-column field="mchNo" :title="$t('payment.device.printer.field.mchNo')" :min-width="140">
<template #default="{ row }">
<a-tag v-if="row.mchNo" color="blue">{{ row.mchNo }}</a-tag>
<span v-else style="color: var(--text-color-placeholder)">{{ $t('payment.device.printer.unassigned') }}</span>
</template>
</vxe-column>
<vxe-column field="vendorCode" :title="$t('payment.device.printer.field.vendorCode')" :min-width="100">
<template #default="{ row }">
{{ row.vendorCode ? $t(vendorI18nMap[row.vendorCode] || row.vendorCode) : '-' }}
</template>
</vxe-column>
<vxe-column
field="status"
:title="$t('payment.device.printer.field.status')"
:min-width="100"
align="center"
>
<template #default="{ row }">
<a-tag v-if="row.status === 'online'" color="green">{{
$t('payment.device.printer.status.online')
}}</a-tag>
<a-tag v-else-if="row.status === 'offline'" color="default">{{
$t('payment.device.printer.status.offline')
}}</a-tag>
<a-tag v-else-if="row.status === 'fault'" color="red">{{
$t('payment.device.printer.status.fault')
}}</a-tag>
<a-tag v-else color="blue">{{ $t('payment.device.printer.status.unbound') }}</a-tag>
</template>
</vxe-column>
<vxe-column
field="bindTime"
:title="$t('payment.device.printer.field.bindTime')"
:min-width="180"
formatter="formatDateTime"
/>
<vxe-column
field="createTime"
:title="$t('payment.device.printer.field.createTime')"
:min-width="180"
formatter="formatDateTime"
/>
<vxe-column fixed="right" width="200" :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.Device.Printer.MANAGE)"
type="link"
size="small"
@click="handleEdit(row)"
>{{ $t('common.edit') }}</a-button
>
<a-button
v-if="hasPermission(PermCodes.Device.Printer.MANAGE) && row.status !== 'online'"
type="link"
size="small"
@click="handleBind(row)"
>{{ $t('payment.device.printer.bind') }}</a-button
>
<a-button
v-else-if="hasPermission(PermCodes.Device.Printer.MANAGE) && row.status === 'online'"
type="link"
size="small"
@click="handleUnbind(row)"
>{{ $t('payment.device.printer.unbind') }}</a-button
>
<a-button
v-if="hasPermission(PermCodes.Device.Printer.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>
<DevicePrinterEdit ref="editRef" @ok="queryPage" />
</div>
</template>

View File

@@ -1,200 +0,0 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { $t } from '@vben/locales';
import { DeviceSpeakerApi, type DeviceSpeakerParam, type DeviceSpeakerResult } from '#/api/payment/device/speaker.api';
import { DeviceVendorConfigApi, type DeviceVendorConfigResult } from '#/api/payment/device/vendor-config.api';
import { DeviceType, deviceVendorMap, vendorI18nMap } from '#/enums/payment/deviceEnum';
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, formEditType } = useFormEdit();
// 厂商选项(从设备类型→厂商映射获取)
const vendorOptions = computed(() => {
const vendorList = deviceVendorMap[DeviceType.SPEAKER] || [];
return vendorList.map((v) => ({
label: $t(vendorI18nMap[v] || v),
value: v,
}));
});
// 厂商配置选项(根据厂商动态加载)
const configOptions = ref<DeviceVendorConfigResult[]>([]);
const formState = ref<DeviceSpeakerParam>({
vendorCode: '',
deviceSn: '',
deviceName: '',
});
const formRules = computed(() => ({
vendorCode: [{ required: true, message: $t('payment.device.speaker.validateVendorCode') }],
vendorConfigId: [{ required: true, message: $t('payment.device.speaker.validateVendorConfig') }],
deviceSn: [{ required: true, message: $t('payment.device.speaker.validateDeviceSn') }],
}));
/**
* 加载厂商配置选项
*/
async function loadConfigOptions(vendorCode?: string) {
if (!vendorCode) {
configOptions.value = [];
return;
}
const { data } = await DeviceVendorConfigApi.listEnabledByVendor(DeviceType.SPEAKER, vendorCode);
configOptions.value = data || [];
}
/**
* 厂商切换时清空配置并重新加载
*/
function handleVendorChange() {
formState.value.vendorConfigId = undefined;
loadConfigOptions(formState.value.vendorCode);
}
/**
* 重置表单
*/
function resetForm() {
formState.value = {
vendorCode: '',
deviceSn: '',
deviceName: '',
};
configOptions.value = [];
formRef.value?.resetFields();
}
/**
* 打开新增弹窗
*/
function show() {
initFormEditType(FormEditType.Add);
resetForm();
}
/**
* 打开编辑弹窗
*/
async function showEdit(record: DeviceSpeakerResult) {
initFormEditType(FormEditType.Edit);
resetForm();
confirmLoading.value = true;
try {
const { data } = await DeviceSpeakerApi.get(record.id!);
const row = data || record;
formState.value = {
id: row.id!,
mchNo: row.mchNo,
vendorCode: row.vendorCode,
vendorConfigId: row.vendorConfigId,
deviceSn: row.deviceSn,
imei: row.imei,
shopId: row.shopId,
deviceName: row.deviceName,
remark: row.remark,
};
// 加载已有厂商的配置列表
if (row.vendorCode) {
await loadConfigOptions(row.vendorCode);
}
} finally {
confirmLoading.value = false;
}
}
/**
* 保存
*/
async function handleOk() {
try {
await formRef.value?.validate();
} catch {
// 校验失败:表单已显示错误提示
return;
}
confirmLoading.value = true;
try {
const payload: DeviceSpeakerParam = { ...formState.value };
await (formEditType.value === FormEditType.Edit
? DeviceSpeakerApi.update(payload)
: DeviceSpeakerApi.add(payload));
message.success($t('common.operationSuccess'));
handleCancel();
emit('ok');
} finally {
confirmLoading.value = false;
}
}
defineExpose({ show, showEdit });
</script>
<template>
<a-modal
v-model:open="visible"
:title="title"
:width="640"
:confirm-loading="confirmLoading"
:destroy-on-hidden="true"
:mask-closable="false"
@ok="handleOk"
@cancel="handleCancel"
>
<a-spin :spinning="confirmLoading">
<a-form
ref="formRef"
:model="formState"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
class="form-compact"
>
<!-- 厂商 -->
<a-form-item :label="$t('payment.device.speaker.field.vendorCode')" name="vendorCode">
<a-select
v-model:value="formState.vendorCode"
:options="vendorOptions"
:placeholder="$t('payment.device.speaker.pleaseSelectVendor')"
@change="handleVendorChange"
/>
</a-form-item>
<!-- 厂商配置 -->
<a-form-item :label="$t('payment.device.speaker.field.vendorConfigId')" name="vendorConfigId">
<a-select
v-model:value="formState.vendorConfigId"
:options="configOptions"
:field-names="{ label: 'configName', value: 'id' }"
:placeholder="$t('payment.device.speaker.pleaseSelectVendorConfig')"
/>
</a-form-item>
<!-- 设备序列号 -->
<a-form-item :label="$t('payment.device.speaker.field.deviceSn')" name="deviceSn">
<a-input v-model:value="formState.deviceSn" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
<!-- 设备名称 -->
<a-form-item :label="$t('payment.device.speaker.field.deviceName')">
<a-input v-model:value="formState.deviceName" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
<!-- 设备IMEI -->
<a-form-item :label="$t('payment.device.speaker.field.imei')">
<a-input v-model:value="formState.imei" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
<!-- 备注 -->
<a-form-item :label="$t('payment.device.speaker.field.remark')">
<a-textarea v-model:value="formState.remark" :rows="2" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>

View File

@@ -1,295 +0,0 @@
<script lang="ts" setup>
import type { VxeTableInstance, VxeToolbarInstance } from 'vxe-table';
import { computed, onMounted, ref } from 'vue';
import { $t } from '@vben/locales';
import { DeviceSpeakerApi, type DeviceSpeakerResult } from '#/api/payment/device/speaker.api';
import { DeviceType, deviceVendorMap, vendorI18nMap } from '#/enums/payment/deviceEnum';
import { BQuery, type QueryField } from '#/components/query';
import { PermCodes } from '#/constants/perm-codes';
import { useMessage } from '#/hooks/useMessage';
import { usePermission } from '#/hooks/usePermission';
import DeviceSpeakerEdit from './DeviceSpeakerEdit.vue';
defineOptions({ name: 'DeviceSpeakerList' });
const { confirm, message } = useMessage();
const { hasPermission } = usePermission();
const loading = ref(false);
const xTable = ref<VxeTableInstance>();
const xToolbar = ref<VxeToolbarInstance>();
const editRef = ref<InstanceType<typeof DeviceSpeakerEdit>>();
const queryForm = ref<Record<string, any>>({});
// 厂商选项(查询用)
const vendorOptions = computed(() => {
const vendorList = deviceVendorMap[DeviceType.SPEAKER] || [];
return vendorList.map((v) => ({
label: $t(vendorI18nMap[v] || v),
value: v,
}));
});
const queryFields = computed<QueryField[]>(() => [
{
type: 'string',
field: 'mchNo',
name: $t('payment.device.speaker.field.mchNo'),
placeholder: $t('common.pleaseInput'),
},
{
type: 'list',
field: 'vendorCode',
name: $t('payment.device.speaker.field.vendorCode'),
placeholder: $t('common.pleaseSelect'),
selectList: vendorOptions.value,
},
{
type: 'string',
field: 'deviceSn',
name: $t('payment.device.speaker.field.deviceSn'),
placeholder: $t('common.pleaseInput'),
},
{
type: 'string',
field: 'deviceName',
name: $t('payment.device.speaker.field.deviceName'),
placeholder: $t('common.pleaseInput'),
},
{
type: 'list',
field: 'status',
name: $t('payment.device.speaker.field.status'),
placeholder: $t('common.pleaseSelect'),
selectList: [
{ label: $t('payment.device.speaker.status.unbound'), value: 'unbound' },
{ label: $t('payment.device.speaker.status.online'), value: 'online' },
{ label: $t('payment.device.speaker.status.offline'), value: 'offline' },
{ label: $t('payment.device.speaker.status.fault'), value: 'fault' },
],
},
]);
const pageConfig = ref({
currentPage: 1,
pageSize: 10,
total: 0,
});
const tableData = ref<DeviceSpeakerResult[]>([]);
/**
* 分页查询云音箱设备列表
*/
function queryPage() {
loading.value = true;
DeviceSpeakerApi.page({
current: pageConfig.value.currentPage,
size: pageConfig.value.pageSize,
...queryForm.value,
})
.then((res: any) => {
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 }: any) {
pageConfig.value.currentPage = currentPage;
pageConfig.value.pageSize = pageSize;
queryPage();
}
onMounted(() => {
xTable.value?.connectToolbar(xToolbar.value as VxeToolbarInstance);
queryPage();
});
function handleAdd() {
editRef.value?.show();
}
function handleEdit(row: DeviceSpeakerResult) {
editRef.value?.showEdit(row);
}
/**
* 删除设备
*/
function handleDelete(row: DeviceSpeakerResult) {
confirm({
content: $t('payment.device.speaker.confirmDelete'),
onOk() {
return DeviceSpeakerApi.delete(row.id!).then(() => {
message.success($t('common.operationSuccess'));
queryPage();
});
},
});
}
/**
* 绑定设备(首期仅更新本地状态)
*/
function handleBind(row: DeviceSpeakerResult) {
confirm({
content: $t('payment.device.speaker.confirmBind'),
onOk() {
return DeviceSpeakerApi.bind(row.id!).then(() => {
message.success($t('common.operationSuccess'));
queryPage();
});
},
});
}
/**
* 解绑设备
*/
function handleUnbind(row: DeviceSpeakerResult) {
confirm({
content: $t('payment.device.speaker.confirmUnbind'),
onOk() {
return DeviceSpeakerApi.unbind(row.id!).then(() => {
message.success($t('common.operationSuccess'));
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-space>
<a-button
v-if="hasPermission(PermCodes.Device.Speaker.MANAGE)"
type="primary"
@click="handleAdd"
>{{ $t('common.add') }}</a-button
>
</a-space>
</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="deviceSn" :title="$t('payment.device.speaker.field.deviceSn')" :min-width="180" />
<vxe-column field="deviceName" :title="$t('payment.device.speaker.field.deviceName')" :min-width="140" />
<vxe-column field="mchNo" :title="$t('payment.device.speaker.field.mchNo')" :min-width="140">
<template #default="{ row }">
<a-tag v-if="row.mchNo" color="blue">{{ row.mchNo }}</a-tag>
<span v-else style="color: var(--text-color-placeholder)">{{ $t('payment.device.speaker.unassigned') }}</span>
</template>
</vxe-column>
<vxe-column field="vendorCode" :title="$t('payment.device.speaker.field.vendorCode')" :min-width="100">
<template #default="{ row }">
{{ row.vendorCode ? $t(vendorI18nMap[row.vendorCode] || row.vendorCode) : '-' }}
</template>
</vxe-column>
<vxe-column
field="status"
:title="$t('payment.device.speaker.field.status')"
:min-width="100"
align="center"
>
<template #default="{ row }">
<a-tag v-if="row.status === 'online'" color="green">{{
$t('payment.device.speaker.status.online')
}}</a-tag>
<a-tag v-else-if="row.status === 'offline'" color="default">{{
$t('payment.device.speaker.status.offline')
}}</a-tag>
<a-tag v-else-if="row.status === 'fault'" color="red">{{
$t('payment.device.speaker.status.fault')
}}</a-tag>
<a-tag v-else color="blue">{{ $t('payment.device.speaker.status.unbound') }}</a-tag>
</template>
</vxe-column>
<vxe-column
field="bindTime"
:title="$t('payment.device.speaker.field.bindTime')"
:min-width="180"
formatter="formatDateTime"
/>
<vxe-column
field="createTime"
:title="$t('payment.device.speaker.field.createTime')"
:min-width="180"
formatter="formatDateTime"
/>
<vxe-column fixed="right" width="200" :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.Device.Speaker.MANAGE)"
type="link"
size="small"
@click="handleEdit(row)"
>{{ $t('common.edit') }}</a-button
>
<a-button
v-if="hasPermission(PermCodes.Device.Speaker.MANAGE) && row.status !== 'online'"
type="link"
size="small"
@click="handleBind(row)"
>{{ $t('payment.device.speaker.bind') }}</a-button
>
<a-button
v-else-if="hasPermission(PermCodes.Device.Speaker.MANAGE) && row.status === 'online'"
type="link"
size="small"
@click="handleUnbind(row)"
>{{ $t('payment.device.speaker.unbind') }}</a-button
>
<a-button
v-if="hasPermission(PermCodes.Device.Speaker.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>
<DeviceSpeakerEdit ref="editRef" @ok="queryPage" />
</div>
</template>

View File

@@ -1,205 +0,0 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { $t } from '@vben/locales';
import { DeviceVendorConfigApi, type DeviceVendorConfigParam, type DeviceVendorConfigResult } from '#/api/payment/device/vendor-config.api';
import { deviceTypeI18nMap, getDeviceTypesByVendor } from '#/enums/payment/deviceEnum';
import { FormEditType } from '#/enums/formEditType';
import { useFormEdit } from '#/hooks/useFormEdit';
import { useMessage } from '#/hooks/useMessage';
defineOptions({ name: 'VendorConfigEdit' });
const emit = defineEmits(['ok']);
const { message } = useMessage();
const formRef = ref();
const { visible, confirmLoading, title, initFormEditType, handleCancel, formEditType, diffForm } = useFormEdit();
// 厂商代码(由父组件传入, 同一抽屉内固定)
const vendorCode = ref('');
// 设备类型选项(收敛到该厂商支持的设备类型, 由 deviceVendorMap 反查)
const deviceTypeOptions = computed(() =>
getDeviceTypesByVendor(vendorCode.value).map((dt) => ({
label: $t(deviceTypeI18nMap[dt] || dt),
value: dt,
})),
);
const formState = ref<DeviceVendorConfigParam>({
deviceType: '',
configName: '',
appId: '',
enable: true,
});
// 原始脱敏数据(来自后端), 用于 diffForm 比对敏感字段是否被修改
const originalForm = ref<DeviceVendorConfigResult>({});
const formRules = computed(() => ({
deviceType: [{ required: true, message: $t('common.pleaseSelect') }],
configName: [{ required: true, message: $t('payment.device.vendor.validateConfigName') }],
appId: [{ required: true, message: $t('payment.device.vendor.validateAppId') }],
appSecret: [{ required: true, message: $t('payment.device.vendor.validateAppSecret') }],
enable: [{ required: true, type: 'boolean', message: $t('common.pleaseSelect') }],
}));
/**
* 重置表单
*/
function resetForm() {
formState.value = {
deviceType: '',
configName: '',
appId: '',
appSecret: '',
enable: true,
extParam: '',
remark: '',
};
formRef.value?.resetFields();
}
/**
* 打开新增弹窗
*/
function show(vc: string) {
vendorCode.value = vc;
initFormEditType(FormEditType.Add);
title.value = $t('payment.device.vendor.action.addConfig');
resetForm();
// 首次新增无原值, originalForm 置空以便 diffForm 正确识别用户输入的新值
originalForm.value = {};
}
/**
* 打开编辑弹窗
*/
async function showEdit(vc: string, record: DeviceVendorConfigResult) {
vendorCode.value = vc;
initFormEditType(FormEditType.Edit);
title.value = $t('payment.device.vendor.action.editConfig');
resetForm();
confirmLoading.value = true;
try {
const { data } = await DeviceVendorConfigApi.get(record.id!);
const row = data || record;
// 保存原始脱敏数据用于 diffForm 比对
originalForm.value = { ...row };
formState.value = {
id: row.id ?? undefined,
deviceType: row.deviceType,
configName: row.configName,
appId: row.appId,
// 预填脱敏值, 未修改时 diffForm 返回 undefined(后端跳过更新)
appSecret: row.appSecret,
enable: row.enable,
extParam: row.extParam,
remark: row.remark,
};
} finally {
confirmLoading.value = false;
}
}
/**
* 保存
*/
async function handleOk() {
try {
await formRef.value?.validate();
} catch {
// 校验失败:表单已显示错误提示
return;
}
confirmLoading.value = true;
try {
// 敏感字段(appSecret)用 diffForm 比对: 未修改返回 undefined(后端跳过), 修改则返回新值
const sensitiveData = diffForm(originalForm.value, formState.value, 'appSecret');
const payload = {
...formState.value,
...sensitiveData,
vendorCode: vendorCode.value,
} as DeviceVendorConfigParam;
await (formEditType.value === FormEditType.Edit
? DeviceVendorConfigApi.update(payload)
: DeviceVendorConfigApi.add(payload));
message.success($t('common.operationSuccess'));
handleCancel();
emit('ok');
} finally {
confirmLoading.value = false;
}
}
defineExpose({ show, showEdit });
</script>
<template>
<a-modal
v-model:open="visible"
:title="title"
:width="600"
:confirm-loading="confirmLoading"
:destroy-on-hidden="true"
:mask-closable="false"
@ok="handleOk"
@cancel="handleCancel"
>
<a-spin :spinning="confirmLoading">
<a-form
ref="formRef"
:model="formState"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
class="form-compact"
>
<!-- 设备类型 -->
<a-form-item :label="$t('payment.device.vendor.field.deviceType')" name="deviceType">
<a-select
v-model:value="formState.deviceType"
:options="deviceTypeOptions"
:placeholder="$t('common.pleaseSelect')"
/>
</a-form-item>
<!-- 配置名称 -->
<a-form-item :label="$t('payment.device.vendor.field.configName')" name="configName">
<a-input
v-model:value="formState.configName"
:placeholder="$t('payment.device.vendor.placeholder.configName')"
/>
</a-form-item>
<!-- 应用ID -->
<a-form-item :label="$t('payment.device.vendor.field.appId')" name="appId">
<a-input
v-model:value="formState.appId"
:placeholder="$t('payment.device.vendor.placeholder.appId')"
/>
</a-form-item>
<!-- 应用密钥 -->
<a-form-item :label="$t('payment.device.vendor.field.appSecret')" name="appSecret">
<a-input
v-model:value="formState.appSecret"
:placeholder="$t('payment.device.vendor.placeholder.appSecret')"
/>
</a-form-item>
<!-- 是否启用 -->
<a-form-item :label="$t('payment.device.vendor.field.enable')" name="enable">
<a-switch v-model:checked="formState.enable" />
</a-form-item>
<!-- 备注 -->
<a-form-item :label="$t('payment.device.vendor.field.remark')">
<a-textarea
v-model:value="formState.remark"
:rows="2"
:placeholder="$t('common.pleaseInput')"
/>
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>

View File

@@ -1,300 +0,0 @@
<script lang="ts" setup>
import { computed, nextTick, ref } from 'vue';
import type { VxeTableInstance, VxeToolbarInstance } from 'vxe-table';
import { $t } from '@vben/locales';
import { IconifyIcon } from '@vben-core/icons';
import VendorLogo from '#/components/device-vendor/VendorLogo.vue';
import { DeviceVendorConfigApi, type DeviceVendorConfigResult } from '#/api/payment/device/vendor-config.api';
import { PermCodes } from '#/constants/perm-codes';
import { deviceTypeI18nMap, deviceVendorMap, vendorI18nMap } from '#/enums/payment/deviceEnum';
import { useMessage } from '#/hooks/useMessage';
import { usePermission } from '#/hooks/usePermission';
import VendorConfigEdit from './VendorConfigEdit.vue';
defineOptions({ name: 'VendorManage' });
const { confirm, message } = useMessage();
const { hasPermission } = usePermission();
// 厂商卡片数据(平台级通用: 所有至少被一种设备类型支持的厂商并集)
const vendors = computed(() => {
const set = new Set<string>();
Object.values(deviceVendorMap).forEach((list) => list.forEach((v) => set.add(v)));
return [...set].map((v) => ({
key: v,
// 厂商名称
title: $t(vendorI18nMap[v] || v),
vendorCode: v,
}));
});
// 抽屉
const drawerVisible = ref(false);
const loading = ref(false);
const currentVendor = ref('');
// 配置列表
const tableData = ref<DeviceVendorConfigResult[]>([]);
const editRef = ref<InstanceType<typeof VendorConfigEdit>>();
const xTable = ref<VxeTableInstance>();
const xToolbar = ref<VxeToolbarInstance>();
// 分页
const pageConfig = ref({
currentPage: 1,
pageSize: 10,
total: 0,
});
/**
* 卡片点击 - 打开抽屉加载该厂商在各设备类型的配置列表
*/
function handleCardClick(vendorCode: string) {
currentVendor.value = vendorCode;
drawerVisible.value = true;
pageConfig.value.currentPage = 1;
queryPage();
}
/**
* 分页查询厂商配置列表(跨设备类型, 按 vendorCode 聚合)
*/
function queryPage() {
loading.value = true;
DeviceVendorConfigApi.page({
current: pageConfig.value.currentPage,
size: pageConfig.value.pageSize,
vendorCode: currentVendor.value,
})
.then((res: any) => {
tableData.value = res.data?.records || [];
pageConfig.value.total = Number(res.data?.total) || 0;
loading.value = false;
nextTick(() => {
xTable.value?.connectToolbar(xToolbar.value as VxeToolbarInstance);
});
})
.catch(() => {
loading.value = false;
});
}
/**
* 分页变化
*/
function handlePageChange({ currentPage, pageSize }: any) {
pageConfig.value.currentPage = currentPage;
pageConfig.value.pageSize = pageSize;
queryPage();
}
function handleAdd() {
editRef.value?.show(currentVendor.value);
}
function handleEdit(row: DeviceVendorConfigResult) {
editRef.value?.showEdit(currentVendor.value, row);
}
/**
* 删除配置
*/
function handleDelete(row: DeviceVendorConfigResult) {
confirm({
content: $t('payment.device.vendor.confirmDelete'),
onOk() {
return DeviceVendorConfigApi.delete(row.id!).then(() => {
message.success($t('common.operationSuccess'));
queryPage();
});
},
});
}
/**
* 关闭抽屉
*/
function handleDrawerClose() {
drawerVisible.value = false;
currentVendor.value = '';
tableData.value = [];
}
</script>
<template>
<div 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.device.vendor.title') }}</span>
</div>
</template>
<!-- 厂商卡片网格 -->
<div class="card-grid">
<a-card
v-for="card in vendors"
:key="card.key"
class="vendor-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: 0, display: 'flex', flexDirection: 'column', height: '100%' } }"
>
<!-- 主体内容 -->
<div class="flex-1 flex flex-col items-center justify-center pt-6 pb-3">
<div class="mb-3 transform transition-transform duration-300 group-hover:scale-110">
<VendorLogo :vendor="card.vendorCode" :size="52" />
</div>
<div class="text-center font-bold text-foreground text-[15px] px-4">
{{ card.title }}
</div>
</div>
<!-- 底部配置条 -->
<div class="flex border-t border-border h-10 bg-muted/50">
<div class="config-slot" @click="handleCardClick(card.vendorCode)">
<div class="flex items-center gap-1.5">
<IconifyIcon icon="ant-design:setting-filled" class="text-blue-500/80 text-sm" />
<span class="text-[10px] font-bold text-muted-foreground uppercase">
{{ $t('payment.device.vendor.title') }}
</span>
</div>
</div>
</div>
</a-card>
</div>
</a-card>
<!-- 配置列表抽屉 -->
<a-drawer
v-model:open="drawerVisible"
:size="1080"
@close="handleDrawerClose"
>
<template #title>
<div class="flex items-center gap-2">
<VendorLogo :vendor="currentVendor" :size="24" />
<span>{{ $t('payment.device.vendor.title') }}</span>
<span class="text-muted-foreground">·</span>
<span class="text-muted-foreground">{{ $t(vendorI18nMap[currentVendor] || currentVendor) }}</span>
</div>
</template>
<a-spin :spinning="loading">
<vxe-toolbar ref="xToolbar" custom refresh :refresh-options="{ queryMethod: queryPage }">
<template #buttons>
<a-button
v-if="hasPermission(PermCodes.Device.VendorConfig.MANAGE)"
type="primary"
@click="handleAdd"
>
{{ $t('payment.device.vendor.action.addConfig') }}
</a-button>
</template>
</vxe-toolbar>
<vxe-table ref="xTable" :row-config="{ keyField: 'id' }" :data="tableData" size="small">
<vxe-column type="seq" :title="$t('common.seq')" width="60" align="center" />
<vxe-column
field="deviceType"
:title="$t('payment.device.vendor.field.deviceType')"
:min-width="100"
>
<template #default="{ row }">
{{ row.deviceType ? $t(deviceTypeI18nMap[row.deviceType] || row.deviceType) : '-' }}
</template>
</vxe-column>
<vxe-column
field="configName"
:title="$t('payment.device.vendor.field.configName')"
:min-width="140"
/>
<vxe-column
field="appId"
:title="$t('payment.device.vendor.field.appId')"
:min-width="160"
/>
<vxe-column
:title="$t('payment.device.vendor.field.enable')"
width="80"
align="center"
>
<template #default="{ row }">
<a-tag v-if="row.enable" color="green">{{ $t('payment.device.vendor.enable') }}</a-tag>
<a-tag v-else>{{ $t('payment.device.vendor.disable') }}</a-tag>
</template>
</vxe-column>
<vxe-column
field="createTime"
:title="$t('payment.device.vendor.field.createTime')"
:min-width="160"
formatter="formatDateTime"
/>
<vxe-column :title="$t('common.operation')" width="120" fixed="right" :show-overflow="false">
<template #default="{ row }">
<a-space :size="2">
<template #separator>
<a-divider type="vertical" />
</template>
<a-button
v-if="hasPermission(PermCodes.Device.VendorConfig.MANAGE)"
type="link"
size="small"
@click="handleEdit(row)"
>{{ $t('common.edit') }}</a-button
>
<a-button
v-if="hasPermission(PermCodes.Device.VendorConfig.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-spin>
<template #footer>
<a-button @click="handleDrawerClose">{{ $t('common.close') }}</a-button>
</template>
</a-drawer>
<VendorConfigEdit ref="editRef" @ok="queryPage" />
</div>
</template>
<style scoped>
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
padding: 4px;
}
.vendor-card {
min-height: 180px;
}
.config-slot {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.config-slot:hover {
background-color: hsl(var(--background));
box-shadow: inset 0 -2px 0 0 hsl(var(--primary));
}
</style>