feat(admin): 终端管理页面与菜单迁移至商户工作台

- 系统终端页面移至 merchant/terminal/(照搬门店模式: 强制 mchNo 路由参数+返回按钮+商户名)
- 通道终端面板(device/terminal/channel)嵌入通道商户详情抽屉, 复用 channel:merchant 权限
- 商户工作台基础管理组新增系统终端卡片入口
- 权限码 device:terminal:system → merchant:terminal(perm-codes.ts)
- menu-titles/workbench 10语种同步(cardTerminal + menu.payment.merchant.terminal)
This commit is contained in:
DaxPay Dev
2026-07-19 00:07:41 +08:00
parent b18d4584ef
commit 638cd9fe1b
39 changed files with 1857 additions and 20 deletions

View File

@@ -0,0 +1,174 @@
import type { BaseEntity, PageResult, Result } from '#/types/web';
import { defHttp } from '#/api/request';
/**
* 系统终端 API
*/
export const TerminalDeviceApi = {
/** 分页 */
page(
params: TerminalDeviceQuery & { current?: number; size?: number },
): Promise<Result<PageResult<TerminalDeviceResult>>> {
return defHttp.get({ url: '/admin/device/terminal/system/page', params });
},
/** 详情 */
get(id: string): Promise<Result<TerminalDeviceResult>> {
return defHttp.get({ url: '/admin/device/terminal/system/get', params: { id } });
},
/** 按商户列表 */
listByMchNo(mchNo: string): Promise<Result<TerminalDeviceResult[]>> {
return defHttp.get({ url: '/admin/device/terminal/system/list-by-mch-no', params: { mchNo } });
},
/** 新增 */
add(data: TerminalDeviceParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/terminal/system/add', data });
},
/** 修改 */
update(data: TerminalDeviceParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/terminal/system/update', data });
},
/** 删除 */
delete(id: string): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/terminal/system/delete', params: { id } });
},
/** 绑定通道终端 */
bind(data: TerminalChannelBindParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/terminal/system/bind', data });
},
/** 解绑通道终端 */
unbind(data: TerminalChannelBindParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/terminal/system/unbind', data });
},
/** 已绑定通道终端 */
boundChannelList(terminalNo: string): Promise<Result<ChannelTerminalResult[]>> {
return defHttp.get({
url: '/admin/device/terminal/system/bound-channel-list',
params: { terminalNo },
});
},
};
/**
* 通道终端 API
*/
export const ChannelTerminalApi = {
/** 分页 */
page(
params: ChannelTerminalQuery & { current?: number; size?: number },
): Promise<Result<PageResult<ChannelTerminalResult>>> {
return defHttp.get({ url: '/admin/device/terminal/channel/page', params });
},
/** 详情 */
get(id: string): Promise<Result<ChannelTerminalResult>> {
return defHttp.get({ url: '/admin/device/terminal/channel/get', params: { id } });
},
/** 按商户列表 */
listByMchNo(mchNo: string): Promise<Result<ChannelTerminalResult[]>> {
return defHttp.get({ url: '/admin/device/terminal/channel/list-by-mch-no', params: { mchNo } });
},
/** 新增 */
add(data: ChannelTerminalParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/terminal/channel/add', data });
},
/** 修改 */
update(data: ChannelTerminalParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/terminal/channel/update', data });
},
/** 删除 */
delete(id: string): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/terminal/channel/delete', params: { id } });
},
/** 绑定系统终端 */
bind(data: TerminalChannelBindParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/terminal/channel/bind', data });
},
/** 解绑系统终端 */
unbind(data: TerminalChannelBindParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/device/terminal/channel/unbind', data });
},
/** 已绑定系统终端 */
boundSystemList(channelTerminalId: string): Promise<Result<TerminalDeviceResult[]>> {
return defHttp.get({
url: '/admin/device/terminal/channel/bound-system-list',
params: { channelTerminalId },
});
},
};
/** 系统终端查询 */
export interface TerminalDeviceQuery {
mchNo?: string;
terminalNo?: string;
name?: string;
storeNo?: string;
enable?: boolean;
}
/** 系统终端参数 */
export interface TerminalDeviceParam {
id?: string;
mchNo?: string;
name?: string;
storeNo?: string;
enable?: boolean;
remark?: string;
}
/** 系统终端结果 */
export interface TerminalDeviceResult extends BaseEntity {
terminalNo?: string;
name?: string;
mchNo?: string;
mchName?: string;
storeNo?: string;
storeName?: string;
enable?: boolean;
remark?: string;
}
/** 通道终端查询 */
export interface ChannelTerminalQuery {
mchNo?: string;
channelMchNo?: string;
channel?: string;
product?: string;
type?: string;
name?: string;
outTerminalNo?: string;
status?: string;
}
/** 通道终端参数 */
export interface ChannelTerminalParam {
id?: string;
mchNo?: string;
channelMchNo?: string;
type?: string;
name?: string;
outTerminalNo?: string;
status?: string;
errorMsg?: string;
remark?: string;
}
/** 通道终端结果 */
export interface ChannelTerminalResult extends BaseEntity {
channelMchNo?: string;
product?: string;
channel?: string;
type?: string;
name?: string;
outTerminalNo?: string;
status?: string;
errorMsg?: string;
remark?: string;
mchNo?: string;
mchName?: string;
}
/** 绑定参数 */
export interface TerminalChannelBindParam {
systemTerminalNo: string;
channelTerminalId: string;
}

View File

@@ -85,6 +85,11 @@ export const PermCodes = {
VIEW: 'merchant:store:view',
MANAGE: 'merchant:store:manage',
},
/** 系统终端 menuCode=merchant:terminal */
Terminal: {
VIEW: 'merchant:terminal:view',
MANAGE: 'merchant:terminal:manage',
},
/** 预留 */
User: {
VIEW: 'merchant:user:view',

View File

@@ -0,0 +1,45 @@
{
"systemTitle": "System Terminals",
"channelTitle": "Channel Terminals",
"field": {
"terminalNo": "Terminal No.",
"name": "Name",
"mchNo": "Merchant No.",
"merchant": "Merchant",
"store": "Store",
"storeNo": "Store No.",
"enable": "Enabled",
"remark": "Remark",
"createTime": "Created At",
"channelMchNo": "Channel Merchant No.",
"channel": "Channel",
"product": "Product",
"type": "Report Type",
"outTerminalNo": "Channel Terminal No.",
"status": "Status",
"errorMsg": "Error Message"
},
"enableYes": "Enabled",
"enableNo": "Disabled",
"add": "Add",
"edit": "Edit",
"bind": "Bind",
"unbind": "Unbind",
"bindManage": "Bindings",
"confirmDelete": "Delete this terminal?",
"confirmUnbind": "Remove this binding?",
"validateName": "Please enter name",
"validateMchNo": "Please select merchant",
"validateChannelMchNo": "Please select channel merchant",
"validateType": "Please select report type",
"validateEnable": "Please select status",
"pleaseSelectMch": "Select merchant",
"pleaseSelectStore": "Select store (optional)",
"pleaseSelectChannelMch": "Select channel merchant",
"pleaseSelectTerminal": "Select system terminal",
"pleaseSelectChannelTerminal": "Select channel terminal",
"boundChannel": "Bound channel terminals",
"boundSystem": "Bound system terminals",
"noBound": "No bindings",
"addBind": "Add binding"
}

View File

@@ -23,6 +23,8 @@
"cardUser": "Merchant User",
"cardUserDesc": "User account management and permission assignment",
"cardStore": "Store Management",
"cardStoreDesc": "Physical store profiles and location management"
"cardStoreDesc": "Physical store profiles and location management",
"cardTerminal": "System Terminals",
"cardTerminalDesc": "Merchant logical terminal codes and channel terminal binding management"
}
}

View File

