feat(manage): 完善设备管理/支付配置/订单/审计日志模块

- 设备管理: 自营设备固定 source=DIRECT, 第三方设备固定 source=PARTNER, 门店终端管理跳转带 storeOrgId 筛选
- 支付配置: 支持平台全局/渠道/代理商/门店四个层级, 列表显示层级标签与所属组织
- 订单管理: 显示渠道/代理商/门店名称, 设备列显示MAC地址
- 审计日志: 新增审计日志和橘视厅调用日志页面及菜单图标SQL
- 门店管理: 兼容 manage 接口统一返回格式 data:{page:{list}}
This commit is contained in:
freeboss
2026-08-01 14:48:22 +08:00
parent 690568861b
commit 630dfddc59
14 changed files with 761 additions and 150 deletions

50
db/add-menu-icons.sql Normal file
View File

@@ -0,0 +1,50 @@
-- ============================================================
-- 为所有菜单补齐图标(只更新 icon 为空的,已有的保留)
-- 图标使用 Element Plus 图标名(PascalCase),侧边栏通过 iconMap[icon] 渲染
-- 执行前建议先跑:SELECT id,name,icon FROM sys_menu WHERE type IN(0,1) ORDER BY parent_id,order_num;
-- 若菜单 name 与下方不一致,请按实际 name 调整 WHERE 条件
-- ============================================================
-- ===== 一级目录 =====
UPDATE `sys_menu` SET `icon`='Briefcase' WHERE `name`='业务管理' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Connection' WHERE `name`='第三方接口管理' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='VideoCamera' WHERE `name`='媒资管理' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Setting' WHERE `name`='系统管理' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='OfficeBuilding' WHERE `name`='组织管理' AND (`icon` IS NULL OR `icon`='');
-- ===== 业务管理 下 =====
UPDATE `sys_menu` SET `icon`='Share' WHERE `name`='渠道管理' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Shop' WHERE `name`='门店管理' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Monitor' WHERE `name`='设备管理' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='PriceTag' WHERE `name`='定价策略' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Wallet' WHERE `name`='支付配置' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Tickets' WHERE `name`='天卡池管理' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Document' WHERE `name`='天卡订单' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Key' WHERE `name`='天卡激活码' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Coin' WHERE `name`='分润规则' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Wallet' WHERE `name`='分账记录' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='RefreshLeft' WHERE `name`='退款记录' AND (`icon` IS NULL OR `icon`='');
-- ===== 第三方接口管理 下 =====
UPDATE `sys_menu` SET `icon`='UserFilled' WHERE `name`='合作伙伴管理' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Files' WHERE `name`='内容包配置' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='ShoppingCart' WHERE `name`='天卡库存' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Document' WHERE `name`='审计日志' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Monitor' WHERE `name`='设备列表' AND (`icon` IS NULL OR `icon`='');
-- ===== 媒资管理 下 =====
UPDATE `sys_menu` SET `icon`='Film' WHERE `name`='橘视厅媒资' AND (`icon` IS NULL OR `icon`='');
-- ===== 系统管理 下 =====
UPDATE `sys_menu` SET `icon`='User' WHERE `name`='用户管理' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='UserFilled'WHERE `name`='角色管理' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Menu' WHERE `name`='菜单管理' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Tools' WHERE `name`='参数配置' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Memo' WHERE `name`='日志管理' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Memo' WHERE `name`='操作日志' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Memo' WHERE `name`='审计日志记录' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Timer' WHERE `name`='定时任务' AND (`icon` IS NULL OR `icon`='');
UPDATE `sys_menu` SET `icon`='Document' WHERE `name`='定时日志' AND (`icon` IS NULL OR `icon`='');
-- ===== 通用兜底:仍为空的菜单统一给一个默认图标 =====
UPDATE `sys_menu` SET `icon`='Menu' WHERE (`icon` IS NULL OR `icon`='') AND `type` IN (0,1);

View File