@@ -0,0 +1,45 @@
{
"systemTitle": "System Terminals",
"channelTitle": "Channel Terminals",
"field": {
"terminalNo": "Terminal No.",
"name": "Name",
"mchNo": "Merchant No.",
"merchant": "Merchant",
"store": "Store",
"storeNo": "Store No.",
"enable": "Enabled",
"remark": "Remark",
"createTime": "Created At",
"channelMchNo": "Channel Merchant No.",
"channel": "Channel",
"product": "Product",
"type": "Report Type",
"outTerminalNo": "Channel Terminal No.",
"status": "Status",
"errorMsg": "Error Message"
},
"enableYes": "Enabled",
"enableNo": "Disabled",
"add": "Add",
"edit": "Edit",
"bind": "Bind",
"unbind": "Unbind",
"bindManage": "Bindings",
"confirmDelete": "Delete this terminal?",
"confirmUnbind": "Remove this binding?",
"validateName": "Please enter name",
"validateMchNo": "Please select merchant",
"validateChannelMchNo": "Please select channel merchant",
"validateType": "Please select report type",
"validateEnable": "Please select status",
"pleaseSelectMch": "Select merchant",
"pleaseSelectStore": "Select store (optional)",
"pleaseSelectChannelMch": "Select channel merchant",
"pleaseSelectTerminal": "Select system terminal",
"pleaseSelectChannelTerminal": "Select channel terminal",
"boundChannel": "Bound channel terminals",
"boundSystem": "Bound system terminals",
"noBound": "No bindings",
"addBind": "Add binding"
}

View File

@@ -23,6 +23,8 @@
"cardUser": "Pengguna Pedagang",
"cardUserDesc": "Manajemen akun pengguna dan penetapan izin",
"cardStore": "Manajemen Toko",
"cardStoreDesc": "Profil toko fisik dan manajemen lokasi"
"cardStoreDesc": "Profil toko fisik dan manajemen lokasi",
"cardTerminal": "Terminal Sistem",
"cardTerminalDesc": "Kelola kode terminal logis pedagang dan pengikatan terminal saluran"
}
}

View File

@@ -0,0 +1,45 @@
{
"systemTitle": "システム端末",
"channelTitle": "チャネル端末",
"field": {
"terminalNo": "Terminal No.",
"name": "Name",
"mchNo": "Merchant No.",
"merchant": "Merchant",
"store": "Store",
"storeNo": "Store No.",
"enable": "Enabled",
"remark": "Remark",
"createTime": "Created At",
"channelMchNo": "Channel Merchant No.",
"channel": "Channel",
"product": "Product",
"type": "Report Type",
"outTerminalNo": "Channel Terminal No.",
"status": "Status",
"errorMsg": "Error Message"
},
"enableYes": "Enabled",
"enableNo": "Disabled",
"add": "追加",
"edit": "編集",
"bind": "紐付け",
"unbind": "解除",
"bindManage": "紐付け管理",
"confirmDelete": "この端末を削除しますか?",
"confirmUnbind": "紐付けを解除しますか?",
"validateName": "Please enter name",
"validateMchNo": "Please select merchant",
"validateChannelMchNo": "Please select channel merchant",
"validateType": "Please select report type",
"validateEnable": "Please select status",
"pleaseSelectMch": "Select merchant",
"pleaseSelectStore": "Select store (optional)",
"pleaseSelectChannelMch": "Select channel merchant",
"pleaseSelectTerminal": "Select system terminal",
"pleaseSelectChannelTerminal": "Select channel terminal",
"boundChannel": "Bound channel terminals",
"boundSystem": "Bound system terminals",
"noBound": "No bindings",
"addBind": "Add binding"
}

View File

@@ -23,6 +23,8 @@
"cardUser": "販売者ユーザー",
"cardUserDesc": "ユーザーアカウントの管理と権限の割り当て",
"cardStore": "店舗管理",
"cardStoreDesc": "実店舗のプロファイルとロケーション管理"
"cardStoreDesc": "実店舗のプロファイルとロケーション管理",
"cardTerminal": "システム端末",
"cardTerminalDesc": "マーチャント論理端末とチャネル端末のバインド管理"
}
}

View File

@@ -0,0 +1,45 @@
{
"systemTitle": "시스템 단말",
"channelTitle": "채널 단말",
"field": {
"terminalNo": "Terminal No.",
"name": "Name",
"mchNo": "Merchant No.",
"merchant": "Merchant",
"store": "Store",
"storeNo": "Store No.",
"enable": "Enabled",
"remark": "Remark",
"createTime": "Created At",
"channelMchNo": "Channel Merchant No.",
"channel": "Channel",
"product": "Product",
"type": "Report Type",
"outTerminalNo": "Channel Terminal No.",
"status": "Status",
"errorMsg": "Error Message"
},
"enableYes": "Enabled",
"enableNo": "Disabled",
"add": "추가",
"edit": "편집",
"bind": "연결",
"unbind": "해제",
"bindManage": "연결 관리",
"confirmDelete": "이 단말을 삭제하시겠습니까?",
"confirmUnbind": "연결을 해제하시겠습니까?",
"validateName": "Please enter name",
"validateMchNo": "Please select merchant",
"validateChannelMchNo": "Please select channel merchant",
"validateType": "Please select report type",
"validateEnable": "Please select status",
"pleaseSelectMch": "Select merchant",
"pleaseSelectStore": "Select store (optional)",
"pleaseSelectChannelMch": "Select channel merchant",
"pleaseSelectTerminal": "Select system terminal",
"pleaseSelectChannelTerminal": "Select channel terminal",
"boundChannel": "Bound channel terminals",
"boundSystem": "Bound system terminals",
"noBound": "No bindings",
"addBind": "Add binding"
}

View File

@@ -23,6 +23,8 @@
"cardUser": "판매자 사용자",
"cardUserDesc": "사용자 계정 관리 및 권한 할당",
"cardStore": "매장관리",
"cardStoreDesc": "실제 매장 프로필 및 위치 관리"
"cardStoreDesc": "실제 매장 프로필 및 위치 관리",
"cardTerminal": "시스템 단말",
"cardTerminalDesc": "머천트 논리 단말 및 채널 단말 바인딩 관리"
}
}

View File

@@ -0,0 +1,45 @@
{
"systemTitle": "System Terminals",
"channelTitle": "Channel Terminals",
"field": {
"terminalNo": "Terminal No.",
"name": "Name",
"mchNo": "Merchant No.",
"merchant": "Merchant",
"store": "Store",
"storeNo": "Store No.",
"enable": "Enabled",
"remark": "Remark",
"createTime": "Created At",
"channelMchNo": "Channel Merchant No.",
"channel": "Channel",
"product": "Product",
"type": "Report Type",
"outTerminalNo": "Channel Terminal No.",
"status": "Status",
"errorMsg": "Error Message"
},
"enableYes": "Enabled",
"enableNo": "Disabled",
"add": "Add",
"edit": "Edit",
"bind": "Bind",
"unbind": "Unbind",
"bindManage": "Bindings",
"confirmDelete": "Delete this terminal?",
"confirmUnbind": "Remove this binding?",
"validateName": "Please enter name",
"validateMchNo": "Please select merchant",
"validateChannelMchNo": "Please select channel merchant",
"validateType": "Please select report type",
"validateEnable": "Please select status",
"pleaseSelectMch": "Select merchant",
"pleaseSelectStore": "Select store (optional)",
"pleaseSelectChannelMch": "Select channel merchant",
"pleaseSelectTerminal": "Select system terminal",
"pleaseSelectChannelTerminal": "Select channel terminal",
"boundChannel": "Bound channel terminals",
"boundSystem": "Bound system terminals",
"noBound": "No bindings",
"addBind": "Add binding"
}

View File

@@ -23,6 +23,8 @@
"cardUser": "Pengguna Saudagar",
"cardUserDesc": "Pengurusan akaun pengguna dan tugasan kebenaran",
"cardStore": "Pengurusan Stor",
"cardStoreDesc": "Profil kedai fizikal dan pengurusan lokasi"
"cardStoreDesc": "Profil kedai fizikal dan pengurusan lokasi",
"cardTerminal": "Terminal Sistem",
"cardTerminalDesc": "Urus kod terminal logik pedagang dan pengikatan terminal saluran"
}
}

View File

@@ -0,0 +1,45 @@
{
"systemTitle": "System Terminals",
"channelTitle": "Channel Terminals",
"field": {
"terminalNo": "Terminal No.",
"name": "Name",
"mchNo": "Merchant No.",
"merchant": "Merchant",
"store": "Store",
"storeNo": "Store No.",
"enable": "Enabled",
"remark": "Remark",
"createTime": "Created At",
"channelMchNo": "Channel Merchant No.",
"channel": "Channel",
"product": "Product",
"type": "Report Type",
"outTerminalNo": "Channel Terminal No.",
"status": "Status",
"errorMsg": "Error Message"
},
"enableYes": "Enabled",
"enableNo": "Disabled",
"add": "Add",
"edit": "Edit",
"bind": "Bind",
"unbind": "Unbind",
"bindManage": "Bindings",
"confirmDelete": "Delete this terminal?",
"confirmUnbind": "Remove this binding?",
"validateName": "Please enter name",
"validateMchNo": "Please select merchant",
"validateChannelMchNo": "Please select channel merchant",
"validateType": "Please select report type",
"validateEnable": "Please select status",
"pleaseSelectMch": "Select merchant",
"pleaseSelectStore": "Select store (optional)",
"pleaseSelectChannelMch": "Select channel merchant",
"pleaseSelectTerminal": "Select system terminal",
"pleaseSelectChannelTerminal": "Select channel terminal",
"boundChannel": "Bound channel terminals",
"boundSystem": "Bound system terminals",
"noBound": "No bindings",
"addBind": "Add binding"
}

View File

@@ -23,6 +23,8 @@
"cardUser": "ผู้ใช้ร้านค้า",
"cardUserDesc": "การจัดการบัญชีผู้ใช้และการมอบหมายสิทธิ์",
"cardStore": "การจัดการร้านค้า",
"cardStoreDesc": "โปรไฟล์ร้านค้าทางกายภาพและการจัดการสถานที่ตั้ง"
"cardStoreDesc": "โปรไฟล์ร้านค้าทางกายภาพและการจัดการสถานที่ตั้ง",
"cardTerminal": "เทอร์มินัลระบบ",
"cardTerminalDesc": "จัดการรหัสเทอร์มินัลตรรกะของผู้ค้าและการผูกเทอร์มินัลช่องทาง"
}
}

View File

@@ -0,0 +1,45 @@
{
"systemTitle": "System Terminals",
"channelTitle": "Channel Terminals",
"field": {
"terminalNo": "Terminal No.",
"name": "Name",
"mchNo": "Merchant No.",
"merchant": "Merchant",
"store": "Store",
"storeNo": "Store No.",
"enable": "Enabled",
"remark": "Remark",
"createTime": "Created At",
"channelMchNo": "Channel Merchant No.",
"channel": "Channel",
"product": "Product",
"type": "Report Type",
"outTerminalNo": "Channel Terminal No.",
"status": "Status",
"errorMsg": "Error Message"
},
"enableYes": "Enabled",
"enableNo": "Disabled",
"add": "Add",
"edit": "Edit",
"bind": "Bind",
"unbind": "Unbind",
"bindManage": "Bindings",
"confirmDelete": "Delete this terminal?",
"confirmUnbind": "Remove this binding?",
"validateName": "Please enter name",
"validateMchNo": "Please select merchant",
"validateChannelMchNo": "Please select channel merchant",
"validateType": "Please select report type",
"validateEnable": "Please select status",
"pleaseSelectMch": "Select merchant",
"pleaseSelectStore": "Select store (optional)",
"pleaseSelectChannelMch": "Select channel merchant",
"pleaseSelectTerminal": "Select system terminal",
"pleaseSelectChannelTerminal": "Select channel terminal",
"boundChannel": "Bound channel terminals",
"boundSystem": "Bound system terminals",
"noBound": "No bindings",
"addBind": "Add binding"
}

View File

@@ -23,6 +23,8 @@
"cardUser": "Người dùng thương gia",
"cardUserDesc": "Quản lý tài khoản người dùng và phân quyền",
"cardStore": "Quản lý cửa hàng",
"cardStoreDesc": "Hồ sơ cửa hàng vật lý và quản lý vị trí"
"cardStoreDesc": "Hồ sơ cửa hàng vật lý và quản lý vị trí",
"cardTerminal": "Thiết bị hệ thống",
"cardTerminalDesc": "Quản lý mã thiết bị logic của thương gia và liên kết thiết bị kênh"
}
}

View File

@@ -0,0 +1,45 @@
{
"systemTitle": "系统终端",
"channelTitle": "通道终端",
"field": {
"terminalNo": "终端编码",
"name": "终端名称",
"mchNo": "商户号",
"merchant": "商户",
"store": "门店",
"storeNo": "门店号",
"enable": "启用",
"remark": "备注",
"createTime": "创建时间",
"channelMchNo": "通道商户号",
"channel": "通道",
"product": "支付产品",
"type": "报送类型",
"outTerminalNo": "通道终端号",
"status": "登记状态",
"errorMsg": "错误信息"
},
"enableYes": "启用",
"enableNo": "停用",
"add": "新增",
"edit": "编辑",
"bind": "绑定",
"unbind": "解绑",
"bindManage": "绑定管理",
"confirmDelete": "确定删除该终端吗?",
"confirmUnbind": "确定解除绑定吗?",
"validateName": "请输入终端名称",
"validateMchNo": "请选择商户",
"validateChannelMchNo": "请选择通道商户",
"validateType": "请选择报送类型",
"validateEnable": "请选择启用状态",
"pleaseSelectMch": "请选择商户",
"pleaseSelectStore": "请选择门店(可选)",
"pleaseSelectChannelMch": "请选择通道商户",
"pleaseSelectTerminal": "请选择系统终端",
"pleaseSelectChannelTerminal": "请选择通道终端",
"boundChannel": "已绑定通道终端",
"boundSystem": "已绑定系统终端",
"noBound": "暂无绑定",
"addBind": "添加绑定"
}

View File

@@ -23,6 +23,8 @@
"cardUser": "商户用户",
"cardUserDesc": "用户账号管理与权限分配",
"cardStore": "门店管理",
"cardStoreDesc": "商户物理门店档案与位置信息管理"
"cardStoreDesc": "商户物理门店档案与位置信息管理",
"cardTerminal": "系统终端",
"cardTerminalDesc": "商户逻辑终端编号与通道终端绑定管理"
}
}

View File

@@ -0,0 +1,45 @@
{
"systemTitle": "系統終端",
"channelTitle": "通道終端",
"field": {
"terminalNo": "终端编码",
"name": "终端名称",
"mchNo": "商户号",
"merchant": "商户",
"store": "门店",
"storeNo": "门店号",
"enable": "启用",
"remark": "备注",
"createTime": "创建时间",
"channelMchNo": "通道商户号",
"channel": "通道",
"product": "支付产品",
"type": "报送类型",
"outTerminalNo": "通道终端号",
"status": "登记状态",
"errorMsg": "错误信息"
},
"enableYes": "启用",
"enableNo": "停用",
"add": "新增",
"edit": "編輯",
"bind": "綁定",
"unbind": "解綁",
"bindManage": "綁定管理",
"confirmDelete": "確定刪除該終端嗎?",
"confirmUnbind": "確定解除綁定嗎?",
"validateName": "请输入终端名称",
"validateMchNo": "请选择商户",
"validateChannelMchNo": "请选择通道商户",
"validateType": "请选择报送类型",
"validateEnable": "请选择启用状态",
"pleaseSelectMch": "请选择商户",
"pleaseSelectStore": "请选择门店(可选)",
"pleaseSelectChannelMch": "请选择通道商户",
"pleaseSelectTerminal": "请选择系统终端",
"pleaseSelectChannelTerminal": "请选择通道终端",
"boundChannel": "已绑定通道终端",
"boundSystem": "已绑定系统终端",
"noBound": "暂无绑定",
"addBind": "添加绑定"
}