@@ -0,0 +1,50 @@
import http from '@/utils/httpRequest'
import type { ManageListResponse, ManagePageParams } from './types'
/**
* 审计日志(管理后台操作审计)
*/
export interface AuditLogItem {
id: number
/** 操作人ID */
actorUserId: number
/** 操作人名 */
name?: string
/** 请求方法 */
method?: string
/** 接口路径 */
api?: string
/** 请求参数 */
param?: string
/** 中文描述 */
paramDesc?: string
/** 操作结果 */
result?: string
/** 变更前 */
before?: string
/** 变更后 */
after?: string
/** 操作原因 */
reason?: string
ip?: string
/** 设备信息 */
deviceInfo?: string
/** 工单号 */
ticketNo?: string
createdAt: string
}
/**
* 审计日志查询参数
*/
export interface AuditLogListParams extends ManagePageParams {
actorUserId?: number
api?: string
method?: string
ticketNo?: string
}
/** 审计日志列表 */
export function getAuditLogList(params?: AuditLogListParams): Promise<ManageListResponse<AuditLogItem>> {
return http.get(http.adornUrl('/manage/audit-log/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
}

View File

@@ -114,6 +114,13 @@ export interface DayCardOrderItem {
/** 非表字段 */ /** 非表字段 */
partnerName?: string partnerName?: string
deviceMac?: string deviceMac?: string
/** 设备MAC(deviceMac 为空时用此字段) */
mac?: string
deviceSn?: string
agentOrgId?: number
/** 组织路径快照 */
orgPathSnapshot?: string
channelOrderNo?: string
} }
/** 订单列表参数 */ /** 订单列表参数 */

View File

@@ -32,21 +32,21 @@ export interface DeviceInfo {
channelOrgId?: number channelOrgId?: number
channelName?: string channelName?: string
agentOrgName?: string agentOrgName?: string
/** 设备来源:PLATFORM-平台 / PARTNER-第三方接口注册 */ /** 设备来源:DIRECT-自营 / PARTNER-第三方接口注册 */
source?: string source?: string
/** 合作伙伴UUID(source=PARTNER 时) */ /** 合作伙伴UUID(source=PARTNER 时) */
partnerId?: string partnerId?: string
} }
/** /**
* 终端列表查询参数 * 终端保存/更新数据
*/ */
export interface DeviceListParams extends ManagePageParams { export interface DeviceListParams extends ManagePageParams {
storeOrgId?: number storeOrgId?: number
status?: string status?: string
deviceName?: string deviceName?: string
mac?: string mac?: string
/** 设备来源:PLATFORM / PARTNER */ /** 设备来源:DIRECT / PARTNER */
source?: string source?: string
/** 合作伙伴UUID(source=PARTNER 时筛选) */ /** 合作伙伴UUID(source=PARTNER 时筛选) */
partnerId?: string partnerId?: string
@@ -68,9 +68,25 @@ export interface DeviceSaveData {
remark?: string remark?: string
} }
/** 下划线转驼峰(兼容后端返回 snake_case 字段,统一为前端驼峰访问) */
function toCamelCase (obj: any): any {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj
const result: any = {}
for (const key in obj) {
result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = obj[key]
}
return result
}
/** 终端分页列表 */ /** 终端分页列表 */
export function getDeviceList(params?: DeviceListParams): Promise<ManageListResponse<DeviceInfo>> { export function getDeviceList(params?: DeviceListParams): Promise<ManageListResponse<DeviceInfo>> {
return http.get(http.adornUrl('/manage/device/list'), { params: http.adornParams(params || {}) }).then(res => res.data) return http.get(http.adornUrl('/manage/device/list'), { params: http.adornParams(params || {}) }).then(res => {
const body = res.data as any
if (body && body.data && body.data.page && Array.isArray(body.data.page.list)) {
body.data.page.list = body.data.page.list.map(toCamelCase)
}
return body
})
} }
/** 终端详情 */ /** 终端详情 */

View File

@@ -26,3 +26,5 @@ export * from './partner'
export * from './content-package' export * from './content-package'
export * from './partner-card' export * from './partner-card'
export * from './openapiCallLog' export * from './openapiCallLog'
export * from './auditLog'
export * from './jstCallLog'

View File

@@ -0,0 +1,50 @@
import http from '@/utils/httpRequest'
import type { ManageListResponse, ManagePageParams } from './types'
/**
* 橘视厅调用日志(设备调用橘视厅 API 的日志)
*/
export interface JstCallLogItem {
id: number
/** 请求URL */
apiPath: string
/** 请求方法 */
requestMethod?: string
/** 请求体 */
requestBody?: string
/** 响应体 */
responseBody?: string
/** HTTP状态码 */
responseCode?: number
/** 是否成功(1=成功,0=失败) */
success: number
/** 耗时(ms) */
duration?: number
/** 错误信息 */
errorMessage?: string
/** 关联设备MAC */
mac?: string
/** 关联设备SN */
deviceSn?: string
/** 关联设备UID */
deviceUid?: string
/** 设备所属门店 */
storeOrgName?: string
createTime: string
}
/**
* 橘视厅调用日志查询参数
*/
export interface JstCallLogListParams extends ManagePageParams {
mac?: string
beginTime?: string
endTime?: string
apiPath?: string
requestBody?: string
}
/** 橘视厅调用日志列表 */
export function getJstCallLogList(params?: JstCallLogListParams): Promise<ManageListResponse<JstCallLogItem>> {
return http.get(http.adornUrl('/manage/jst/call-log/list'), { params: http.adornParams(params || {}) }).then(res => res.data)
}

View File

@@ -2,11 +2,16 @@ import http from '@/utils/httpRequest'
import type { ManageListResponse, ManageResponse, ManagePageParams } from './types' import type { ManageListResponse, ManageResponse, ManagePageParams } from './types'
/** /**
* 微信支付配置信息 * 微信支付配置(按层级:平台全局/渠道/代理商/门店,三选一留空=全局)
*/ */
export interface PayConfigInfo { export interface PayConfigInfo {
id: number id: number
partnerId: number | null /** 渠道组织ID(渠道级配置) */
channelOrgId?: number
/** 代理商组织ID(代理商级配置) */
agentOrgId?: number
/** 门店组织ID(门店级配置) */
storeOrgId?: number
appId: string appId: string
mchId: string mchId: string
apiV3Key: string apiV3Key: string
@@ -15,22 +20,31 @@ export interface PayConfigInfo {
notifyUrl: string notifyUrl: string
status: string status: string
remark: string remark: string
/** join 名称(后端返回) */
channelName?: string
agentOrgName?: string
storeName?: string
} }
/** /**
* 支付配置列表查询参数 * 支付配置列表查询参数
*/ */
export interface PayConfigListParams extends ManagePageParams { export interface PayConfigListParams extends ManagePageParams {
partnerId?: number channelOrgId?: number
agentOrgId?: number
storeOrgId?: number
status?: string status?: string
} }
/** /**
* 支付配置保存/更新数据 * 支付配置保存/更新数据
* 配置层级三选一:channelOrgId/agentOrgId/storeOrgId,都留空=平台全局
*/ */
export interface PayConfigSaveData { export interface PayConfigSaveData {
id?: number id?: number
partnerId?: number channelOrgId?: number
agentOrgId?: number
storeOrgId?: number
appId: string appId: string
mchId: string mchId: string
apiV3Key: string apiV3Key: string
@@ -47,7 +61,7 @@ export function getPayConfigList(params?: PayConfigListParams): Promise<ManageLi
} }
/** 支付配置详情 */ /** 支付配置详情 */
export function getPayConfigInfo(id: number): Promise<ManageResponse<{ config?: PayConfigInfo }>> { export function getPayConfigInfo(id: number): Promise<ManageResponse<PayConfigInfo>> {
return http.get(http.adornUrl(`/manage/payconfig/info/${id}`), { params: http.adornParams() }).then(res => res.data) return http.get(http.adornUrl(`/manage/payconfig/info/${id}`), { params: http.adornParams() }).then(res => res.data)
} }

View File

@@ -0,0 +1,159 @@
<template>
<div class="mod-audit-log">
<el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()">
<el-form-item>
<el-input v-model="dataForm.name" placeholder="操作人" clearable style="width:140px" disabled title="按操作人ID查询,此处仅展示"></el-input>
</el-form-item>
<el-form-item>
<el-input v-model="dataForm.api" placeholder="接口路径" clearable style="width:200px"></el-input>
</el-form-item>
<el-form-item>
<el-input v-model="dataForm.method" placeholder="请求方法" clearable style="width:100px"></el-input>
</el-form-item>
<el-form-item>
<el-input v-model="dataForm.ticketNo" placeholder="工单号" clearable style="width:160px"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
</el-form-item>
</el-form>
<div class="table-page-wrap">
<el-table :data="dataList" border v-loading="dataListLoading" style="width: 100%">
<el-table-column type="index" header-align="center" align="center" width="60" label="序号" />
<el-table-column prop="name" header-align="center" align="center" min-width="120" label="操作人">
<template #default="scope">{{ scope.row.name || scope.row.actorUserId || '-' }}</template>
</el-table-column>
<el-table-column prop="method" header-align="center" align="center" width="80" label="方法" />
<el-table-column prop="api" header-align="center" align="center" min-width="200" label="接口" show-overflow-tooltip />
<el-table-column prop="paramDesc" header-align="center" align="center" min-width="160" label="操作描述" show-overflow-tooltip>
<template #default="scope">{{ scope.row.paramDesc || '-' }}</template>
</el-table-column>
<el-table-column prop="result" header-align="center" align="center" width="100" label="结果">
<template #default="scope">
<el-tag v-if="scope.row.result === 'SUCCESS' || scope.row.result === '成功'" size="small" type="success">成功</el-tag>
<el-tag v-else-if="scope.row.result" size="small" type="danger">{{ scope.row.result }}</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="ip" header-align="center" align="center" width="130" label="IP">
<template #default="scope">{{ scope.row.ip || '-' }}</template>
</el-table-column>
<el-table-column prop="ticketNo" header-align="center" align="center" min-width="140" label="工单号">
<template #default="scope">{{ scope.row.ticketNo || '-' }}</template>
</el-table-column>
<el-table-column prop="createdAt" header-align="center" align="center" width="170" label="操作时间" />
<el-table-column fixed="right" header-align="center" align="center" width="90" label="操作">
<template #default="scope">
<el-button link type="primary" size="small" @click="viewDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
</div>
<!-- 详情弹窗 -->
<el-dialog v-model="detailVisible" title="审计日志详情" width="760px" top="5vh">
<el-descriptions :column="1" border>
<el-descriptions-item label="操作人">{{ detailData.name || detailData.actorUserId || '-' }}</el-descriptions-item>
<el-descriptions-item label="方法">{{ detailData.method || '-' }}</el-descriptions-item>
<el-descriptions-item label="接口">{{ detailData.api || '-' }}</el-descriptions-item>
<el-descriptions-item label="操作描述">{{ detailData.paramDesc || '-' }}</el-descriptions-item>
<el-descriptions-item label="结果">{{ detailData.result || '-' }}</el-descriptions-item>
<el-descriptions-item label="工单号">{{ detailData.ticketNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="IP">{{ detailData.ip || '-' }}</el-descriptions-item>
<el-descriptions-item label="设备信息">{{ detailData.deviceInfo || '-' }}</el-descriptions-item>
<el-descriptions-item label="原因">{{ detailData.reason || '-' }}</el-descriptions-item>
<el-descriptions-item label="请求参数">
<pre style="white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow: auto; margin: 0">{{ detailData.param || '-' }}</pre>
</el-descriptions-item>
<el-descriptions-item label="变更前">
<pre style="white-space: pre-wrap; word-break: break-all; max-height: 160px; overflow: auto; margin: 0">{{ detailData.before || '-' }}</pre>
</el-descriptions-item>
<el-descriptions-item label="变更后">
<pre style="white-space: pre-wrap; word-break: break-all; max-height: 160px; overflow: auto; margin: 0">{{ detailData.after || '-' }}</pre>
</el-descriptions-item>
<el-descriptions-item label="操作时间">{{ detailData.createdAt || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button type="primary" @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { getAuditLogList, type AuditLogItem } from '@/api/manage/auditLog'
interface DataForm {
name: string
api: string
method: string
ticketNo: string
}
const dataForm = reactive<DataForm>({ name: '', api: '', method: '', ticketNo: '' })
const dataList = ref<AuditLogItem[]>([])
const pageIndex = ref<number>(1)
const pageSize = ref<number>(10)
const totalPage = ref<number>(0)
const dataListLoading = ref<boolean>(false)
async function getDataList (): Promise<void> {
dataListLoading.value = true
try {
const res = await getAuditLogList({
page: pageIndex.value,
limit: pageSize.value,
api: dataForm.api || undefined,
method: dataForm.method || undefined,
ticketNo: dataForm.ticketNo || undefined
})
if (res && res.code === 0) {
dataList.value = res.data?.page?.list || []
totalPage.value = res.data?.page?.totalCount || 0
} else {
dataList.value = []
totalPage.value = 0
}
} finally {
dataListLoading.value = false
}
}
function sizeChangeHandle (val: number): void {
pageSize.value = val
pageIndex.value = 1
getDataList()
}
function currentChangeHandle (val: number): void {
pageIndex.value = val
getDataList()
}
const detailVisible = ref<boolean>(false)
const detailData = reactive<Partial<AuditLogItem>>({})
function viewDetail (row: AuditLogItem): void {
Object.assign(detailData, row)
detailVisible.value = true
}
onMounted(() => {
getDataList()
})
</script>
<style scoped>
.mod-audit-log {
padding: 10px;
}
</style>

View File

@@ -13,9 +13,6 @@
<el-option label="已取消" value="CANCELLED"></el-option> <el-option label="已取消" value="CANCELLED"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item>
<org-tree-select v-model="dataForm.partnerId" placeholder="合作商" width="160px" />
</el-form-item>
<el-form-item> <el-form-item>
<el-date-picker <el-date-picker
v-model="dateRange" v-model="dateRange"
@@ -35,11 +32,18 @@
<el-table :data="dataList" border v-loading="dataListLoading" style="width: 100%;"> <el-table :data="dataList" border v-loading="dataListLoading" style="width: 100%;">
<el-table-column type="index" header-align="center" align="center" width="60" label="序号" /> <el-table-column type="index" header-align="center" align="center" width="60" label="序号" />
<el-table-column prop="orderNo" header-align="center" align="center" min-width="180" label="订单号" show-overflow-tooltip /> <el-table-column prop="orderNo" header-align="center" align="center" min-width="180" label="订单号" show-overflow-tooltip />
<el-table-column prop="partnerId" header-align="center" align="center" width="100" label="合作商" /> <el-table-column header-align="center" align="center" width="140" label="设备MAC" show-overflow-tooltip>
<el-table-column prop="deviceId" header-align="center" align="center" width="90" label="设备" /> <template #default="{ row }">{{ row.mac || row.deviceMac || '-' }}</template>
<el-table-column prop="deviceMac" header-align="center" align="center" width="140" label="设备MAC" show-overflow-tooltip /> </el-table-column>
<el-table-column prop="storeOrgId" header-align="center" align="center" width="100" label="门店" /> <el-table-column header-align="center" align="center" width="110" label="渠道">
<el-table-column prop="channelOrgId" header-align="center" align="center" width="100" label="渠道" /> <template #default="{ row }">{{ orgNameOf(row.channelOrgId) }}</template>
</el-table-column>
<el-table-column header-align="center" align="center" width="110" label="代理商">
<template #default="{ row }">{{ orgNameOf(row.agentOrgId) }}</template>
</el-table-column>
<el-table-column header-align="center" align="center" width="110" label="门店">
<template #default="{ row }">{{ orgNameOf(row.storeOrgId) }}</template>
</el-table-column>
<el-table-column prop="cardNo" header-align="center" align="center" min-width="160" label="卡密(激活码)" show-overflow-tooltip /> <el-table-column prop="cardNo" header-align="center" align="center" min-width="160" label="卡密(激活码)" show-overflow-tooltip />
<el-table-column prop="status" header-align="center" align="center" width="100" label="订单状态"> <el-table-column prop="status" header-align="center" align="center" width="100" label="订单状态">
<template #default="{ row }"> <template #default="{ row }">
@@ -77,11 +81,11 @@
<el-dialog v-model="detailVisible" title="订单详情" width="720px"> <el-dialog v-model="detailVisible" title="订单详情" width="720px">
<el-descriptions :column="2" border v-loading="detailLoading"> <el-descriptions :column="2" border v-loading="detailLoading">
<el-descriptions-item label="订单号">{{ detailData?.orderNo || '-' }}</el-descriptions-item> <el-descriptions-item label="订单号">{{ detailData?.orderNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="合作商">{{ detailData?.partnerId ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="设备">{{ detailData?.deviceId ?? '-' }}</el-descriptions-item> <el-descriptions-item label="设备">{{ detailData?.deviceId ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="设备MAC">{{ detailData?.deviceMac || '-' }}</el-descriptions-item> <el-descriptions-item label="设备MAC">{{ detailData?.mac || detailData?.deviceMac || '-' }}</el-descriptions-item>
<el-descriptions-item label="门店">{{ detailData?.storeOrgId ?? '-' }}</el-descriptions-item> <el-descriptions-item label="渠道">{{ orgNameOf(detailData?.channelOrgId) }}</el-descriptions-item>
<el-descriptions-item label="渠道">{{ detailData?.channelOrgId ?? '-' }}</el-descriptions-item> <el-descriptions-item label="代理商">{{ orgNameOf(detailData?.agentOrgId) }}</el-descriptions-item>
<el-descriptions-item label="门店">{{ orgNameOf(detailData?.storeOrgId) }}</el-descriptions-item>
<el-descriptions-item label="卡密(激活码)">{{ detailData?.cardNo || '-' }}</el-descriptions-item> <el-descriptions-item label="卡密(激活码)">{{ detailData?.cardNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="订单状态"> <el-descriptions-item label="订单状态">
<el-tag v-if="detailData?.status" :type="getStatusTagType(detailData.status)" size="small"> <el-tag v-if="detailData?.status" :type="getStatusTagType(detailData.status)" size="small">
@@ -109,17 +113,16 @@
import { reactive, ref, onMounted } from 'vue' import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { getDayCardOrderList, getDayCardOrderInfo, type DayCardOrderItem } from '@/api/manage/daycard' import { getDayCardOrderList, getDayCardOrderInfo, type DayCardOrderItem } from '@/api/manage/daycard'
import { getOrgAllTree } from '@/api/org'
interface DataForm { interface DataForm {
orderNo: string orderNo: string
status: string status: string
partnerId: string
} }
const dataForm = reactive<DataForm>({ const dataForm = reactive<DataForm>({
orderNo: '', orderNo: '',
status: '', status: ''
partnerId: ''
}) })
const dateRange = ref<[string, string] | []>([]) const dateRange = ref<[string, string] | []>([])
@@ -164,6 +167,37 @@ function formatAmount (amount: number | null | undefined): string {
return '¥' + Number(amount).toFixed(2) return '¥' + Number(amount).toFixed(2)
} }
/** 组织 ID -> 名称 映射(回显渠道/代理商/门店) */
const orgMap = ref<Map<number, string>>(new Map())
function flattenOrgTree (nodes: any[]): Map<number, string> {
const map = new Map<number, string>()
const walk = (list: any[]) => {
list.forEach(n => {
map.set(n.id, n.name)
if (n.children?.length) walk(n.children)
})
}
walk(nodes)
return map
}
async function loadOrgMap (): Promise<void> {
try {
const res = await getOrgAllTree()
const body = (res as any).data
const list = body?.data?.list || body?.data || []
orgMap.value = flattenOrgTree(Array.isArray(list) ? list : [])
} catch {
// 静默
}
}
function orgNameOf (orgId?: number | null): string {
if (orgId == null) return '-'
return orgMap.value.get(orgId) || `#${orgId}`
}
async function getDataList (): Promise<void> { async function getDataList (): Promise<void> {
dataListLoading.value = true dataListLoading.value = true
try { try {
@@ -173,7 +207,6 @@ async function getDataList (): Promise<void> {
} }
if (dataForm.orderNo) params.orderNo = dataForm.orderNo if (dataForm.orderNo) params.orderNo = dataForm.orderNo
if (dataForm.status) params.status = dataForm.status if (dataForm.status) params.status = dataForm.status
if (dataForm.partnerId) params.partnerId = dataForm.partnerId
if (dateRange.value && dateRange.value.length === 2) { if (dateRange.value && dateRange.value.length === 2) {
params.startDate = dateRange.value[0] params.startDate = dateRange.value[0]
params.endDate = dateRange.value[1] params.endDate = dateRange.value[1]
@@ -231,6 +264,7 @@ async function detailHandle (row: DayCardOrderItem): Promise<void> {
} }
onMounted(() => { onMounted(() => {
loadOrgMap()
getDataList() getDataList()
}) })
</script> </script>

View File

@@ -33,12 +33,6 @@
width="60" width="60"
label="序号"> label="序号">
</el-table-column> </el-table-column>
<el-table-column
prop="deviceName"
header-align="center"
align="center"
label="设备名称">
</el-table-column>
<el-table-column <el-table-column
prop="mac" prop="mac"
header-align="center" header-align="center"
@@ -91,12 +85,12 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="storeName"
header-align="center" header-align="center"
align="center" align="center"
min-width="140"
label="所属门店"> label="所属门店">
<template #default="scope"> <template #default="scope">
{{ scope.row.storeName || '-' }} {{ scope.row.storeName || orgNameOf(scope.row.storeOrgId) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
@@ -228,16 +222,42 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref, onMounted } from 'vue' import { reactive, ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus' import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { useRoute } from 'vue-router'
import { getDeviceList, saveOrUpdateDevice, deleteDevice, unbindDeviceOrg, unbindDayCard, type DeviceInfo, type DeviceSaveData } from '@/api/manage/device' import { getDeviceList, saveOrUpdateDevice, deleteDevice, unbindDeviceOrg, unbindDayCard, type DeviceInfo, type DeviceSaveData } from '@/api/manage/device'
import { getDeviceBenefits, type DeviceBenefits } from '@/api/manage/daycard' import { getDeviceBenefits, type DeviceBenefits } from '@/api/manage/daycard'
import { getOrgAllTree } from '@/api/org'
import OrgTreeSelect from '@/components/org-tree-select/index.vue' import OrgTreeSelect from '@/components/org-tree-select/index.vue'
const route = useRoute()
/** 组织 ID -> 名称(回显门店) */
const orgMap = ref<Record<number, string>>({})
async function loadOrgMap (): Promise<void> {
try {
const res = await getOrgAllTree()
const body = (res as any).data
const list = body?.data?.list || body?.data || []
const walk = (nodes: any[]) => {
nodes.forEach(n => {
orgMap.value[n.id] = n.name
if (n.children?.length) walk(n.children)
})
}
if (Array.isArray(list)) walk(list)
} catch { /* 静默 */ }
}
function orgNameOf (orgId?: number | null): string {
if (orgId == null) return '-'
return orgMap.value[orgId] || `#${orgId}`
}
// ===================== 列表 ===================== // ===================== 列表 =====================
const dataForm = reactive({ const dataForm = reactive({
deviceName: '', deviceName: '',
mac: '', mac: '',
status: '' status: '',
storeOrgId: undefined as number | undefined
}) })
const dataList = ref<DeviceInfo[]>([]) const dataList = ref<DeviceInfo[]>([])
const pageIndex = ref<number>(1) const pageIndex = ref<number>(1)
@@ -253,7 +273,9 @@ async function getDataList (): Promise<void> {
limit: pageSize.value, limit: pageSize.value,
deviceName: dataForm.deviceName, deviceName: dataForm.deviceName,
mac: dataForm.mac, mac: dataForm.mac,
status: dataForm.status status: dataForm.status,
storeOrgId: dataForm.storeOrgId,
source: 'DIRECT'
}) })
if (res && res.code === 0) { if (res && res.code === 0) {
dataList.value = res.data?.page?.list || [] dataList.value = res.data?.page?.list || []
@@ -492,6 +514,12 @@ async function showBenefits (row: DeviceInfo): Promise<void> {
} }
onMounted(() => { onMounted(() => {
// 从门店终端管理跳转来时,带 storeOrgId 筛选该门店设备
const q = route.query.storeOrgId
if (q != null && q !== '') {
dataForm.storeOrgId = Number(q)
}
loadOrgMap()
getDataList() getDataList()
}) })
</script> </script>

View File

@@ -0,0 +1,161 @@
<template>
<div class="mod-jst-call-log">
<el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()">
<el-form-item>
<el-input v-model="dataForm.mac" placeholder="MAC地址" clearable style="width:160px"></el-input>
</el-form-item>
<el-form-item>
<el-input v-model="dataForm.apiPath" placeholder="接口路径" clearable style="width:200px"></el-input>
</el-form-item>
<el-form-item label="开始">
<el-date-picker v-model="dataForm.beginTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="开始时间" style="width:200px" />
</el-form-item>
<el-form-item label="结束">
<el-date-picker v-model="dataForm.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="结束时间" style="width:200px" />
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
</el-form-item>
</el-form>
<div class="table-page-wrap">
<el-table :data="dataList" border v-loading="dataListLoading" style="width: 100%">
<el-table-column type="index" header-align="center" align="center" width="60" label="序号" />
<el-table-column prop="apiPath" header-align="center" align="center" min-width="220" label="接口" show-overflow-tooltip />
<el-table-column prop="requestMethod" header-align="center" align="center" width="80" label="方法" />
<el-table-column prop="success" header-align="center" align="center" width="90" label="结果">
<template #default="scope">
<el-tag v-if="scope.row.success === 1" size="small" type="success">成功</el-tag>
<el-tag v-else size="small" type="danger">失败</el-tag>
</template>
</el-table-column>
<el-table-column prop="responseCode" header-align="center" align="center" width="90" label="状态码">
<template #default="scope">{{ scope.row.responseCode || '-' }}</template>
</el-table-column>
<el-table-column prop="duration" header-align="center" align="center" width="90" label="耗时(ms)" />
<el-table-column prop="mac" header-align="center" align="center" width="140" label="MAC">
<template #default="scope">{{ scope.row.mac || '-' }}</template>
</el-table-column>
<el-table-column prop="deviceSn" header-align="center" align="center" min-width="140" label="设备SN" show-overflow-tooltip>
<template #default="scope">{{ scope.row.deviceSn || '-' }}</template>
</el-table-column>
<el-table-column prop="storeOrgName" header-align="center" align="center" min-width="140" label="门店">
<template #default="scope">{{ scope.row.storeOrgName || '-' }}</template>
</el-table-column>
<el-table-column prop="createTime" header-align="center" align="center" width="170" label="调用时间" />
<el-table-column fixed="right" header-align="center" align="center" width="90" label="操作">
<template #default="scope">
<el-button link type="primary" size="small" @click="viewDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
</div>
<!-- 详情弹窗 -->
<el-dialog v-model="detailVisible" title="调用详情" width="760px" top="5vh">
<el-descriptions :column="1" border>
<el-descriptions-item label="接口">{{ detailData.apiPath }}</el-descriptions-item>
<el-descriptions-item label="方法">{{ detailData.requestMethod || '-' }}</el-descriptions-item>
<el-descriptions-item label="结果">
<el-tag v-if="detailData.success === 1" size="small" type="success">成功</el-tag>
<el-tag v-else size="small" type="danger">失败</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态码">{{ detailData.responseCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="耗时">{{ detailData.duration ?? '-' }} ms</el-descriptions-item>
<el-descriptions-item label="MAC">{{ detailData.mac || '-' }}</el-descriptions-item>
<el-descriptions-item label="设备SN">{{ detailData.deviceSn || '-' }}</el-descriptions-item>
<el-descriptions-item label="设备UID">{{ detailData.deviceUid || '-' }}</el-descriptions-item>
<el-descriptions-item label="门店">{{ detailData.storeOrgName || '-' }}</el-descriptions-item>
<el-descriptions-item v-if="detailData.errorMessage" label="错误信息">{{ detailData.errorMessage }}</el-descriptions-item>
<el-descriptions-item label="请求体">
<pre style="white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow: auto; margin: 0">{{ detailData.requestBody || '-' }}</pre>
</el-descriptions-item>
<el-descriptions-item label="响应体">
<pre style="white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow: auto; margin: 0">{{ detailData.responseBody || '-' }}</pre>
</el-descriptions-item>
<el-descriptions-item label="调用时间">{{ detailData.createTime || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button type="primary" @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { getJstCallLogList, type JstCallLogItem } from '@/api/manage/jstCallLog'
interface DataForm {
mac: string
apiPath: string
beginTime: string
endTime: string
}
const dataForm = reactive<DataForm>({ mac: '', apiPath: '', beginTime: '', endTime: '' })
const dataList = ref<JstCallLogItem[]>([])
const pageIndex = ref<number>(1)
const pageSize = ref<number>(10)
const totalPage = ref<number>(0)
const dataListLoading = ref<boolean>(false)
async function getDataList (): Promise<void> {
dataListLoading.value = true
try {
const res = await getJstCallLogList({
page: pageIndex.value,
limit: pageSize.value,
mac: dataForm.mac || undefined,
apiPath: dataForm.apiPath || undefined,
beginTime: dataForm.beginTime || undefined,
endTime: dataForm.endTime || undefined
})
if (res && res.code === 0) {
dataList.value = res.data?.page?.list || []
totalPage.value = res.data?.page?.totalCount || 0
} else {
dataList.value = []
totalPage.value = 0
}
} finally {
dataListLoading.value = false
}
}
function sizeChangeHandle (val: number): void {
pageSize.value = val
pageIndex.value = 1
getDataList()
}
function currentChangeHandle (val: number): void {
pageIndex.value = val
getDataList()
}
const detailVisible = ref<boolean>(false)
const detailData = reactive<Partial<JstCallLogItem>>({})
function viewDetail (row: JstCallLogItem): void {
Object.assign(detailData, row)
detailVisible.value = true
}
onMounted(() => {
getDataList()
})
</script>
<style scoped>
.mod-jst-call-log {
padding: 10px;
}
</style>

View File

@@ -2,11 +2,9 @@
<div class="mod-pay-config"> <div class="mod-pay-config">
<el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()"> <el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()">
<el-form-item> <el-form-item>
<org-tree-select v-model="dataForm.partnerId" placeholder="合作商" width="180px" /> <el-select v-model="dataForm.status" placeholder="状态" clearable style="width: 120px">
</el-form-item> <el-option label="启用" value="ACTIVE"></el-option>
<el-form-item> <el-option label="禁用" value="INACTIVE"></el-option>
<el-select v-model="dataForm.status" placeholder="状态" clearable style="width: 120px;">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@@ -16,10 +14,23 @@
<el-button type="primary" @click="addHandle()">新增</el-button> <el-button type="primary" @click="addHandle()">新增</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div style="margin-bottom: 8px; color: #909399; font-size: 13px">
配置优先级:门店 &gt; 代理商 &gt; 渠道 &gt; 平台全局(下单时逐级查找)
</div>
<div class="table-page-wrap"> <div class="table-page-wrap">
<el-table :data="dataList" border v-loading="dataListLoading" style="width: 100%;"> <el-table :data="dataList" border v-loading="dataListLoading" style="width: 100%">
<el-table-column type="index" header-align="center" align="center" width="50" label="序号"></el-table-column> <el-table-column type="index" header-align="center" align="center" width="50" label="序号"></el-table-column>
<el-table-column prop="partnerId" header-align="center" align="center" label="合作商"></el-table-column> <el-table-column header-align="center" align="center" width="100" label="层级">
<template #default="scope">
<el-tag v-if="scope.row.storeOrgId" size="small" type="danger">门店</el-tag>
<el-tag v-else-if="scope.row.agentOrgId" size="small" type="warning">代理商</el-tag>
<el-tag v-else-if="scope.row.channelOrgId" size="small" type="success">渠道</el-tag>
<el-tag v-else size="small" type="info">全局</el-tag>
</template>
</el-table-column>
<el-table-column header-align="center" align="center" min-width="160" label="所属组织">
<template #default="scope">{{ orgNameOf(scope.row) }}</template>
</el-table-column>
<el-table-column prop="appId" header-align="center" align="center" label="应用ID"></el-table-column> <el-table-column prop="appId" header-align="center" align="center" label="应用ID"></el-table-column>
<el-table-column prop="mchId" header-align="center" align="center" label="商户号"></el-table-column> <el-table-column prop="mchId" header-align="center" align="center" label="商户号"></el-table-column>
<el-table-column prop="certSerialNo" header-align="center" align="center" label="证书序列号" show-overflow-tooltip></el-table-column> <el-table-column prop="certSerialNo" header-align="center" align="center" label="证书序列号" show-overflow-tooltip></el-table-column>
@@ -49,10 +60,19 @@
layout="total, sizes, prev, pager, next, jumper"> layout="total, sizes, prev, pager, next, jumper">
</el-pagination> </el-pagination>
</div> </div>
<el-dialog v-model="addOrUpdateVisible" :title="addOrUpdateTitle" width="500px">
<el-dialog v-model="addOrUpdateVisible" :title="addOrUpdateTitle" width="560px">
<el-form :model="addOrUpdateForm" :rules="addOrUpdateRules" ref="addOrUpdateFormRef" label-width="120px"> <el-form :model="addOrUpdateForm" :rules="addOrUpdateRules" ref="addOrUpdateFormRef" label-width="120px">
<el-form-item label="合作商" prop="partnerId"> <el-form-item label="配置层级" prop="configLevel">
<org-tree-select v-model="addOrUpdateForm.partnerId" placeholder="请选择合作商" /> <el-radio-group v-model="addOrUpdateForm.configLevel" @change="onLevelChange">
<el-radio label="GLOBAL">平台全局</el-radio>
<el-radio label="CHANNEL">渠道</el-radio>
<el-radio label="AGENT">代理商</el-radio>
<el-radio label="STORE">门店</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="addOrUpdateForm.configLevel !== 'GLOBAL'" label="所属组织" prop="orgId">
<org-tree-select v-model="addOrUpdateForm.orgId" :hide-node-types="hideTypesByLevel" placeholder="请选择组织" />
</el-form-item> </el-form-item>
<el-form-item label="应用ID" prop="appId"> <el-form-item label="应用ID" prop="appId">
<el-input v-model="addOrUpdateForm.appId" placeholder="请输入应用ID"></el-input> <el-input v-model="addOrUpdateForm.appId" placeholder="请输入应用ID"></el-input>
@@ -73,13 +93,7 @@
<el-input v-model="addOrUpdateForm.notifyUrl" placeholder="请输入回调地址"></el-input> <el-input v-model="addOrUpdateForm.notifyUrl" placeholder="请输入回调地址"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-switch <el-switch v-model="addOrUpdateForm.status" active-value="ACTIVE" inactive-value="INACTIVE" active-text="启用" inactive-text="禁用"></el-switch>
v-model="addOrUpdateForm.status"
active-value="ACTIVE"
inactive-value="INACTIVE"
active-text="启用"
inactive-text="禁用">
</el-switch>
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
<el-input v-model="addOrUpdateForm.remark" placeholder="请输入备注"></el-input> <el-input v-model="addOrUpdateForm.remark" placeholder="请输入备注"></el-input>
@@ -94,64 +108,24 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref, onMounted } from 'vue' import { reactive, ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus' import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { getPayConfigList, saveOrUpdatePayConfig, deletePayConfig, type PayConfigInfo } from '@/api/manage/payConfig' import { getPayConfigList, saveOrUpdatePayConfig, deletePayConfig, type PayConfigInfo } from '@/api/manage/payConfig'
import OrgTreeSelect from '@/components/org-tree-select/index.vue'
const statusOptions = [ const dataForm = reactive<{ status: string }>({ status: '' })
{ label: '启用', value: 'ACTIVE' },
{ label: '禁用', value: 'INACTIVE' }
]
const dataForm = reactive<{
partnerId: string | number
status: string
}>({
partnerId: '',
status: ''
})
const dataList = ref<PayConfigInfo[]>([]) const dataList = ref<PayConfigInfo[]>([])
const pageIndex = ref<number>(1) const pageIndex = ref<number>(1)
const pageSize = ref<number>(10) const pageSize = ref<number>(10)
const totalPage = ref<number>(0) const totalPage = ref<number>(0)
const dataListLoading = ref<boolean>(false) const dataListLoading = ref<boolean>(false)
const addOrUpdateVisible = ref<boolean>(false) /** 列表:层级名称回显 */
const addOrUpdateTitle = ref<string>('') function orgNameOf (row: PayConfigInfo): string {
const addOrUpdateFormRef = ref<FormInstance>() if (row.storeOrgId) return row.storeName || `门店#${row.storeOrgId}`
if (row.agentOrgId) return row.agentOrgName || `代理商#${row.agentOrgId}`
interface AddOrUpdateForm { if (row.channelOrgId) return row.channelName || `渠道#${row.channelOrgId}`
id: number | null return '平台全局'
partnerId: number | null
appId: string
mchId: string
apiV3Key: string
certSerialNo: string
privateKeyPath: string
notifyUrl: string
status: string
remark: string
}
const addOrUpdateForm = reactive<AddOrUpdateForm>({
id: null,
partnerId: null,
appId: '',
mchId: '',
apiV3Key: '',
certSerialNo: '',
privateKeyPath: '',
notifyUrl: '',
status: 'ACTIVE',
remark: ''
})
const addOrUpdateRules: FormRules = {
partnerId: [{ required: true, message: '请选择合作商', trigger: 'change' }],
appId: [{ required: true, message: '应用ID不能为空', trigger: 'blur' }],
mchId: [{ required: true, message: '商户号不能为空', trigger: 'blur' }],
apiV3Key: [{ required: true, message: 'API V3密钥不能为空', trigger: 'blur' }],
notifyUrl: [{ required: true, message: '回调地址不能为空', trigger: 'blur' }]
} }
function getDataList (): void { function getDataList (): void {
@@ -159,8 +133,7 @@ function getDataList (): void {
getPayConfigList({ getPayConfigList({
page: pageIndex.value, page: pageIndex.value,
limit: pageSize.value, limit: pageSize.value,
partnerId: dataForm.partnerId ? Number(dataForm.partnerId) : undefined, status: dataForm.status || undefined
status: dataForm.status
}).then((res) => { }).then((res) => {
if (res && res.code === 0) { if (res && res.code === 0) {
dataList.value = res.data?.page?.list || [] dataList.value = res.data?.page?.list || []
@@ -186,9 +159,69 @@ function currentChangeHandle (val: number): void {
getDataList() getDataList()
} }
// ===================== 新增/修改 =====================
const addOrUpdateVisible = ref<boolean>(false)
const addOrUpdateTitle = ref<string>('')
const addOrUpdateFormRef = ref<FormInstance>()
type ConfigLevel = 'GLOBAL' | 'CHANNEL' | 'AGENT' | 'STORE'
interface AddOrUpdateForm {
id: number | null
configLevel: ConfigLevel
orgId: number | string | null
appId: string
mchId: string
apiV3Key: string
certSerialNo: string
privateKeyPath: string
notifyUrl: string
status: string
remark: string
}
const addOrUpdateForm = reactive<AddOrUpdateForm>({
id: null,
configLevel: 'GLOBAL',
orgId: null,
appId: '',
mchId: '',
apiV3Key: '',
certSerialNo: '',
privateKeyPath: '',
notifyUrl: '',
status: 'ACTIVE',
remark: ''
})
/** 按层级过滤组织树:只显示对应类型的节点 */
const hideTypesByLevel = computed<string[]>(() => {
switch (addOrUpdateForm.configLevel) {
case 'CHANNEL': return ['STORE', 'SERVICE_PROVIDER']
case 'AGENT': return ['STORE', 'CHANNEL']
case 'STORE': return ['CHANNEL', 'SERVICE_PROVIDER']
default: return []
}
})
const addOrUpdateRules: FormRules = {
appId: [{ required: true, message: '应用ID不能为空', trigger: 'blur' }],
mchId: [{ required: true, message: '商户号不能为空', trigger: 'blur' }],
apiV3Key: [{ required: true, message: 'API V3密钥不能为空', trigger: 'blur' }],
notifyUrl: [{ required: true, message: '回调地址不能为空', trigger: 'blur' }],
orgId: [{ required: true, message: '请选择所属组织', trigger: 'change' }]
}
function onLevelChange (): void {
addOrUpdateForm.orgId = null
addOrUpdateFormRef.value?.clearValidate('orgId')
}
function resetForm (): void { function resetForm (): void {
addOrUpdateForm.id = null addOrUpdateForm.id = null
addOrUpdateForm.partnerId = null addOrUpdateForm.configLevel = 'GLOBAL'
addOrUpdateForm.orgId = null
addOrUpdateForm.appId = '' addOrUpdateForm.appId = ''
addOrUpdateForm.mchId = '' addOrUpdateForm.mchId = ''
addOrUpdateForm.apiV3Key = '' addOrUpdateForm.apiV3Key = ''
@@ -201,17 +234,15 @@ function resetForm (): void {
function addHandle (): void { function addHandle (): void {
resetForm() resetForm()
addOrUpdateTitle.value = '新增' addOrUpdateTitle.value = '新增支付配置'
addOrUpdateVisible.value = true addOrUpdateVisible.value = true
addOrUpdateFormRef.value?.clearValidate() addOrUpdateFormRef.value?.clearValidate()
} }
function editHandle (row: PayConfigInfo): void { function editHandle (row: PayConfigInfo): void {
resetForm() resetForm()
addOrUpdateTitle.value = '修改' addOrUpdateTitle.value = '修改支付配置'
addOrUpdateVisible.value = true
addOrUpdateForm.id = row.id addOrUpdateForm.id = row.id
addOrUpdateForm.partnerId = row.partnerId
addOrUpdateForm.appId = row.appId addOrUpdateForm.appId = row.appId
addOrUpdateForm.mchId = row.mchId addOrUpdateForm.mchId = row.mchId
addOrUpdateForm.apiV3Key = row.apiV3Key addOrUpdateForm.apiV3Key = row.apiV3Key
@@ -220,14 +251,29 @@ function editHandle (row: PayConfigInfo): void {
addOrUpdateForm.notifyUrl = row.notifyUrl addOrUpdateForm.notifyUrl = row.notifyUrl
addOrUpdateForm.status = row.status addOrUpdateForm.status = row.status
addOrUpdateForm.remark = row.remark addOrUpdateForm.remark = row.remark
// 根据现有 orgId 判断层级
if (row.storeOrgId) {
addOrUpdateForm.configLevel = 'STORE'
addOrUpdateForm.orgId = row.storeOrgId
} else if (row.agentOrgId) {
addOrUpdateForm.configLevel = 'AGENT'
addOrUpdateForm.orgId = row.agentOrgId
} else if (row.channelOrgId) {
addOrUpdateForm.configLevel = 'CHANNEL'
addOrUpdateForm.orgId = row.channelOrgId
} else {
addOrUpdateForm.configLevel = 'GLOBAL'
addOrUpdateForm.orgId = null
}
addOrUpdateVisible.value = true
} }
function submitForm (): void { function submitForm (): void {
addOrUpdateFormRef.value?.validate((valid) => { addOrUpdateFormRef.value?.validate((valid) => {
if (valid) { if (!valid) return
saveOrUpdatePayConfig({ // 根据层级填对应 orgId,其他留空
const payload: any = {
id: addOrUpdateForm.id || undefined, id: addOrUpdateForm.id || undefined,
partnerId: addOrUpdateForm.partnerId ?? undefined,
appId: addOrUpdateForm.appId, appId: addOrUpdateForm.appId,
mchId: addOrUpdateForm.mchId, mchId: addOrUpdateForm.mchId,
apiV3Key: addOrUpdateForm.apiV3Key, apiV3Key: addOrUpdateForm.apiV3Key,
@@ -236,39 +282,31 @@ function submitForm (): void {
notifyUrl: addOrUpdateForm.notifyUrl, notifyUrl: addOrUpdateForm.notifyUrl,
status: addOrUpdateForm.status, status: addOrUpdateForm.status,
remark: addOrUpdateForm.remark remark: addOrUpdateForm.remark
}).then((res) => {
if (res && res.code === 0) {
ElMessage.success({
message: '操作成功',
duration: 1500,
onClose: () => {
addOrUpdateVisible.value = false
getDataList()
} }
}) if (addOrUpdateForm.configLevel === 'CHANNEL') {
payload.channelOrgId = addOrUpdateForm.orgId
} else if (addOrUpdateForm.configLevel === 'AGENT') {
payload.agentOrgId = addOrUpdateForm.orgId
} else if (addOrUpdateForm.configLevel === 'STORE') {
payload.storeOrgId = addOrUpdateForm.orgId
}
saveOrUpdatePayConfig(payload).then((res) => {
if (res && res.code === 0) {
ElMessage.success({ message: '操作成功', duration: 1500, onClose: () => { addOrUpdateVisible.value = false; getDataList() } })
} else { } else {
ElMessage.error(res.msg) ElMessage.error(res.msg)
} }
}) })
}
}) })
} }
function deleteHandle (id: number): void { function deleteHandle (id: number): void {
ElMessageBox.confirm(`确定对[id=${id}]进行[删除]操作?`, '提示', { ElMessageBox.confirm(`确定对[id=${id}]进行[删除]操作?`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
cancelButtonText: '取消',
type: 'warning'
}).then(() => { }).then(() => {
deletePayConfig([id]).then((res) => { deletePayConfig([id]).then((res) => {
if (res && res.code === 0) { if (res && res.code === 0) {
ElMessage.success({ ElMessage.success({ message: '操作成功', duration: 1500, onClose: () => getDataList() })
message: '操作成功',
duration: 1500,
onClose: () => {
getDataList()
}
})
} else { } else {
ElMessage.error(res.msg) ElMessage.error(res.msg)
} }

View File

@@ -197,7 +197,7 @@ function goTerminalManage (row: ShopRow) {
ElMessage.warning('无法跳转:缺少门店标识') ElMessage.warning('无法跳转:缺少门店标识')
return return
} }
router.push({ name: 'device-device', query: { storeOrgId: String(id) } }).catch(() => {}) router.push({ name: 'manage-device', query: { storeOrgId: String(id) } }).catch(() => {})
} }
function onMoreMenuCommand (cmd: string, row: ShopRow) { function onMoreMenuCommand (cmd: string, row: ShopRow) {

View File

@@ -455,12 +455,14 @@ interface StoreSearchResponse {
} }
} }
function pickStoreSearchPage(data: StoreSearchResponse | undefined): { list: ShopRow[]; total: number } { function pickStoreSearchPage(data: any): { list: ShopRow[]; total: number } {
if (!data) return { list: [], total: 0 } if (!data) return { list: [], total: 0 }
const block = data.data ?? data.page ?? {} // 兼容:data.data.page.list(manage统一)/ data.data.list(PageResult)/ data.page.list / data.list
const list = Array.isArray(block.list) ? block.list : [] const inner = data.data ?? data.page ?? data
const total = block.totalCount ?? 0 const block = (inner && typeof inner === 'object' && !Array.isArray(inner)) ? (inner.page ?? inner) : inner
return { list, total } const list = Array.isArray(block?.list) ? block.list : (Array.isArray(inner) ? inner : [])
const total = block?.totalCount ?? inner?.totalCount ?? 0
return { list, total: Number(total) || 0 }
} }
function getDataList() { function getDataList() {