View File

@@ -23,6 +23,8 @@
"cardUser": "商户用户",
"cardUserDesc": "用户賬號管理與權限分配",
"cardStore": "門店管理",
"cardStoreDesc": "商户物理門店檔案與位置信息管理"
"cardStoreDesc": "商户物理門店檔案與位置信息管理",
"cardTerminal": "系統終端",
"cardTerminalDesc": "商户邏輯終端編號與通道終端綁定管理"
}
}

View File

@@ -0,0 +1,45 @@
{
"systemTitle": "系統終端",
"channelTitle": "通道終端",
"field": {
"terminalNo": "终端编码",
"name": "终端名称",
"mchNo": "商户号",
"merchant": "商户",
"store": "门店",
"storeNo": "门店号",
"enable": "启用",
"remark": "备注",
"createTime": "创建时间",
"channelMchNo": "通道商户号",
"channel": "通道",
"product": "支付产品",
"type": "报送类型",
"outTerminalNo": "通道终端号",
"status": "登记状态",
"errorMsg": "错误信息"
},
"enableYes": "启用",
"enableNo": "停用",
"add": "新增",
"edit": "編輯",
"bind": "綁定",
"unbind": "解綁",
"bindManage": "綁定管理",
"confirmDelete": "確定刪除該終端嗎?",
"confirmUnbind": "確定解除綁定嗎?",
"validateName": "请输入终端名称",
"validateMchNo": "请选择商户",
"validateChannelMchNo": "请选择通道商户",
"validateType": "请选择报送类型",
"validateEnable": "请选择启用状态",
"pleaseSelectMch": "请选择商户",
"pleaseSelectStore": "请选择门店(可选)",
"pleaseSelectChannelMch": "请选择通道商户",
"pleaseSelectTerminal": "请选择系统终端",
"pleaseSelectChannelTerminal": "请选择通道终端",
"boundChannel": "已绑定通道终端",
"boundSystem": "已绑定系统终端",
"noBound": "暂无绑定",
"addBind": "添加绑定"
}

View File

@@ -23,6 +23,8 @@
"cardUser": "商戶使用者",
"cardUserDesc": "使用者賬號管理與許可權分配",
"cardStore": "門店管理",
"cardStoreDesc": "商戶物理門店檔案與位置資訊管理"
"cardStoreDesc": "商戶物理門店檔案與位置資訊管理",
"cardTerminal": "系統終端",
"cardTerminalDesc": "商戶邏輯終端編號與通道終端綁定管理"
}
}

View File

@@ -93,6 +93,6 @@
"menu.system.security": "Security Management",
"menu.system.security.system": "System Security",
"menu.system.security.pay": "Pay Security",
"menu.device.terminal.system": "System Terminals",
"menu.payment.merchant.terminal": "System Terminals",
"menu.device.terminal.channel": "Channel Terminals"
}

View File

@@ -93,6 +93,6 @@
"menu.system.security": "Manajemen Keamanan",
"menu.system.security.system": "Keamanan Sistem",
"menu.system.security.pay": "Keamanan Pembayaran",
"menu.device.terminal.system": "Terminal Sistem",
"menu.payment.merchant.terminal": "Terminal Sistem",
"menu.device.terminal.channel": "Terminal Saluran"
}

View File

@@ -93,6 +93,6 @@
"menu.system.security": "セキュリティ管理",
"menu.system.security.system": "システムセキュリティ",
"menu.system.security.pay": "決済セキュリティ",
"menu.device.terminal.system": "システム端末",
"menu.payment.merchant.terminal": "システム端末",
"menu.device.terminal.channel": "チャネル端末"
}

View File

@@ -93,6 +93,6 @@
"menu.system.security": "보안 관리",
"menu.system.security.system": "시스템 보안",
"menu.system.security.pay": "결제 보안",
"menu.device.terminal.system": "시스템 단말",
"menu.payment.merchant.terminal": "시스템 단말",
"menu.device.terminal.channel": "채널 단말"
}

View File

@@ -93,6 +93,6 @@
"menu.system.security": "Pengurusan Keselamatan",
"menu.system.security.system": "Keselamatan Sistem",
"menu.system.security.pay": "Keselamatan Pembayaran",
"menu.device.terminal.system": "Terminal Sistem",
"menu.payment.merchant.terminal": "Terminal Sistem",
"menu.device.terminal.channel": "Terminal Saluran"
}

View File

@@ -93,6 +93,6 @@
"menu.system.security": "การจัดการความปลอดภัย",
"menu.system.security.system": "ความปลอดภัยระบบ",
"menu.system.security.pay": "ความปลอดภัยการชำระเงิน",
"menu.device.terminal.system": "เทอร์มินัลระบบ",
"menu.payment.merchant.terminal": "เทอร์มินัลระบบ",
"menu.device.terminal.channel": "เทอร์มินัลช่องทาง"
}

View File

@@ -93,6 +93,6 @@
"menu.system.security": "Quản lý bảo mật",
"menu.system.security.system": "Bảo mật hệ thống",
"menu.system.security.pay": "Bảo mật thanh toán",
"menu.device.terminal.system": "Thiết bị hệ thống",
"menu.payment.merchant.terminal": "Thiết bị hệ thống",
"menu.device.terminal.channel": "Thiết bị kênh"
}

View File

@@ -93,6 +93,6 @@
"menu.system.security": "安全管理",
"menu.system.security.system": "系统安全配置",
"menu.system.security.pay": "支付安全配置",
"menu.device.terminal.system": "系统终端",
"menu.payment.merchant.terminal": "系统终端",
"menu.device.terminal.channel": "通道终端"
}

View File

@@ -93,6 +93,6 @@
"menu.system.security": "安全管理",
"menu.system.security.system": "系統安全配置",
"menu.system.security.pay": "支付安全配置",
"menu.device.terminal.system": "系統終端",
"menu.payment.merchant.terminal": "系統終端",
"menu.device.terminal.channel": "通道終端"
}

View File

@@ -93,6 +93,6 @@
"menu.system.security": "安全管理",
"menu.system.security.system": "系統安全配置",
"menu.system.security.pay": "支付安全配置",
"menu.device.terminal.system": "系統終端",
"menu.payment.merchant.terminal": "系統終端",
"menu.device.terminal.channel": "通道終端"
}

View File

@@ -0,0 +1,148 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { $t } from '@vben/locales';
import {
ChannelTerminalApi,
TerminalDeviceApi,
type ChannelTerminalResult,
type TerminalDeviceResult,
} from '#/api/payment/device/terminal.api';
import { PermCodes } from '#/constants/perm-codes';
import { useMessage } from '#/hooks/useMessage';
import { usePermission } from '#/hooks/usePermission';
const { confirm, message } = useMessage();
const { hasPermission } = usePermission();
const visible = ref(false);
const loading = ref(false);
const current = ref<ChannelTerminalResult | null>(null);
const boundList = ref<TerminalDeviceResult[]>([]);
const candidateList = ref<TerminalDeviceResult[]>([]);
const selectedTerminalNo = ref<string | undefined>();
const title = computed(() => {
if (!current.value) {
return $t('payment.device.terminal.bindManage');
}
return `${$t('payment.device.terminal.bindManage')} - ${current.value.name || current.value.id}`;
});
/**
* 打开绑定抽屉
*/
async function show(row: ChannelTerminalResult) {
current.value = row;
selectedTerminalNo.value = undefined;
visible.value = true;
await refresh();
}
/**
* 刷新绑定列表
*/
async function refresh() {
if (!current.value?.id || !current.value.mchNo) {
return;
}
loading.value = true;
try {
const [boundRes, allRes] = await Promise.all([
ChannelTerminalApi.boundSystemList(current.value.id),
TerminalDeviceApi.listByMchNo(current.value.mchNo),
]);
boundList.value = boundRes.data || [];
const boundNos = new Set(boundList.value.map((i) => i.terminalNo));
candidateList.value = (allRes.data || []).filter((i) => !boundNos.has(i.terminalNo));
} finally {
loading.value = false;
}
}
/**
* 添加绑定
*/
async function handleBind() {
if (!selectedTerminalNo.value || !current.value?.id) {
message.warning($t('payment.device.terminal.pleaseSelectTerminal'));
return;
}
await ChannelTerminalApi.bind({
systemTerminalNo: selectedTerminalNo.value,
channelTerminalId: current.value.id,
});
message.success($t('common.operationSuccess'));
selectedTerminalNo.value = undefined;
await refresh();
}
/**
* 解绑
*/
function handleUnbind(row: TerminalDeviceResult) {
if (!current.value?.id || !row.terminalNo) {
return;
}
confirm({
content: $t('payment.device.terminal.confirmUnbind'),
onOk() {
return ChannelTerminalApi.unbind({
systemTerminalNo: row.terminalNo!,
channelTerminalId: current.value!.id!,
}).then(async () => {
message.success($t('common.operationSuccess'));
await refresh();
});
},
});
}
defineExpose({ show });
</script>
<template>
<a-drawer v-model:open="visible" :title="title" :width="640" :destroy-on-close="true">
<a-spin :spinning="loading">
<div v-if="hasPermission(PermCodes.Channel.Merchant.MANAGE)" class="mb-4 flex gap-2">
<a-select
v-model:value="selectedTerminalNo"
class="flex-1"
:placeholder="$t('payment.device.terminal.pleaseSelectTerminal')"
:options="
candidateList.map((i) => ({
label: `${i.name || '-'} (${i.terminalNo})`,
value: i.terminalNo,
}))
"
show-search
option-filter-prop="label"
allow-clear
/>
<a-button type="primary" @click="handleBind">{{ $t('payment.device.terminal.addBind') }}</a-button>
</div>
<div class="mb-2 font-medium">{{ $t('payment.device.terminal.boundSystem') }}</div>
<a-empty v-if="boundList.length === 0" :description="$t('payment.device.terminal.noBound')" />
<a-list v-else :data-source="boundList" bordered>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :title="item.name" :description="item.terminalNo" />
<template #actions>
<a-button
v-if="hasPermission(PermCodes.Channel.Merchant.MANAGE)"
type="link"
size="small"
danger
@click="handleUnbind(item)"
>
{{ $t('payment.device.terminal.unbind') }}
</a-button>
</template>
</a-list-item>
</template>
</a-list>
</a-spin>
</a-drawer>
</template>

View File

@@ -0,0 +1,178 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { $t } from '@vben/locales';
import {
ChannelTerminalApi,
type ChannelTerminalParam,
type ChannelTerminalResult,
} from '#/api/payment/device/terminal.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, addable } = useFormEdit();
// 通道商户上下文(新增时锁定)
const context = ref<{ mchNo: string; channelMchNo: string }>({ mchNo: '', channelMchNo: '' });
const editReadonly = ref<{ channelMchNo?: string; type?: string }>({});
const formState = ref<ChannelTerminalParam>({
name: '',
type: 'common',
status: 'init',
});
// 报送类型(前端 dict.json)
const typeOptions = computed(() => [
{ label: $t('dict.terminal_type.common'), value: 'common' },
{ label: $t('dict.terminal_type.wechat'), value: 'wechat' },
{ label: $t('dict.terminal_type.alipay'), value: 'alipay' },
{ label: $t('dict.terminal_type.union'), value: 'union' },
]);
// 登记状态(前端 dict.json)
const statusOptions = computed(() => [
{ label: $t('dict.channel_terminal_status.init'), value: 'init' },
{ label: $t('dict.channel_terminal_status.wait'), value: 'wait' },
{ label: $t('dict.channel_terminal_status.submit'), value: 'submit' },
{ label: $t('dict.channel_terminal_status.logged'), value: 'logged' },
{ label: $t('dict.channel_terminal_status.error'), value: 'error' },
]);
const formRules = computed(() => ({
type: [{ required: true, message: $t('payment.device.terminal.validateType') }],
name: [{ required: true, message: $t('payment.device.terminal.validateName') }],
}));
function resetForm() {
formState.value = {
name: '',
type: 'common',
status: 'init',
mchNo: context.value.mchNo,
channelMchNo: context.value.channelMchNo,
};
editReadonly.value = {};
formRef.value?.resetFields();
}
/**
* 打开新增(通道商户上下文已锁定)
*/
function showAdd(ctx: { mchNo: string; channelMchNo: string }) {
context.value = ctx;
initFormEditType(FormEditType.Add);
resetForm();
visible.value = true;
}
/**
* 打开编辑
*/
async function showEdit(record: ChannelTerminalResult) {
context.value = {
mchNo: record.mchNo || '',
channelMchNo: record.channelMchNo || '',
};
initFormEditType(FormEditType.Edit);
resetForm();
confirmLoading.value = true;
visible.value = true;
try {
const { data } = await ChannelTerminalApi.get(record.id!);
const row = data || record;
formState.value = {
id: row.id!,
mchNo: row.mchNo,
channelMchNo: row.channelMchNo,
type: row.type,
name: row.name,
outTerminalNo: row.outTerminalNo,
status: row.status,
errorMsg: row.errorMsg,
remark: row.remark,
};
editReadonly.value = { channelMchNo: row.channelMchNo, type: row.type };
} finally {
confirmLoading.value = false;
}
}
/**
* 保存
*/
async function handleOk() {
try {
await formRef.value?.validate();
} catch {
return;
}
confirmLoading.value = true;
try {
if (addable.value) {
await ChannelTerminalApi.add({
...formState.value,
mchNo: context.value.mchNo,
channelMchNo: context.value.channelMchNo,
});
} else {
await ChannelTerminalApi.update(formState.value);
}
message.success($t('common.operationSuccess'));
handleCancel();
emit('ok');
} finally {
confirmLoading.value = false;
}
}
defineExpose({ showAdd, showEdit });
</script>
<template>
<a-modal
v-model:open="visible"
:title="title"
:confirm-loading="confirmLoading"
:destroy-on-close="true"
:width="640"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form ref="formRef" :model="formState" :rules="formRules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<!-- 通道商户号只读展示 -->
<a-form-item :label="$t('payment.device.terminal.field.channelMchNo')">
<a-input :value="addable ? context.channelMchNo : editReadonly.channelMchNo" disabled />
</a-form-item>
<a-form-item :label="$t('payment.device.terminal.field.type')" name="type">
<a-select
v-model:value="formState.type"
:options="typeOptions"
:disabled="!addable"
:placeholder="$t('common.pleaseSelect')"
/>
</a-form-item>
<a-form-item :label="$t('payment.device.terminal.field.name')" name="name">
<a-input v-model:value="formState.name" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
<a-form-item :label="$t('payment.device.terminal.field.outTerminalNo')" name="outTerminalNo">
<a-input v-model:value="formState.outTerminalNo" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
<a-form-item :label="$t('payment.device.terminal.field.status')" name="status">
<a-select v-model:value="formState.status" :options="statusOptions" :placeholder="$t('common.pleaseSelect')" />
</a-form-item>
<a-form-item :label="$t('payment.device.terminal.field.errorMsg')" name="errorMsg">
<a-input v-model:value="formState.errorMsg" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
<a-form-item :label="$t('payment.device.terminal.field.remark')" name="remark">
<a-textarea v-model:value="formState.remark" :rows="3" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
</a-form>
</a-modal>
</template>

View File

@@ -0,0 +1,234 @@
<script lang="ts" setup>
import type { VxeTableInstance, VxeToolbarInstance } from 'vxe-table';
import { computed, ref, watch } from 'vue';
import { $t } from '@vben/locales';
import {
ChannelTerminalApi,
type ChannelTerminalResult,
} from '#/api/payment/device/terminal.api';
import { PermCodes } from '#/constants/perm-codes';
import { useMessage } from '#/hooks/useMessage';
import { usePermission } from '#/hooks/usePermission';
import ChannelTerminalBindDrawer from './ChannelTerminalBindDrawer.vue';
import ChannelTerminalEdit from './ChannelTerminalEdit.vue';
defineOptions({ name: 'ChannelTerminalPanel' });
const props = defineProps<{
/** 系统商户号 */
mchNo: string;
/** 通道商户号 */
channelMchNo: string;
}>();
const { confirm, message } = useMessage();
const { hasPermission } = usePermission();
const visible = ref(false);
const loading = ref(false);
const xTable = ref<VxeTableInstance>();
const xToolbar = ref<VxeToolbarInstance>();
const editRef = ref<InstanceType<typeof ChannelTerminalEdit>>();
const bindRef = ref<InstanceType<typeof ChannelTerminalBindDrawer>>();
const pageConfig = ref({
currentPage: 1,
pageSize: 10,
total: 0,
});
const tableData = ref<ChannelTerminalResult[]>([]);
const drawerTitle = computed(() => {
// 终端台账 - 通道商户号
return `${$t('payment.merchant.channelMerchant.cardTerminal')} - ${props.channelMchNo || ''}`;
});
/**
* 打开面板
*/
function open() {
visible.value = true;
pageConfig.value.currentPage = 1;
queryPage();
}
/**
* 分页查询(固定当前通道商户)
*/
function queryPage() {
if (!props.channelMchNo) {
return;
}
loading.value = true;
ChannelTerminalApi.page({
current: pageConfig.value.currentPage,
size: pageConfig.value.pageSize,
mchNo: props.mchNo,
channelMchNo: props.channelMchNo,
})
.then((res: any) => {
tableData.value = res.data.records || [];
pageConfig.value.total = Number(res.data.total) || 0;
loading.value = false;
})
.catch(() => {
loading.value = false;
});
}
function handlePageChange({ currentPage, pageSize }: any) {
pageConfig.value.currentPage = currentPage;
pageConfig.value.pageSize = pageSize;
queryPage();
}
function handleAdd() {
editRef.value?.showAdd({ mchNo: props.mchNo, channelMchNo: props.channelMchNo });
}
function handleEdit(row: ChannelTerminalResult) {
editRef.value?.showEdit(row);
}
function handleBind(row: ChannelTerminalResult) {
bindRef.value?.show(row);
}
/**
* 删除通道终端
*/
function handleDelete(row: ChannelTerminalResult) {
confirm({
// 确定删除该终端吗?
content: $t('payment.device.terminal.confirmDelete'),
onOk() {
return ChannelTerminalApi.delete(row.id!).then(() => {
message.success($t('common.operationSuccess'));
queryPage();
});
},
});
}
/**
* 状态文案
*/
function statusLabel(status?: string) {
if (!status) {
return '-';
}
return $t(`dict.channel_terminal_status.${status}`);
}
/**
* 类型文案
*/
function typeLabel(type?: string) {
if (!type) {
return '-';
}
return $t(`dict.terminal_type.${type}`);
}
watch(visible, (open) => {
if (open) {
// 抽屉打开后连接 toolbar
setTimeout(() => {
xTable.value?.connectToolbar(xToolbar.value as VxeToolbarInstance);
}, 0);
}
});
defineExpose({ open });
</script>
<template>
<a-drawer
v-model:open="visible"
:title="drawerTitle"
:width="960"
:destroy-on-close="true"
:body-style="{ paddingBottom: '24px' }"
>
<vxe-toolbar ref="xToolbar" custom refresh :refresh-options="{ queryMethod: queryPage }">
<template #buttons>
<a-button
v-if="hasPermission(PermCodes.Channel.Merchant.MANAGE)"
type="primary"
@click="handleAdd"
>
{{ $t('payment.device.terminal.add') }}
</a-button>
</template>
</vxe-toolbar>
<vxe-table ref="xTable" :row-config="{ keyField: 'id' }" :data="tableData" :loading="loading">
<vxe-column field="name" :title="$t('payment.device.terminal.field.name')" :min-width="120" />
<vxe-column field="type" :title="$t('payment.device.terminal.field.type')" :min-width="120">
<template #default="{ row }">
{{ typeLabel(row.type) }}
</template>
</vxe-column>
<vxe-column
field="outTerminalNo"
:title="$t('payment.device.terminal.field.outTerminalNo')"
:min-width="140"
/>
<vxe-column field="status" :title="$t('payment.device.terminal.field.status')" :min-width="100" align="center">
<template #default="{ row }">
<a-tag>{{ statusLabel(row.status) }}</a-tag>
</template>
</vxe-column>
<vxe-column field="errorMsg" :title="$t('payment.device.terminal.field.errorMsg')" :min-width="120" />
<vxe-column fixed="right" width="220" :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.Channel.Merchant.MANAGE)"
type="link"
size="small"
@click="handleEdit(row)"
>
{{ $t('common.edit') }}
</a-button>
<a-button
v-if="hasPermission(PermCodes.Channel.Merchant.VIEW)"
type="link"
size="small"
@click="handleBind(row)"
>
{{ $t('payment.device.terminal.bindManage') }}
</a-button>
<a-button
v-if="hasPermission(PermCodes.Channel.Merchant.MANAGE)"
type="link"
size="small"
danger
@click="handleDelete(row)"
>
{{ $t('common.delete') }}
</a-button>
</a-space>
</template>
</vxe-column>
</vxe-table>
<div class="mt-3 flex justify-end">
<vxe-pager
v-model:current-page="pageConfig.currentPage"
v-model:page-size="pageConfig.pageSize"
:total="pageConfig.total"
@page-change="handlePageChange"
/>
</div>
<ChannelTerminalEdit ref="editRef" @ok="queryPage" />
<ChannelTerminalBindDrawer ref="bindRef" />
</a-drawer>
</template>

View File

@@ -58,6 +58,15 @@
description: $t('payment.merchant.workbench.workbench.cardStoreDesc'),
route: '/payment/merchant/store',
},
{
key: 'terminal',
// 系统终端
title: $t('payment.merchant.workbench.workbench.cardTerminal'),
icon: 'ant-design:monitor-outlined',
// 商户逻辑终端编号与通道终端绑定管理
description: $t('payment.merchant.workbench.workbench.cardTerminalDesc'),
route: '/payment/merchant/terminal',
},
],
},
{

View File

@@ -0,0 +1,152 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { $t } from '@vben/locales';
import {
ChannelTerminalApi,
type ChannelTerminalResult,
TerminalDeviceApi,
type TerminalDeviceResult,
} from '#/api/payment/device/terminal.api';
import { PermCodes } from '#/constants/perm-codes';
import { useMessage } from '#/hooks/useMessage';
import { usePermission } from '#/hooks/usePermission';
const { confirm, message } = useMessage();
const { hasPermission } = usePermission();
const visible = ref(false);
const loading = ref(false);
const current = ref<null | TerminalDeviceResult>(null);
const boundList = ref<ChannelTerminalResult[]>([]);
const candidateList = ref<ChannelTerminalResult[]>([]);
const selectedChannelId = ref<string | undefined>();
const title = computed(() => {
if (!current.value) {
return $t('payment.device.terminal.bindManage');
}
return `${$t('payment.device.terminal.bindManage')} - ${current.value.terminalNo}`;
});
/**
* 打开绑定抽屉
*/
async function show(row: TerminalDeviceResult) {
current.value = row;
selectedChannelId.value = undefined;
visible.value = true;
await refresh();
}
/**
* 刷新已绑定与候选列表
*/
async function refresh() {
if (!current.value?.terminalNo || !current.value.mchNo) {
return;
}
loading.value = true;
try {
const [boundRes, allRes] = await Promise.all([
TerminalDeviceApi.boundChannelList(current.value.terminalNo),
ChannelTerminalApi.listByMchNo(current.value.mchNo),
]);
boundList.value = boundRes.data || [];
const boundIds = new Set(boundList.value.map((i) => i.id));
candidateList.value = (allRes.data || []).filter((i) => !boundIds.has(i.id));
} finally {
loading.value = false;
}
}
/**
* 添加绑定
*/
async function handleBind() {
if (!selectedChannelId.value || !current.value?.terminalNo) {
message.warning($t('payment.device.terminal.pleaseSelectChannelTerminal'));
return;
}
await TerminalDeviceApi.bind({
systemTerminalNo: current.value.terminalNo,
channelTerminalId: selectedChannelId.value,
});
message.success($t('common.operationSuccess'));
selectedChannelId.value = undefined;
await refresh();
}
/**
* 解绑
*/
function handleUnbind(row: ChannelTerminalResult) {
if (!current.value?.terminalNo || !row.id) {
return;
}
confirm({
// 确定解除绑定吗?
content: $t('payment.device.terminal.confirmUnbind'),
onOk() {
return TerminalDeviceApi.unbind({
systemTerminalNo: current.value!.terminalNo!,
channelTerminalId: row.id!,
}).then(async () => {
message.success($t('common.operationSuccess'));
await refresh();
});
},
});
}
defineExpose({ show });
</script>
<template>
<a-drawer v-model:open="visible" :title="title" :width="640" :destroy-on-close="true">
<a-spin :spinning="loading">
<div v-if="hasPermission(PermCodes.Merchant.Terminal.MANAGE)" class="mb-4 flex gap-2">
<a-select
v-model:value="selectedChannelId"
class="flex-1"
:placeholder="$t('payment.device.terminal.pleaseSelectChannelTerminal')"
:options="
candidateList.map((i) => ({
label: `${i.name || '-'} / ${i.channelMchNo || '-'} / ${i.type || '-'}`,
value: i.id,
}))
"
show-search
option-filter-prop="label"
allow-clear
/>
<a-button type="primary" @click="handleBind">{{ $t('payment.device.terminal.addBind') }}</a-button>
</div>
<div class="mb-2 font-medium">{{ $t('payment.device.terminal.boundChannel') }}</div>
<a-empty v-if="boundList.length === 0" :description="$t('payment.device.terminal.noBound')" />
<a-list v-else :data-source="boundList" bordered>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta
:title="item.name"
:description="`${item.channelMchNo || '-'} · ${item.type || '-'} · ${item.outTerminalNo || '-'}`"
/>
<template #actions>
<a-button
v-if="hasPermission(PermCodes.Merchant.Terminal.MANAGE)"
type="link"
size="small"
danger
@click="handleUnbind(item)"
>
{{ $t('payment.device.terminal.unbind') }}
</a-button>
</template>
</a-list-item>
</template>
</a-list>
</a-spin>
</a-drawer>
</template>

View File

@@ -0,0 +1,163 @@
<script lang="ts" setup>
import type { LabelValue } from '#/types/web';
import { computed, ref } from 'vue';
import { $t } from '@vben/locales';
import {
TerminalDeviceApi,
type TerminalDeviceParam,
type TerminalDeviceResult,
} from '#/api/payment/device/terminal.api';
import { MchStoreInfoApi } from '#/api/payment/merchant/store.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, addable } = useFormEdit();
const storeOptions = ref<LabelValue[]>([]);
const editTerminalNo = ref('');
// 锁定的商户号(由列表页传入, 新增/编辑均使用此商户)
const lockedMchNo = ref<string>('');
const formState = ref<TerminalDeviceParam>({
name: '',
enable: true,
});
const formRules = computed(() => ({
name: [{ required: true, message: $t('payment.device.terminal.validateName') }],
enable: [{ required: true, message: $t('payment.device.terminal.validateEnable') }],
}));
/**
* 按商户加载门店
*/
async function loadStoreOptions(mchNo?: string, keepStoreNo?: string) {
storeOptions.value = [];
if (!keepStoreNo) {
formState.value.storeNo = undefined;
}
if (!mchNo) {
return;
}
const { data } = await MchStoreInfoApi.page({ mchNo, current: 1, size: 200 });
storeOptions.value = (data?.records || []).map((item) => ({
label: item.storeName ? `${item.storeName} (${item.storeNo})` : (item.storeNo as string),
value: item.storeNo as string,
}));
}
function resetForm() {
formState.value = { name: '', enable: true };
editTerminalNo.value = '';
storeOptions.value = [];
formRef.value?.resetFields();
}
/**
* 打开新增(传入锁定商户号)
*/
async function showAdd(mchNo: string) {
initFormEditType(FormEditType.Add);
resetForm();
lockedMchNo.value = mchNo;
formState.value.mchNo = mchNo;
visible.value = true;
await loadStoreOptions(mchNo);
}
/**
* 打开编辑
*/
async function showEdit(record: TerminalDeviceResult) {
initFormEditType(FormEditType.Edit);
resetForm();
confirmLoading.value = true;
visible.value = true;
try {
const { data } = await TerminalDeviceApi.get(record.id!);
const row = data || record;
lockedMchNo.value = row.mchNo || '';
formState.value = {
id: row.id!,
mchNo: row.mchNo,
name: row.name,
storeNo: row.storeNo,
enable: row.enable ?? true,
remark: row.remark,
};
editTerminalNo.value = row.terminalNo || '';
await loadStoreOptions(row.mchNo, row.storeNo);
} finally {
confirmLoading.value = false;
}
}
/**
* 保存
*/
async function handleOk() {
try {
await formRef.value?.validate();
} catch {
return;
}
confirmLoading.value = true;
try {
await (addable.value ? TerminalDeviceApi.add(formState.value) : TerminalDeviceApi.update(formState.value));
message.success($t('common.operationSuccess'));
handleCancel();
emit('ok');
} finally {
confirmLoading.value = false;
}
}
defineExpose({ showAdd, showEdit });
</script>
<template>
<a-modal
v-model:open="visible"
:title="title"
:confirm-loading="confirmLoading"
:destroy-on-close="true"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form ref="formRef" :model="formState" :rules="formRules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item v-if="!addable" :label="$t('payment.device.terminal.field.terminalNo')">
<a-input :value="editTerminalNo" disabled />
</a-form-item>
<a-form-item :label="$t('payment.device.terminal.field.store')" name="storeNo">
<a-select
v-model:value="formState.storeNo"
:options="storeOptions"
:placeholder="$t('payment.device.terminal.pleaseSelectStore')"
allow-clear
show-search
option-filter-prop="label"
/>
</a-form-item>
<a-form-item :label="$t('payment.device.terminal.field.name')" name="name">
<a-input v-model:value="formState.name" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
<a-form-item :label="$t('payment.device.terminal.field.enable')" name="enable">
<a-radio-group v-model:value="formState.enable" button-style="solid">
<a-radio-button :value="true">{{ $t('payment.device.terminal.enableYes') }}</a-radio-button>
<a-radio-button :value="false">{{ $t('payment.device.terminal.enableNo') }}</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item :label="$t('payment.device.terminal.field.remark')" name="remark">
<a-textarea v-model:value="formState.remark" :rows="3" :placeholder="$t('common.pleaseInput')" />
</a-form-item>
</a-form>
</a-modal>
</template>

View File

@@ -0,0 +1,304 @@
<script lang="ts" setup>
import type { VxeTableInstance, VxeToolbarInstance } from 'vxe-table';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { IconifyIcon } from '@vben-core/icons';
import { TerminalDeviceApi, type TerminalDeviceResult } from '#/api/payment/device/terminal.api';
import { MerchantApi, type MerchantInfo } from '#/api/payment/merchant/merchant.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 SystemTerminalBindDrawer from './SystemTerminalBindDrawer.vue';
import SystemTerminalEdit from './SystemTerminalEdit.vue';
defineOptions({ name: 'SystemTerminalList' });
const router = useRouter();
// 商户号从路由 query 获取
const routeContext = useRequiredRouteQuery({
keys: ['mchNo'],
messageKey: 'payment.common.route.missingMchNo',
fallbackPath: '/payment/merchant',
});
const { confirm, message } = useMessage();
const { hasPermission } = usePermission();
const mchNo = computed(() => routeContext.query.value.mchNo);
// 商户信息(用于标题展示商户名称)
const merchantInfo = ref<MerchantInfo>({});
const loading = ref(false);
const xTable = ref<VxeTableInstance>();
const xToolbar = ref<VxeToolbarInstance>();
const editRef = ref<InstanceType<typeof SystemTerminalEdit>>();
const bindRef = ref<InstanceType<typeof SystemTerminalBindDrawer>>();
const queryForm = ref<Record<string, any>>({});
const queryFields = computed<QueryField[]>(() => [
{
type: 'string',
field: 'terminalNo',
// 终端编码
name: $t('payment.device.terminal.field.terminalNo'),
placeholder: $t('common.pleaseInput'),
},
{
type: 'string',
field: 'name',
// 终端名称
name: $t('payment.device.terminal.field.name'),
placeholder: $t('common.pleaseInput'),
},
{
type: 'string',
field: 'storeNo',
// 门店号
name: $t('payment.device.terminal.field.storeNo'),
placeholder: $t('common.pleaseInput'),
},
{
type: 'list',
field: 'enable',
// 启用
name: $t('payment.device.terminal.field.enable'),
placeholder: $t('common.pleaseSelect'),
selectList: [
{ label: $t('payment.device.terminal.enableYes'), value: true },
{ label: $t('payment.device.terminal.enableNo'), value: false },
],
},
]);
const pageConfig = ref({
currentPage: 1,
pageSize: 10,
total: 0,
});
const tableData = ref<TerminalDeviceResult[]>([]);
/**
* 分页查询
*/
function queryPage() {
if (!mchNo.value) {
loading.value = false;
return;
}
loading.value = true;
TerminalDeviceApi.page({
current: pageConfig.value.currentPage,
size: pageConfig.value.pageSize,
mchNo: mchNo.value,
...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();
}
function handleAdd() {
editRef.value?.showAdd(mchNo.value!);
}
function handleEdit(row: TerminalDeviceResult) {
editRef.value?.showEdit(row);
}
function handleBind(row: TerminalDeviceResult) {
bindRef.value?.show(row);
}
/**
* 删除系统终端
*/
function handleDelete(row: TerminalDeviceResult) {
confirm({
// 确定删除该终端吗?
content: $t('payment.device.terminal.confirmDelete'),
onOk() {
return TerminalDeviceApi.delete(row.id!).then(() => {
message.success($t('common.operationSuccess'));
queryPage();
});
},
});
}
onMounted(() => {
if (!routeContext.isValid.value) {
return;
}
xTable.value?.connectToolbar(xToolbar.value as VxeToolbarInstance);
// 加载商户信息用于标题展示
loadMerchantInfo();
queryPage();
});
/**
* 返回商户工作台
*/
function handleBack() {
router.push({
path: '/payment/merchant/manage',
query: { mchNo: mchNo.value },
});
}
/**
* 加载商户信息
*/
function loadMerchantInfo() {
if (!mchNo.value) return;
MerchantApi.findByMchNo(mchNo.value).then(({ data }) => {
if (data) {
merchantInfo.value = data;
}
});
}
</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-3 p-3 bg-background rounded-lg list-page-compact">
<a-card>
<template #title>
<div class="flex items-center gap-2">
<!-- 国际化系统终端 -->
<a-button
type="text"
class="flex items-center justify-center rounded-full hover:bg-accent"
@click="handleBack"
>
<template #icon>
<IconifyIcon icon="ant-design:arrow-left-outlined" class="text-lg" />
</template>
</a-button>
<span class="text-lg font-bold text-foreground">{{ $t('payment.device.terminal.systemTitle') }}</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" />
</a-card>
<div class="mt-4">
<a-card>
<vxe-toolbar ref="xToolbar" custom refresh :refresh-options="{ queryMethod: queryPage }">
<template #buttons>
<a-button v-if="hasPermission(PermCodes.Merchant.Terminal.MANAGE)" type="primary" @click="handleAdd">
{{ $t('payment.device.terminal.add') }}
</a-button>
</template>
</vxe-toolbar>
<vxe-table ref="xTable" :row-config="{ keyField: 'id' }" :data="tableData" :loading="loading">
<vxe-column field="terminalNo" :title="$t('payment.device.terminal.field.terminalNo')" :min-width="200" />
<vxe-column field="name" :title="$t('payment.device.terminal.field.name')" :min-width="140" />
<vxe-column field="storeName" :title="$t('payment.device.terminal.field.store')" :min-width="160">
<template #default="{ row }">
<div v-if="row.storeNo" class="flex flex-col">
<span>{{ row.storeName || '-' }}</span>
<span class="text-xs text-muted-foreground">{{ row.storeNo }}</span>
</div>
<span v-else style="color: var(--text-color-placeholder)">-</span>
</template>
</vxe-column>
<vxe-column
field="enable"
:title="$t('payment.device.terminal.field.enable')"
:min-width="100"
align="center"
>
<template #default="{ row }">
<a-tag v-if="row.enable" color="green">{{ $t('payment.device.terminal.enableYes') }}</a-tag>
<a-tag v-else color="default">{{ $t('payment.device.terminal.enableNo') }}</a-tag>
</template>
</vxe-column>
<vxe-column
field="createTime"
:title="$t('payment.device.terminal.field.createTime')"
:min-width="180"
formatter="formatDateTime"
/>
<vxe-column fixed="right" width="220" :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.Terminal.MANAGE)"
type="link"
size="small"
@click="handleEdit(row)"
>
{{ $t('common.edit') }}
</a-button>
<a-button
v-if="hasPermission(PermCodes.Merchant.Terminal.VIEW)"
type="link"
size="small"
@click="handleBind(row)"
>
{{ $t('payment.device.terminal.bindManage') }}
</a-button>
<a-button
v-if="hasPermission(PermCodes.Merchant.Terminal.MANAGE)"
type="link"
size="small"
danger
@click="handleDelete(row)"
>
{{ $t('common.delete') }}
</a-button>
</a-space>
</template>
</vxe-column>
</vxe-table>
<div class="mt-3 flex justify-end">
<vxe-pager
v-model:current-page="pageConfig.currentPage"
v-model:page-size="pageConfig.pageSize"
:total="pageConfig.total"
@page-change="handlePageChange"
/>
</div>
</a-card>
</div>
<SystemTerminalEdit ref="editRef" @ok="queryPage" />
<SystemTerminalBindDrawer ref="bindRef" />
</div>
</template>