Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
dbe543ae9a
91 files changed
+13649
-2267
No files matched your search
@@ -1,10 +1,28 @@
|
||||
import request from "/@/utils/request"
|
||||
|
||||
export function print(query?: Object) {
|
||||
export function print(data?: Object) {
|
||||
return request({
|
||||
url: "/hard/hard-invoice/print",
|
||||
method: "get",
|
||||
params: query,
|
||||
method: "post",
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 失败任务按原打印序号重试。 */
|
||||
export function retry(data?: Object) {
|
||||
return request({
|
||||
url: "/hard/hard-invoice/retry",
|
||||
method: "post",
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 已打印兑换券创建下一打印序号并记录人工原因。 */
|
||||
export function reprint(data?: Object) {
|
||||
return request({
|
||||
url: "/hard/hard-invoice/reprint",
|
||||
method: "post",
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,111 @@
|
||||
import request from "/@/utils/request"
|
||||
|
||||
/** 订单详情中的单张识别证据,字段来自 HardOrderEvidenceImageVo。 */
|
||||
export interface HardOrderEvidenceImage {
|
||||
/** 服务端证据主键。 */
|
||||
id: string | number
|
||||
/** 证据类型稳定编码。 */
|
||||
evidenceType: "FACE_RECOGNITION" | "MENU_RECOGNITION" | string
|
||||
/** 图片角色稳定编码。 */
|
||||
evidenceRole: "FACE_REFERENCE" | "FACE_LIVE" | "MENU_LIVE" | string
|
||||
/** 图片角色中文名称。 */
|
||||
evidenceRoleName?: string
|
||||
/** 同一角色下从零开始的图片序号。 */
|
||||
sequenceNo: number
|
||||
/** 终端采集时间。 */
|
||||
capturedAt?: string
|
||||
/** 人脸识别相似度快照。 */
|
||||
similarity?: number | string
|
||||
/** 产生证据的设备主键。 */
|
||||
hardId?: string | number
|
||||
/** 证据文件访问地址。 */
|
||||
imageUrl?: string
|
||||
/** 证据状态稳定编码。 */
|
||||
status: "PENDING" | "AVAILABLE" | "UNAVAILABLE" | string
|
||||
/** 永久缺失或复制失败原因。 */
|
||||
failureReason?: string
|
||||
}
|
||||
|
||||
/** 外卖订单的期望送达配置;非外卖或历史订单未记录时为空。 */
|
||||
export interface HardTakeOutAppointment {
|
||||
/** 送达方式稳定编码:IMMEDIATE 表示立即送,SCHEDULED 表示预约时段。 */
|
||||
expectDeliveryType?: "IMMEDIATE" | "SCHEDULED" | string
|
||||
/** 预约送达日期,仅预约时段模式使用。 */
|
||||
expectDeliveryDate?: string
|
||||
/** 预约时段开始时间。 */
|
||||
expectDeliveryStart?: string
|
||||
/** 预约时段结束时间。 */
|
||||
expectDeliveryEnd?: string
|
||||
/** 立即送模式下计算出的预计送达时间点。 */
|
||||
expectArriveTime?: string
|
||||
}
|
||||
|
||||
/** 当前页面使用的订单详情扩展字段。 */
|
||||
export interface HardOrderDetail {
|
||||
/** 订单主键。 */
|
||||
id: string | number
|
||||
/** 设备名称。 */
|
||||
hardName?: string
|
||||
/** 订单详情才返回的长期识别证据。 */
|
||||
evidenceImages?: HardOrderEvidenceImage[]
|
||||
/** 外卖订单详情关联的期望送达配置。 */
|
||||
takeOutAppt?: HardTakeOutAppointment
|
||||
/** 兼容订单详情其余历史字段。 */
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** 订单列表统计查询条件;字段与订单分页筛选保持一致。 */
|
||||
export type HardOrderStatisticsQuery = Record<string, any>
|
||||
|
||||
/** 订单按餐段汇总的统计,金额单位均为分。 */
|
||||
export interface HardOrderMealStatistics {
|
||||
/** 餐段字典编码;历史订单未记录时为空。 */
|
||||
hourType?: string | null
|
||||
/** 订单数。 */
|
||||
orderCount?: number
|
||||
/** 购物车商品份数。 */
|
||||
productCount?: number
|
||||
/** 商品总价。 */
|
||||
totalPrice?: number
|
||||
/** 额外支付。 */
|
||||
extraPrice?: number
|
||||
/** 配送费。 */
|
||||
deliveryPrice?: number
|
||||
/** 实际支付。 */
|
||||
payPrice?: number
|
||||
/** 已退款金额。 */
|
||||
refundPrice?: number
|
||||
/** 订单实际金额,即实际支付减已退款。 */
|
||||
actualPrice?: number
|
||||
}
|
||||
|
||||
/** 订单统计通用汇总,金额单位均为分。 */
|
||||
export interface HardOrderStatisticsSummary {
|
||||
orderCount?: number
|
||||
productCount?: number
|
||||
totalPrice?: number
|
||||
extraPrice?: number
|
||||
deliveryPrice?: number
|
||||
payPrice?: number
|
||||
refundPrice?: number
|
||||
actualPrice?: number
|
||||
meals?: HardOrderMealStatistics[]
|
||||
}
|
||||
|
||||
/** 订单按预计使用日期拆分的统计。 */
|
||||
export interface HardOrderDailyStatistics extends HardOrderStatisticsSummary {
|
||||
date?: string
|
||||
}
|
||||
|
||||
/** 订单统计接口统一响应。 */
|
||||
export interface HardOrderStatisticsResponse {
|
||||
data: {
|
||||
total?: HardOrderStatisticsSummary
|
||||
daily?: HardOrderDailyStatistics[]
|
||||
}
|
||||
msg?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 离线订单手动支付
|
||||
*/
|
||||
@@ -11,6 +117,34 @@ export function offlinePay(data: any) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 智能柜手动结算提交的商品项;productId 为 hard_spu 主键。 */
|
||||
export interface HardDoorManualSettleItem {
|
||||
productId: string | number
|
||||
productCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能柜订单手动结算(第三方识别不可用时,人工核对视频后结算)
|
||||
*/
|
||||
export function manualSettle(data: { orderId: string | number; items: HardDoorManualSettleItem[]; reason?: string }) {
|
||||
return request({
|
||||
url: "/hard/hard-order/manual-settle",
|
||||
method: "post",
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能柜订单手动取消(用户未拿商品)
|
||||
*/
|
||||
export function manualCancel(data: { orderId: string | number; reason?: string }) {
|
||||
return request({
|
||||
url: "/hard/hard-order/manual-cancel",
|
||||
method: "post",
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function pageJoin(query: any) {
|
||||
return request({
|
||||
url: "/hard/hard-order/page-join",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import request from "/@/utils/request"
|
||||
|
||||
/** 后端 Long/雪花主键在浏览器中必须以字符串传递,避免 Number 精度丢失。 */
|
||||
export type HardwareId = string
|
||||
|
||||
/** 业务终端与无屏 USB 打印机资产的绑定详情。 */
|
||||
export interface HardPrinterBinding {
|
||||
id?: HardwareId
|
||||
sourceHardId: HardwareId
|
||||
printerHardId: HardwareId
|
||||
sourceHardName?: string
|
||||
sourceHardType?: string
|
||||
printerHardName?: string
|
||||
printerConfig?: HardPrinterConfig
|
||||
}
|
||||
|
||||
/** 尔印 USB 打印机配置。VID/PID 尚未由业务终端回填时允许为空。 */
|
||||
export interface HardPrinterConfig {
|
||||
id?: HardwareId
|
||||
printerHardId: HardwareId
|
||||
printerModel?: string
|
||||
usbVendorId?: number
|
||||
usbProductId?: number
|
||||
paperWidth?: number
|
||||
autoCut?: string
|
||||
imageMode?: string
|
||||
}
|
||||
|
||||
/** 查询来源业务终端当前绑定的打印机资产。 */
|
||||
export function getBinding(hardId: HardwareId) {
|
||||
return request({ url: `/hard/hard-printer/binding/${hardId}`, method: "get" })
|
||||
}
|
||||
|
||||
/** 查询同商户同部门可绑定的空闲打印机资产。 */
|
||||
export function listAvailable(sourceHardId: HardwareId) {
|
||||
return request({ url: "/hard/hard-printer/available", method: "get", params: { sourceHardId } })
|
||||
}
|
||||
|
||||
/** 查询指定商户下已完成打印机绑定的业务终端。 */
|
||||
export function listBoundSources(merchantId: HardwareId) {
|
||||
return request({ url: "/hard/hard-printer/sources", method: "get", params: { merchantId } })
|
||||
}
|
||||
|
||||
/** 建立来源业务终端与 EY_PRINT 的一对一绑定。 */
|
||||
export function bind(data: { sourceHardId: HardwareId; printerHardId: HardwareId }) {
|
||||
return request({ url: "/hard/hard-printer/binding", method: "post", data })
|
||||
}
|
||||
|
||||
/** 解除来源业务终端绑定。 */
|
||||
export function unbind(sourceHardId: HardwareId) {
|
||||
return request({ url: `/hard/hard-printer/binding/${sourceHardId}`, method: "delete" })
|
||||
}
|
||||
|
||||
/** 查询无屏 USB 打印机资产配置。 */
|
||||
export function getConfig(printerHardId: HardwareId) {
|
||||
return request({ url: `/hard/hard-printer/config/${printerHardId}`, method: "get" })
|
||||
}
|
||||
|
||||
/** 保存无屏 USB 打印机资产配置。 */
|
||||
export function saveConfig(data: HardPrinterConfig) {
|
||||
return request({ url: "/hard/hard-printer/config", method: "put", data })
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import request from "/@/utils/request"
|
||||
|
||||
export interface TerminalLogQuery {
|
||||
current?: number
|
||||
size?: number
|
||||
hardId?: string | number
|
||||
hardName?: string
|
||||
uniqueCode?: string
|
||||
merchantId?: string | number
|
||||
deptId?: string | number
|
||||
logDateStart?: string
|
||||
logDateEnd?: string
|
||||
uploadTimeStart?: string
|
||||
uploadTimeEnd?: string
|
||||
}
|
||||
|
||||
export interface TerminalLogDevice {
|
||||
hardId: string | number
|
||||
hardName: string
|
||||
uniqueCode: string
|
||||
hardType?: string
|
||||
merchantId?: string | number
|
||||
deptId?: string | number
|
||||
fileCount: number
|
||||
latestUploadTime?: string
|
||||
}
|
||||
|
||||
export interface TerminalLogTreeNode {
|
||||
key: string
|
||||
label: string
|
||||
relativePath: string
|
||||
directory: boolean
|
||||
fileId?: string | number
|
||||
fileSize?: number
|
||||
logDate?: string
|
||||
uploadTime?: string
|
||||
children?: TerminalLogTreeNode[]
|
||||
}
|
||||
|
||||
export interface TerminalLogPreview {
|
||||
id: string | number
|
||||
originalName: string
|
||||
relativePath: string
|
||||
content: string
|
||||
lineCount: number
|
||||
fileSize: number
|
||||
truncated: boolean
|
||||
}
|
||||
|
||||
export function fetchList(query?: Object) {
|
||||
return request({
|
||||
url: "/hard/hardTerminalLogFile/page",
|
||||
method: "get",
|
||||
params: query,
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchDevicePage(query?: TerminalLogQuery) {
|
||||
return request({
|
||||
url: "/hard/hardTerminalLogFile/device-page",
|
||||
method: "get",
|
||||
params: query,
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchFileTree(query: TerminalLogQuery) {
|
||||
return request({
|
||||
url: "/hard/hardTerminalLogFile/file-tree",
|
||||
method: "get",
|
||||
params: query,
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchPreview(id: string | number) {
|
||||
return request({
|
||||
url: `/hard/hardTerminalLogFile/${id}/preview`,
|
||||
method: "get",
|
||||
})
|
||||
}
|
||||
|
||||
export function pullTerminalLog(hardId: string | number) {
|
||||
return request({
|
||||
url: `/hard/hardTerminalLogFile/pull/${hardId}`,
|
||||
method: "post",
|
||||
})
|
||||
}
|
||||
|
||||
export function downloadUrl(id: string | number) {
|
||||
return `/hard/hardTerminalLogFile/${id}/download`
|
||||
}
|
||||
|
||||
export function downloadFile(id: string | number) {
|
||||
return request({
|
||||
url: downloadUrl(id),
|
||||
method: "get",
|
||||
responseType: "blob",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import request from "/@/utils/request"
|
||||
|
||||
export function fetchList(query?: Object) {
|
||||
return request({
|
||||
url: "/hard/xfj-log/page",
|
||||
method: "get",
|
||||
params: query,
|
||||
})
|
||||
}
|
||||
|
||||
export function addObj(obj?: Object) {
|
||||
return request({
|
||||
url: "/hard/xfj-log",
|
||||
method: "post",
|
||||
data: obj,
|
||||
})
|
||||
}
|
||||
|
||||
export function getObj(id?: string) {
|
||||
return request({
|
||||
url: "/hard/xfj-log/" + id,
|
||||
method: "get",
|
||||
})
|
||||
}
|
||||
|
||||
export function delObjs(ids?: Object) {
|
||||
return request({
|
||||
url: "/hard/xfj-log",
|
||||
method: "delete",
|
||||
data: ids,
|
||||
})
|
||||
}
|
||||
|
||||
export function putObj(obj?: Object) {
|
||||
return request({
|
||||
url: "/hard/xfj-log",
|
||||
method: "put",
|
||||
data: obj,
|
||||
})
|
||||
}
|
||||
@@ -9,6 +9,14 @@ export function refreshQrcode(query?: Object) {
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新全部商户二维码
|
||||
export function refreshAllQrcode() {
|
||||
return request({
|
||||
url: "/wallet/mch-manage/refresh-all-qrcode",
|
||||
method: "get",
|
||||
})
|
||||
}
|
||||
|
||||
export function select(query?: Object) {
|
||||
return request({
|
||||
url: "/wallet/mch-manage/select",
|
||||
|
||||
@@ -1,5 +1,54 @@
|
||||
import request from "/@/utils/request"
|
||||
|
||||
/**
|
||||
* 流水备注的前后端统一字符上限,与 wallet_transaction.remark 字段长度保持一致。
|
||||
*/
|
||||
export const WALLET_TRANSACTION_REMARK_MAX_LENGTH = 256
|
||||
|
||||
/**
|
||||
* 钱包流水备注修改参数,只允许提交流水编号和备注。
|
||||
*/
|
||||
export interface WalletTransactionRemarkParams {
|
||||
/** 需要修改的钱包流水编号。 */
|
||||
id: string | number
|
||||
/** 新备注;空字符串表示清除原备注。 */
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 钱包流水信息修改参数,只允许提交流水编号、触发部门和创建时间。
|
||||
*/
|
||||
export interface WalletTransactionUpdateInfoParams {
|
||||
/** 需要修改的钱包流水编号。 */
|
||||
id: string | number
|
||||
/** 流水触发事件归属的新部门编号。 */
|
||||
deptId: string | number
|
||||
/** 流水新的创建时间,格式为 yyyy-MM-dd HH:mm:ss;用于更正流水归属统计周期。 */
|
||||
createTime: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改单笔钱包流水备注
|
||||
*/
|
||||
export function updateRemark(data: WalletTransactionRemarkParams) {
|
||||
return request({
|
||||
url: "/wallet/wallet-transaction/update-remark",
|
||||
method: "put",
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改单笔钱包流水的归属部门和创建时间
|
||||
*/
|
||||
export function updateInfo(data: WalletTransactionUpdateInfoParams) {
|
||||
return request({
|
||||
url: "/wallet/wallet-transaction/update-info",
|
||||
method: "put",
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 各自食堂退还记录
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,592 @@
|
||||
<template>
|
||||
<Splitpanes class="draggable-dep-mch-tree" @resize="state.treeWidth = $event[0].size">
|
||||
<pane :size="state.treeWidth" class="draggable-dep-mch-tree__tree-pane">
|
||||
<!-- 树 -->
|
||||
<div v-loading="activeState.loading" class="dep-mch-tree-panel">
|
||||
<!-- tab区域:用户部门/商户部门,标签右上角显示当前勾选数量 -->
|
||||
<div class="dep-mch-tree-panel__tabs">
|
||||
<el-tabs v-model="state.activeTab" @tab-change="handleTabChange">
|
||||
<el-tab-pane name="DEPT">
|
||||
<template #label>
|
||||
<span class="dep-mch-tree-panel__tab-label">
|
||||
{{ deptTabLabel }}
|
||||
<el-badge :value="state.dept.checkedTreeNodes.length"
|
||||
:hidden="state.dept.checkedTreeNodes.length === 0" />
|
||||
</span>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="MCH_DEPT">
|
||||
<template #label>
|
||||
<span class="dep-mch-tree-panel__tab-label">
|
||||
{{ mchTabLabel }}
|
||||
<el-badge :value="state.mchDept.checkedTreeNodes.length"
|
||||
:hidden="state.mchDept.checkedTreeNodes.length === 0" />
|
||||
</span>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 头部区域 -->
|
||||
<div class="dep-mch-tree-panel__header">
|
||||
<el-input v-model="activeState.searchName" class="dep-mch-tree-panel__search" suffix-icon="search"
|
||||
:placeholder="activePlaceholder" :aria-label="activePlaceholder || '搜索树节点'" size="small"
|
||||
clearable @change="handleSearchInputChange" />
|
||||
<div v-if="showExpand" class="dep-mch-tree-panel__operations">
|
||||
<el-dropdown :hide-on-click="false">
|
||||
<button type="button" class="dep-mch-tree-panel__more" title="树操作" aria-label="树操作">
|
||||
<el-icon>
|
||||
<MoreFilled />
|
||||
</el-icon>
|
||||
</button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<el-button class="!h-5 !text-gray-500 dark:!text-white dark:hover:!text-primary"
|
||||
link :icon="activeState.isExpand ? 'expand' : 'fold'"
|
||||
@click="toggleRowExpansionAll(activeState.isExpand ? false : true)">
|
||||
{{ activeState.isExpand ? "折叠" : "展开" }}
|
||||
</el-button>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 树形结构:用户部门树 -->
|
||||
<div v-show="state.activeTab === 'DEPT'" class="dep-mch-tree-panel__body">
|
||||
<el-tree ref="deptTreeRef" class="dep-mch-tree-panel__tree" :props="props.props"
|
||||
:data="state.dept.treeDataList" :expand-on-click-node="false" node-key="id"
|
||||
:highlight-current="nodeClickable ? true : false" default-expand-all show-checkbox
|
||||
@node-click="(data: any) => handleNodeClick(data, 'DEPT')"
|
||||
@check="(data: any, checkStatus: any) => handleCheck(data, checkStatus, 'DEPT')">
|
||||
<template #default="{ node, data }">
|
||||
<el-tooltip v-if="data.isLock" effect="dark" :content="$t('sysuser.noDataScopeTip')"
|
||||
placement="right">
|
||||
<span class="dep-mch-tree-panel__locked-node">
|
||||
{{ node.label }}
|
||||
<SvgIcon name="ele-Lock" />
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
|
||||
<!-- 树形结构:商户部门树 -->
|
||||
<div v-show="state.activeTab === 'MCH_DEPT'" class="dep-mch-tree-panel__body">
|
||||
<el-tree ref="mchDeptTreeRef" class="dep-mch-tree-panel__tree" :props="props.props"
|
||||
:data="state.mchDept.treeDataList" :expand-on-click-node="false" node-key="id"
|
||||
:highlight-current="nodeClickable ? true : false" default-expand-all show-checkbox
|
||||
@node-click="(data: any) => handleNodeClick(data, 'MCH_DEPT')"
|
||||
@check="(data: any, checkStatus: any) => handleCheck(data, checkStatus, 'MCH_DEPT')">
|
||||
<template #default="{ node, data }">
|
||||
<el-tooltip v-if="data.isLock" effect="dark" :content="$t('sysuser.noDataScopeTip')"
|
||||
placement="right">
|
||||
<span class="dep-mch-tree-panel__locked-node">
|
||||
{{ node.label }}
|
||||
<SvgIcon name="ele-Lock" />
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
|
||||
<!-- 底部区域:当前tab的包含/不包含开关(包含=绿色,不包含=红色) -->
|
||||
<div class="dep-mch-tree-panel__footer">
|
||||
<el-switch v-model="activeState.switch" size="small" :inactive-text="switchInactiveText"
|
||||
:active-text="switchActiveText" :inactive-color="switchInactiveColor"
|
||||
:active-color="switchActiveColor" @change="handleSwitchChange"
|
||||
:disabled="nodeClickable && activeState.checkedTreeNodes.length > 0" />
|
||||
</div>
|
||||
</div>
|
||||
</pane>
|
||||
|
||||
<pane :size="100 - state.treeWidth" class="draggable-dep-mch-tree__content-pane">
|
||||
<!-- 折叠按钮 -->
|
||||
<button type="button" class="draggable-dep-mch-tree__fold"
|
||||
:title="treeVisible ? `收起${activeTreeName}` : `展开${activeTreeName}`"
|
||||
:aria-label="treeVisible ? `收起${activeTreeName}` : `展开${activeTreeName}`" @click="handleClickTreeFold()">
|
||||
<el-icon>
|
||||
<ArrowLeftBold v-if="treeVisible" />
|
||||
<ArrowRightBold v-else />
|
||||
</el-icon>
|
||||
</button>
|
||||
|
||||
<div class="draggable-dep-mch-tree__content">
|
||||
<slot name="default" />
|
||||
</div>
|
||||
</pane>
|
||||
</Splitpanes>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="DraggableDepMchTree">
|
||||
import { computed } from "vue"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { deptTree } from "/@/api/admin/dept"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
placeholder?: string // 用户部门输入框占位符
|
||||
mchPlaceholder?: string // 商户部门输入框占位符
|
||||
deptTabLabel?: string // 用户部门tab文案
|
||||
mchTabLabel?: string // 商户部门tab文案
|
||||
showExpand?: boolean // 折叠控制
|
||||
query?: (searchName?: string) => Promise<any> // 部门树查询函数(两个tab共用)
|
||||
props?: object
|
||||
nodeClickable?: boolean
|
||||
switchInactiveText?: string | undefined
|
||||
switchActiveText?: string | undefined
|
||||
switchInactiveColor?: string // 包含状态开关颜色
|
||||
switchActiveColor?: string // 不包含状态开关颜色
|
||||
defaultTreeWidth?: number | string
|
||||
defaultTab?: string // 默认tab:DEPT | MCH_DEPT
|
||||
}>(),
|
||||
{
|
||||
placeholder: "",
|
||||
mchPlaceholder: "",
|
||||
deptTabLabel: "用户部门",
|
||||
mchTabLabel: "商户部门",
|
||||
showExpand: true,
|
||||
query: (name?: string) => deptTree({ deptName: name || "" }),
|
||||
props: () => {
|
||||
return {
|
||||
label: "name",
|
||||
children: "children",
|
||||
value: "id",
|
||||
disabled: "isLock",
|
||||
}
|
||||
},
|
||||
nodeClickable: true,
|
||||
switchInactiveText: "包含",
|
||||
switchActiveText: "不包含",
|
||||
switchInactiveColor: "var(--el-color-success)",
|
||||
switchActiveColor: "var(--el-color-danger)",
|
||||
defaultTreeWidth: 20,
|
||||
defaultTab: "DEPT",
|
||||
},
|
||||
)
|
||||
const emit = defineEmits(["getSelectNode"])
|
||||
|
||||
const deptTreeRef = ref() // 用户部门树组件ref
|
||||
const mchDeptTreeRef = ref() // 商户部门树组件ref
|
||||
|
||||
const createTabState = () => ({
|
||||
searchName: "", // 查询关键字
|
||||
isExpand: true, // 是否展开树所有节点
|
||||
treeDataList: [] as any[],
|
||||
curClickedNode: {} as any,
|
||||
checkedTreeNodes: [] as any[], // 被勾选的树节点
|
||||
loading: false,
|
||||
switch: false, // switch开关(false=包含 true=不包含)
|
||||
inited: false, // 是否已加载过树数据
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
activeTab: props.defaultTab, // 当前tab:DEPT | MCH_DEPT
|
||||
treeWidth: Number(props.defaultTreeWidth), // 树组件宽度
|
||||
treeWidthMemo: Number(props.defaultTreeWidth), // 树组件宽度记录
|
||||
dept: createTabState(), // 用户部门tab状态
|
||||
mchDept: createTabState(), // 商户部门tab状态
|
||||
})
|
||||
|
||||
// 当前tab状态
|
||||
const activeState = computed(() => (state.activeTab === "MCH_DEPT" ? state.mchDept : state.dept))
|
||||
// 当前tab的树ref
|
||||
const activeTreeRef = computed(() => (state.activeTab === "MCH_DEPT" ? mchDeptTreeRef : deptTreeRef))
|
||||
// 当前tab输入框占位符
|
||||
const activePlaceholder = computed(() =>
|
||||
state.activeTab === "MCH_DEPT" ? props.mchPlaceholder || "请输入商户部门名称" : props.placeholder || "请输入用户部门名称",
|
||||
)
|
||||
// 当前tab树名称
|
||||
const activeTreeName = computed(() => (state.activeTab === "MCH_DEPT" ? "商户部门树" : "用户部门树"))
|
||||
|
||||
// 是否折叠树组件
|
||||
const treeVisible = computed(() => state.treeWidth > 0)
|
||||
|
||||
// 获取指定tab的部门树数据(两个tab共用同一棵部门树查询)
|
||||
const getTabTreeData = (tab: "DEPT" | "MCH_DEPT") => {
|
||||
const tabState = tab === "MCH_DEPT" ? state.mchDept : state.dept
|
||||
if (props.query instanceof Function) {
|
||||
tabState.loading = true
|
||||
tabState.inited = true
|
||||
const result = props.query(tabState.searchName)
|
||||
// 如果查询结果为 Promise 类型,则进行后续处理
|
||||
if ((typeof result === "object" || typeof result === "function") && typeof result.then === "function") {
|
||||
result
|
||||
.then((r: any) => {
|
||||
tabState.treeDataList = r.data
|
||||
})
|
||||
.catch((err) => {
|
||||
useMessage().error(err.msg)
|
||||
})
|
||||
.finally(() => (tabState.loading = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前tab树数据
|
||||
const getTreeData = () => getTabTreeData(state.activeTab as "DEPT" | "MCH_DEPT")
|
||||
|
||||
// tab切换
|
||||
const handleTabChange = (tab: any) => {
|
||||
state.activeTab = tab
|
||||
// 懒加载:首次切换到该tab时才请求树数据
|
||||
if (!activeState.value.inited) getTreeData()
|
||||
}
|
||||
|
||||
// 点击折叠树组件
|
||||
const handleClickTreeFold = () => {
|
||||
if (treeVisible.value) {
|
||||
state.treeWidthMemo = state.treeWidth
|
||||
state.treeWidth = 0
|
||||
} else {
|
||||
state.treeWidth = state.treeWidthMemo
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框改变
|
||||
const handleSearchInputChange = () => {
|
||||
activeTreeRef.value!.setCheckedKeys([], false)
|
||||
activeState.value.checkedTreeNodes = []
|
||||
activeState.value.curClickedNode = {}
|
||||
getTreeData()
|
||||
}
|
||||
|
||||
// 点击树形结构节点
|
||||
const handleNodeClick = (nodeData: any, tab: "DEPT" | "MCH_DEPT") => {
|
||||
if (!props.nodeClickable) return
|
||||
const tabState = tab === "MCH_DEPT" ? state.mchDept : state.dept
|
||||
tabState.curClickedNode = nodeData
|
||||
emit("getSelectNode", tabState.curClickedNode, tabState.checkedTreeNodes, tabState.switch, tab)
|
||||
}
|
||||
|
||||
// 勾选树节点时
|
||||
const handleCheck = (_data: any, checkStatus: any, tab: "DEPT" | "MCH_DEPT") => {
|
||||
const tabState = tab === "MCH_DEPT" ? state.mchDept : state.dept
|
||||
const { checkedNodes } = checkStatus
|
||||
tabState.checkedTreeNodes = checkedNodes
|
||||
if (props.nodeClickable && tabState.checkedTreeNodes.length > 0) {
|
||||
tabState.switch = false
|
||||
}
|
||||
emit("getSelectNode", tabState.curClickedNode, tabState.checkedTreeNodes, tabState.switch, tab)
|
||||
}
|
||||
|
||||
// switch改变时
|
||||
const handleSwitchChange = () =>
|
||||
emit("getSelectNode", activeState.value.curClickedNode, activeState.value.checkedTreeNodes, activeState.value.switch, state.activeTab)
|
||||
|
||||
// 切换所有节点的展开/收起状态。status:true 为展开,false 为收起
|
||||
const toggleRowExpansionAll = (status: boolean) => {
|
||||
activeState.value.isExpand = status
|
||||
const nodes = activeTreeRef.value!.store._getAllNodes()
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
nodes[i].expanded = status
|
||||
}
|
||||
}
|
||||
|
||||
// 重置数据(重置用户部门+商户部门两个tab)
|
||||
const resetData = () => {
|
||||
state.dept = createTabState()
|
||||
state.mchDept = createTabState()
|
||||
// 保持当前tab,重新加载当前tab数据
|
||||
getTreeData()
|
||||
}
|
||||
onMounted(() => {
|
||||
getTreeData()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
getTreeData,
|
||||
resetData,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.draggable-dep-mch-tree {
|
||||
min-width: 0;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.draggable-dep-mch-tree__tree-pane,
|
||||
.draggable-dep-mch-tree__content-pane {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
padding: 8px 6px 6px 8px;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__tabs {
|
||||
flex: 0 0 auto;
|
||||
padding-bottom: 2px;
|
||||
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__tabs .el-tabs {
|
||||
--el-tabs-header-height: 28px;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__tabs .el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__tabs .el-tabs__nav-wrap::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__tabs .el-tabs__item {
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
font-size: var(--el-font-size-small);
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__tab-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__tab-label .el-badge__content {
|
||||
top: -6px;
|
||||
right: -12px;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__header {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__operations {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
outline: none;
|
||||
transition: color 180ms ease, background-color 180ms ease;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__more .el-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__more:hover {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__more:focus-visible {
|
||||
color: var(--el-color-primary);
|
||||
outline: 2px solid var(--el-color-primary-light-5);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 4px 2px 4px 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__tree {
|
||||
--el-tree-node-content-height: 26px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
font-size: var(--el-font-size-extra-small);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__tree .el-tree-node {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__tree .el-tree-node__content {
|
||||
min-width: 0;
|
||||
padding-right: 4px;
|
||||
overflow: hidden;
|
||||
border-radius: var(--el-border-radius-small);
|
||||
transition: color 180ms ease, background-color 180ms ease;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__tree .el-tree-node__content>.el-tree-node__expand-icon {
|
||||
padding: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__tree .el-tree-node__content>label.el-checkbox {
|
||||
--el-checkbox-input-height: 12px;
|
||||
--el-checkbox-input-width: 12px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__tree .el-tree-node__label,
|
||||
.dep-mch-tree-panel__locked-node {
|
||||
display: inline-flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__locked-node .svg-icon {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__footer {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 30px;
|
||||
padding-top: 5px;
|
||||
overflow: hidden;
|
||||
background: var(--el-bg-color);
|
||||
border-top: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
|
||||
.dep-mch-tree-panel__footer .el-switch {
|
||||
font-size: var(--el-font-size-extra-small);
|
||||
}
|
||||
|
||||
.draggable-dep-mch-tree__content-pane {
|
||||
position: relative;
|
||||
/* 分隔线与折叠按钮使用同一定位容器,避免跨 pane 定位后被裁剪或覆盖。 */
|
||||
}
|
||||
|
||||
.draggable-dep-mch-tree__content-pane::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 1px;
|
||||
content: "";
|
||||
background: var(--el-border-color-lighter);
|
||||
transition: background-color 180ms ease, width 180ms ease;
|
||||
}
|
||||
|
||||
.draggable-dep-mch-tree__content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px 8px 8px 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.draggable-dep-mch-tree__fold {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 20px;
|
||||
height: 56px;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
outline: none;
|
||||
transform: translateY(-50%);
|
||||
transition: color 180ms ease;
|
||||
}
|
||||
|
||||
.draggable-dep-mch-tree__fold::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 12px;
|
||||
content: "";
|
||||
background: var(--el-fill-color-dark);
|
||||
border-radius: 0 var(--el-border-radius-round) var(--el-border-radius-round) 0;
|
||||
transition: background-color 180ms ease;
|
||||
}
|
||||
|
||||
.draggable-dep-mch-tree__fold .el-icon {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 12px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.draggable-dep-mch-tree__fold:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.draggable-dep-mch-tree__fold:hover::before,
|
||||
.draggable-dep-mch-tree__fold:focus-visible::before {
|
||||
background: var(--el-color-primary-light-8);
|
||||
}
|
||||
|
||||
.draggable-dep-mch-tree__fold:focus-visible {
|
||||
color: var(--el-color-primary);
|
||||
outline: 2px solid var(--el-color-primary-light-5);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.draggable-dep-mch-tree.splitpanes--vertical>.splitpanes__splitter {
|
||||
position: relative;
|
||||
flex: 0 0 6px;
|
||||
width: 6px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.draggable-dep-mch-tree.splitpanes--vertical>.splitpanes__splitter:hover+.draggable-dep-mch-tree__content-pane::before,
|
||||
.draggable-dep-mch-tree.splitpanes--vertical>.splitpanes__splitter:active+.draggable-dep-mch-tree__content-pane::before {
|
||||
width: 2px;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
.dep-mch-tree-panel__more,
|
||||
.dep-mch-tree-panel__tree .el-tree-node__content,
|
||||
.draggable-dep-mch-tree__fold,
|
||||
.draggable-dep-mch-tree__fold::before,
|
||||
.draggable-dep-mch-tree__content-pane::before {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.draggable-dep-mch-tree.splitpanes--vertical>.splitpanes__pane {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,25 +1,24 @@
|
||||
<template>
|
||||
<Splitpanes @resize="state.treeWidth = $event[0].size">
|
||||
<pane :size="state.treeWidth">
|
||||
<Splitpanes class="draggable-dep-tree" @resize="state.treeWidth = $event[0].size">
|
||||
<pane :size="state.treeWidth" class="draggable-dep-tree__tree-pane">
|
||||
<!-- 树 -->
|
||||
<div v-loading="state.loading" class="h-full flex flex-col m-1">
|
||||
<div v-loading="state.loading" class="dep-tree-panel">
|
||||
<!-- 头部区域 -->
|
||||
<div class="flex items-center pb-2 shadow-gray-400 z-10"
|
||||
style="box-shadow: 0 8px 8px -8px rgba(0, 0, 0, 0.1)">
|
||||
<div class="w-4/5">
|
||||
<el-input v-model="state.searchName" suffix-icon="search" :placeholder="placeholder" clearable
|
||||
@change="handleSearchInputChange" />
|
||||
</div>
|
||||
<div class="flex-1 ml-[5%] leading-none" v-if="showExpand">
|
||||
<div class="dep-tree-panel__header">
|
||||
<el-input v-model="state.searchName" class="dep-tree-panel__search" suffix-icon="search"
|
||||
:placeholder="placeholder" :aria-label="placeholder || '搜索树节点'" size="small" clearable
|
||||
@change="handleSearchInputChange" />
|
||||
<div v-if="showExpand" class="dep-tree-panel__operations">
|
||||
<el-dropdown :hide-on-click="false">
|
||||
<el-icon style="transform: rotate(90deg)">
|
||||
<MoreFilled />
|
||||
</el-icon>
|
||||
<button type="button" class="dep-tree-panel__more" title="树操作" aria-label="树操作">
|
||||
<el-icon>
|
||||
<MoreFilled />
|
||||
</el-icon>
|
||||
</button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<el-button
|
||||
class="!h-[20px] !text-gray-500 dark:!text-white dark:hover:!text-primary"
|
||||
<el-button class="!h-5 !text-gray-500 dark:!text-white dark:hover:!text-primary"
|
||||
link :icon="state.isExpand ? 'expand' : 'fold'"
|
||||
@click="toggleRowExpansionAll(state.isExpand ? false : true)">
|
||||
{{ state.isExpand ? "折叠" : "展开" }}
|
||||
@@ -32,14 +31,14 @@
|
||||
</div>
|
||||
|
||||
<!-- 树形结构 -->
|
||||
<div class="flex-1 overflow-hidden hover:overflow-auto">
|
||||
<el-tree ref="treeRef" :props="props.props" :data="state.treeDataList" :expand-on-click-node="false"
|
||||
node-key="id" :highlight-current="nodeClickable ? true : false" default-expand-all show-checkbox
|
||||
@node-click="handleNodeClick" @check="handleCheck">
|
||||
<div class="dep-tree-panel__body">
|
||||
<el-tree ref="treeRef" class="dep-tree-panel__tree" :props="props.props" :data="state.treeDataList"
|
||||
:expand-on-click-node="false" node-key="id" :highlight-current="nodeClickable ? true : false"
|
||||
default-expand-all show-checkbox @node-click="handleNodeClick" @check="handleCheck">
|
||||
<template #default="{ node, data }">
|
||||
<el-tooltip v-if="data.isLock" effect="dark" :content="$t('sysuser.noDataScopeTip')"
|
||||
placement="right">
|
||||
<span>
|
||||
<span class="dep-tree-panel__locked-node">
|
||||
{{ node.label }}
|
||||
<SvgIcon name="ele-Lock" />
|
||||
</span>
|
||||
@@ -49,28 +48,25 @@
|
||||
</div>
|
||||
|
||||
<!-- 底部区域 -->
|
||||
<div class="flex justify-center items-center pt-2 z-50 sticky bottom-0 overflow-hidden"
|
||||
style="box-shadow: 0 -8px 8px -8px rgba(0, 0, 0, 0.1)">
|
||||
<el-switch v-model="state.switch" @change="handleSwitchChange" :inactive-text="switchInactiveText"
|
||||
:active-text="switchActiveText"
|
||||
<div class="dep-tree-panel__footer">
|
||||
<el-switch v-model="state.switch" size="small" :inactive-text="switchInactiveText"
|
||||
:active-text="switchActiveText" @change="handleSwitchChange"
|
||||
:disabled="nodeClickable && state.checkedTreeNodes.length > 0" />
|
||||
</div>
|
||||
</div>
|
||||
</pane>
|
||||
|
||||
<pane :size="100 - state.treeWidth" class="pl-1 relative">
|
||||
<pane :size="100 - state.treeWidth" class="draggable-dep-tree__content-pane">
|
||||
<!-- 折叠按钮 -->
|
||||
<div class="w-[12px] h-[56px] bg-gray-200 absolute left-0 top-[44vh] translate-y-[-50%] transition-all cursor-pointer rounded-tr-2xl rounded-br-2xl"
|
||||
@click="handleClickTreeFold()">
|
||||
<i class="text-gray-400 inline-block text-center leading-[62px] hover:opacity-50">
|
||||
<el-icon>
|
||||
<ArrowLeftBold v-if="state.treeWidth > 0" />
|
||||
<ArrowRightBold v-else />
|
||||
</el-icon>
|
||||
</i>
|
||||
</div>
|
||||
<button type="button" class="draggable-dep-tree__fold" :title="treeVisible ? '收起部门树' : '展开部门树'"
|
||||
:aria-label="treeVisible ? '收起部门树' : '展开部门树'" @click="handleClickTreeFold()">
|
||||
<el-icon>
|
||||
<ArrowLeftBold v-if="treeVisible" />
|
||||
<ArrowRightBold v-else />
|
||||
</el-icon>
|
||||
</button>
|
||||
|
||||
<div class="w-full h-full p-3 overflow-auto">
|
||||
<div class="draggable-dep-tree__content">
|
||||
<slot name="default" />
|
||||
</div>
|
||||
</pane>
|
||||
@@ -213,21 +209,257 @@ defineExpose({
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.splitpanes--vertical>.splitpanes__splitter {
|
||||
background: var(--el-color-primary);
|
||||
/* min-width: 2px; */
|
||||
<style lang="scss">
|
||||
.draggable-dep-tree {
|
||||
min-width: 0;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.draggable-dep-tree__tree-pane,
|
||||
.draggable-dep-tree__content-pane {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dep-tree-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
padding: 8px 6px 6px 8px;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.dep-tree-panel__header {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
|
||||
.dep-tree-panel__search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dep-tree-panel__operations {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dep-tree-panel__more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
outline: none;
|
||||
transition: color 180ms ease, background-color 180ms ease;
|
||||
}
|
||||
|
||||
.dep-tree-panel__more .el-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.dep-tree-panel__more:hover {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.dep-tree-panel__more:focus-visible {
|
||||
color: var(--el-color-primary);
|
||||
outline: 2px solid var(--el-color-primary-light-5);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.dep-tree-panel__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 4px 2px 4px 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dep-tree-panel__tree {
|
||||
--el-tree-node-content-height: 26px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
font-size: var(--el-font-size-extra-small);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dep-tree-panel__tree .el-tree-node {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dep-tree-panel__tree .el-tree-node__content {
|
||||
min-width: 0;
|
||||
padding-right: 4px;
|
||||
overflow: hidden;
|
||||
border-radius: var(--el-border-radius-small);
|
||||
transition: color 180ms ease, background-color 180ms ease;
|
||||
}
|
||||
|
||||
.dep-tree-panel__tree .el-tree-node__content>.el-tree-node__expand-icon {
|
||||
padding: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dep-tree-panel__tree .el-tree-node__content>label.el-checkbox {
|
||||
--el-checkbox-input-height: 12px;
|
||||
--el-checkbox-input-width: 12px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.dep-tree-panel__tree .el-tree-node__label,
|
||||
.dep-tree-panel__locked-node {
|
||||
display: inline-flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dep-tree-panel__locked-node .svg-icon {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.dep-tree-panel__footer {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 30px;
|
||||
padding-top: 5px;
|
||||
overflow: hidden;
|
||||
background: var(--el-bg-color);
|
||||
border-top: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
|
||||
.dep-tree-panel__footer .el-switch {
|
||||
--el-switch-on-color: var(--el-color-primary);
|
||||
font-size: var(--el-font-size-extra-small);
|
||||
}
|
||||
|
||||
.draggable-dep-tree__content-pane {
|
||||
position: relative;
|
||||
/* 分隔线与折叠按钮使用同一定位容器,避免跨 pane 定位后被裁剪或覆盖。 */
|
||||
}
|
||||
|
||||
.draggable-dep-tree__content-pane::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 1px;
|
||||
content: "";
|
||||
background: var(--el-border-color-lighter);
|
||||
transition: background-color 180ms ease, width 180ms ease;
|
||||
}
|
||||
|
||||
.draggable-dep-tree__content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px 8px 8px 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.draggable-dep-tree__fold {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 20px;
|
||||
height: 56px;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
outline: none;
|
||||
transform: translateY(-50%);
|
||||
transition: color 180ms ease;
|
||||
}
|
||||
|
||||
.draggable-dep-tree__fold::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 12px;
|
||||
content: "";
|
||||
background: var(--el-fill-color-dark);
|
||||
border-radius: 0 var(--el-border-radius-round) var(--el-border-radius-round) 0;
|
||||
transition: background-color 180ms ease;
|
||||
}
|
||||
|
||||
.draggable-dep-tree__fold .el-icon {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 12px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.draggable-dep-tree__fold:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.draggable-dep-tree__fold:hover::before,
|
||||
.draggable-dep-tree__fold:focus-visible::before {
|
||||
background: var(--el-color-primary-light-8);
|
||||
}
|
||||
|
||||
.draggable-dep-tree__fold:focus-visible {
|
||||
color: var(--el-color-primary);
|
||||
outline: 2px solid var(--el-color-primary-light-5);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.draggable-dep-tree.splitpanes--vertical>.splitpanes__splitter {
|
||||
position: relative;
|
||||
flex: 0 0 6px;
|
||||
width: 6px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.draggable-dep-tree.splitpanes--vertical>.splitpanes__splitter:hover+.draggable-dep-tree__content-pane::before,
|
||||
.draggable-dep-tree.splitpanes--vertical>.splitpanes__splitter:active+.draggable-dep-tree__content-pane::before {
|
||||
width: 2px;
|
||||
transition: all 0.3s ease;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.splitpanes--vertical>.splitpanes__splitter:active {
|
||||
background: var(--el-color-primary);
|
||||
opacity: 0.6;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
.splitpanes--vertical>.splitpanes__splitter:hover {
|
||||
background: var(--el-color-primary);
|
||||
opacity: 0.6;
|
||||
.dep-tree-panel__more,
|
||||
.dep-tree-panel__tree .el-tree-node__content,
|
||||
.draggable-dep-tree__fold,
|
||||
.draggable-dep-tree__fold::before,
|
||||
.draggable-dep-tree__content-pane::before {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.draggable-dep-tree.splitpanes--vertical>.splitpanes__pane {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="preview-text">
|
||||
<el-button v-if="textContent" :style="textViewStyle" type="text" link @click="textViewVisible = true">
|
||||
<span :style="textViewStyle" class="text-view">{{ textContent }}</span>
|
||||
</el-button>
|
||||
<span v-else>--</span>
|
||||
|
||||
<el-dialog v-model="textViewVisible" :title="title" width="min(720px, 90vw)" append-to-body>
|
||||
<pre class="text-content" v-text="textContent"></pre>
|
||||
<template #footer>
|
||||
<el-button @click="textViewVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="preview-text">
|
||||
type Props = {
|
||||
width?: number
|
||||
text?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
width: 0,
|
||||
text: "",
|
||||
title: "文本详情",
|
||||
})
|
||||
|
||||
const textViewVisible = ref(false)
|
||||
|
||||
const textContent = computed(() => props.text || "")
|
||||
|
||||
const textViewStyle = computed(() => ({
|
||||
width: props.width ? `${props.width}px` : "160px",
|
||||
maxWidth: "100%",
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.preview-text {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.text-view {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
max-height: 60vh;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
color: var(--el-text-color-primary);
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
background-color: var(--el-fill-color-light);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<section class="search-panel" :aria-labelledby="titleId">
|
||||
<header class="search-panel__header">
|
||||
<div class="search-panel__title-wrap">
|
||||
<h2 :id="titleId" class="search-panel__title">{{ title }}</h2>
|
||||
<el-tag v-if="!expanded && hiddenActiveCount > 0" size="small" effect="plain" round>
|
||||
更多条件 {{ hiddenActiveCount }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<button v-if="hasMoreFields" type="button" class="search-panel__toggle" :aria-controls="contentId"
|
||||
:aria-expanded="expanded" @click="toggleExpanded">
|
||||
{{ expanded ? collapseText : expandText }}
|
||||
<el-icon class="search-panel__toggle-icon">
|
||||
<ArrowUp v-if="expanded" />
|
||||
<ArrowDown v-else />
|
||||
</el-icon>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<el-form ref="formRef" :model="model" inline class="search-panel__form" @keyup.enter="handleSearch">
|
||||
<div class="search-panel__row">
|
||||
<slot />
|
||||
<div v-if="!expanded || !hasMoreFields" class="search-panel__actions">
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="handleSearch">
|
||||
{{ searchText }}
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" :disabled="loading" @click="handleReset">
|
||||
{{ resetText }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="expanded && hasMoreFields" :id="contentId" class="search-panel__row search-panel__more">
|
||||
<slot name="more" />
|
||||
<div class="search-panel__actions">
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="handleSearch">
|
||||
{{ searchText }}
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" :disabled="loading" @click="handleReset">
|
||||
{{ resetText }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="search-panel">
|
||||
import { ArrowDown, ArrowUp, Refresh, Search } from "@element-plus/icons-vue"
|
||||
import type { FormInstance } from "element-plus"
|
||||
import { getCurrentInstance, useSlots } from "vue"
|
||||
|
||||
/** 全局搜索面板属性。 */
|
||||
interface SearchPanelProps {
|
||||
/** 查询表单模型,由业务页面持有并提交,组件不修改字段语义。 */
|
||||
model: Record<string, any>
|
||||
/** 面板标题。 */
|
||||
title?: string
|
||||
/** 查询按钮文案。 */
|
||||
searchText?: string
|
||||
/** 重置按钮文案。 */
|
||||
resetText?: string
|
||||
/** 查询请求是否进行中,用于禁用重复提交。 */
|
||||
loading?: boolean
|
||||
/** 折叠时仍在生效的隐藏条件数量,帮助用户识别隐含筛选。 */
|
||||
hiddenActiveCount?: number
|
||||
/** 展开按钮文案。 */
|
||||
expandText?: string
|
||||
/** 收起按钮文案。 */
|
||||
collapseText?: string
|
||||
}
|
||||
|
||||
/** 全局搜索面板事件。 */
|
||||
interface SearchPanelEmits {
|
||||
/** 用户点击查询或在表单中按回车时触发。 */
|
||||
(event: "search"): void
|
||||
/** 用户点击重置时触发,字段清理由业务页面按原有规则完成。 */
|
||||
(event: "reset"): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SearchPanelProps>(), {
|
||||
title: "搜索",
|
||||
searchText: "查询",
|
||||
resetText: "重置",
|
||||
loading: false,
|
||||
hiddenActiveCount: 0,
|
||||
expandText: "展开",
|
||||
collapseText: "收起",
|
||||
})
|
||||
const emit = defineEmits<SearchPanelEmits>()
|
||||
const slots = useSlots()
|
||||
|
||||
/** 更多条件的展开状态,支持业务页面按需双向控制。 */
|
||||
const expanded = defineModel<boolean>("expanded", { default: false })
|
||||
/** Element Plus 表单实例,用于向业务页面提供标准表单能力。 */
|
||||
const formRef = ref<FormInstance>()
|
||||
/** 当前组件实例标识,用于关联折叠按钮与可展开区域。 */
|
||||
const instanceUid = getCurrentInstance()?.uid ?? 0
|
||||
/** 面板标题元素 ID。 */
|
||||
const titleId = `search-panel-title-${instanceUid}`
|
||||
/** 更多条件区域 ID。 */
|
||||
const contentId = `search-panel-content-${instanceUid}`
|
||||
/** 是否提供了更多条件插槽。 */
|
||||
const hasMoreFields = computed(() => Boolean(slots.more))
|
||||
|
||||
/** 切换更多查询条件的展开状态。 */
|
||||
const toggleExpanded = () => {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
|
||||
/** 触发业务页面查询,加载期间忽略重复提交。 */
|
||||
const handleSearch = () => {
|
||||
if (props.loading) return
|
||||
emit("search")
|
||||
}
|
||||
|
||||
/** 将重置操作交给业务页面,保留部门树等关联状态的既有清理规则。 */
|
||||
const handleReset = () => {
|
||||
if (props.loading) return
|
||||
emit("reset")
|
||||
}
|
||||
|
||||
/** 清除表单字段及校验状态,供业务页面的重置方法调用。 */
|
||||
const resetFields = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/** 清除表单校验状态,不修改查询模型。 */
|
||||
const clearValidate = () => {
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
defineExpose({ resetFields, clearValidate })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-panel {
|
||||
width: 100%;
|
||||
padding: 8px 12px 0;
|
||||
margin-bottom: 8px;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
}
|
||||
|
||||
.search-panel__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 24px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.search-panel__title-wrap,
|
||||
.search-panel__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-panel__title-wrap {
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-panel__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
font-size: var(--el-font-size-small);
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.search-panel__title::before {
|
||||
width: 2px;
|
||||
height: 14px;
|
||||
content: "";
|
||||
background: var(--el-color-primary);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.search-panel__toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
padding: 0;
|
||||
margin-left: 12px;
|
||||
font: inherit;
|
||||
font-size: var(--el-font-size-small);
|
||||
line-height: 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: none;
|
||||
transition: color 180ms ease;
|
||||
}
|
||||
|
||||
.search-panel__toggle:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.search-panel__toggle:focus-visible {
|
||||
color: var(--el-color-primary);
|
||||
outline: 2px solid var(--el-color-primary-light-5);
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.search-panel__toggle-icon {
|
||||
margin-left: 4px;
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
.search-panel__row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.search-panel__more {
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
|
||||
.search-panel__actions {
|
||||
gap: 6px;
|
||||
min-height: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.search-panel__actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.search-panel__form :deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.search-panel {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.search-panel__row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-panel__form :deep(.el-form-item) {
|
||||
width: 100% !important;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.search-panel__actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.search-panel__toggle-icon {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useDict } from "/@/hooks/dict"
|
||||
import type { Ref } from "vue"
|
||||
|
||||
/** 用户身份字典中的单个展示选项。 */
|
||||
export interface UserIdentityTypeOption {
|
||||
/** 面向管理人员展示的身份名称。 */
|
||||
label: string
|
||||
/** 前后端共同使用的用户身份协议编码。 */
|
||||
value: string | number
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有用户身份信息组件共享的字典引用。
|
||||
*
|
||||
* 表格会同时创建多行组件实例,复用同一引用可避免字典尚未缓存时重复发起请求。
|
||||
*/
|
||||
let sharedIdentityTypeOptions: Ref<UserIdentityTypeOption[]> | undefined
|
||||
|
||||
/**
|
||||
* 获取用户身份字典的共享响应式引用,首次调用时沿用项目统一字典 Hook 初始化。
|
||||
*
|
||||
* @returns 所有用户身份信息组件共同读取的身份字典引用
|
||||
*/
|
||||
export const useUserIdentityTypeOptions = (): Ref<UserIdentityTypeOption[]> => {
|
||||
if (!sharedIdentityTypeOptions) {
|
||||
const { user_identity_type } = useDict("user_identity_type")
|
||||
sharedIdentityTypeOptions = user_identity_type
|
||||
}
|
||||
return sharedIdentityTypeOptions
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div v-if="identityItems.length" class="min-w-0 text-left">
|
||||
<div v-for="identity in identityItems" :key="identity.type" class="user-identity-info__item min-w-0">
|
||||
<div class="flex min-w-0 items-center gap-1.5 leading-4">
|
||||
<span class="h-3 w-0.5 shrink-0 rounded-sm bg-primary" aria-hidden="true"></span>
|
||||
<span class="truncate text-xs font-semibold text-[var(--el-text-color-primary)]" :title="identity.label">
|
||||
{{ identity.label }}
|
||||
</span>
|
||||
</div>
|
||||
<dl class="m-0 mt-0.5 grid gap-0 text-xs leading-4">
|
||||
<div v-for="detail in identity.details" :key="detail.label" class="flex min-w-0 gap-2">
|
||||
<dt class="w-20 shrink-0 text-[var(--el-text-color-secondary)]">{{ detail.label }}</dt>
|
||||
<dd class="m-0 truncate text-[var(--el-text-color-primary)]" :title="detail.value">
|
||||
{{ detail.value }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-xs leading-4 text-[var(--el-text-color-placeholder)]">{{ props.emptyText }}</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="UserIdentityInfo">
|
||||
import { DEFAULT_USER_IDENTITY, STUDENT_USER_IDENTITY, TEACHER_USER_IDENTITY } from "/@/constants/userIdentity"
|
||||
import { useUserIdentityTypeOptions } from "./identity-options"
|
||||
|
||||
/** 学生身份对应的扩展资料摘要。 */
|
||||
interface UserStudentIdentityInfo {
|
||||
/** 学生学号;为空时组件显示未填写。 */
|
||||
studentNo?: string | number | null
|
||||
/** 学校名称;为空时组件显示未填写。 */
|
||||
schoolName?: string | null
|
||||
/** 班级名称;为空时组件显示未填写。 */
|
||||
className?: string | null
|
||||
/** 监护人手机号集合,多个手机号由英文逗号分隔。 */
|
||||
guardianPhones?: string | null
|
||||
}
|
||||
|
||||
/** 老师身份对应的扩展资料摘要。 */
|
||||
interface UserTeacherIdentityInfo {
|
||||
/** 老师学号;为空时组件显示未填写。 */
|
||||
teacherNo?: string | number | null
|
||||
}
|
||||
|
||||
/** 可复用身份信息组件需要的最小用户摘要。 */
|
||||
interface UserIdentityInfoUser {
|
||||
/** 用户拥有的身份协议编码列表;为空数组或缺失时展示空状态。 */
|
||||
identityTypes?: Array<string | number> | null
|
||||
/** 默认用户身份使用的绑定手机号。 */
|
||||
phone?: string | number | null
|
||||
/** 学生身份使用的扩展资料。 */
|
||||
studentInfo?: UserStudentIdentityInfo | null
|
||||
/** 老师身份使用的扩展资料。 */
|
||||
teacherInfo?: UserTeacherIdentityInfo | null
|
||||
}
|
||||
|
||||
/** 用户身份信息组件的展示配置。 */
|
||||
interface UserIdentityInfoProps {
|
||||
/** 后端接口或上层列表传入的用户身份摘要。 */
|
||||
user: UserIdentityInfoUser
|
||||
/** 用户没有任何身份时展示的占位文案。 */
|
||||
emptyText?: string
|
||||
}
|
||||
|
||||
/** 当前组件输入及其空状态默认文案。 */
|
||||
const props = withDefaults(defineProps<UserIdentityInfoProps>(), {
|
||||
emptyText: "暂无身份信息",
|
||||
})
|
||||
|
||||
/** 用户身份字典的共享响应式引用,用于将协议编码转换为展示名称。 */
|
||||
const identityTypeOptions = useUserIdentityTypeOptions()
|
||||
|
||||
/** 用户身份区中的单项资料。 */
|
||||
interface UserIdentityDetailItem {
|
||||
/** 面向管理人员展示的资料字段名称。 */
|
||||
label: string
|
||||
/** 接口值经过空值规范化后的展示文本。 */
|
||||
value: string
|
||||
}
|
||||
|
||||
/** 用户身份区中的一个身份及其全部资料。 */
|
||||
interface UserIdentityDisplayItem {
|
||||
/** 前后端共同使用的身份协议编码,同时作为稳定渲染键。 */
|
||||
type: string
|
||||
/** 字典映射后的身份名称,未知编码回退为原始编码。 */
|
||||
label: string
|
||||
/** 当前身份需要展示的资料列表。 */
|
||||
details: UserIdentityDetailItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 将接口值规范为表格展示文本,避免 null、空字符串或纯空白形成无法识别的留白。
|
||||
*
|
||||
* @param value 用户身份接口返回的原始资料值
|
||||
* @param emptyText 资料缺失时展示的业务占位文案
|
||||
* @returns 去除首尾空白后的资料文本,或指定占位文案
|
||||
*/
|
||||
const formatIdentityValue = (value: unknown, emptyText = "未填写") => {
|
||||
if (value === undefined || value === null) return emptyText
|
||||
const text = String(value).trim()
|
||||
return text || emptyText
|
||||
}
|
||||
|
||||
/**
|
||||
* 按身份协议编码提取该身份对应的资料,未知编码保留原始值以便排查历史数据。
|
||||
*
|
||||
* @param identityType 前后端共同使用的身份协议编码
|
||||
* @returns 当前身份需要展示的字段名称和值
|
||||
*/
|
||||
const getIdentityDetails = (identityType: string): UserIdentityDetailItem[] => {
|
||||
if (identityType === DEFAULT_USER_IDENTITY) {
|
||||
return [{ label: "手机号", value: formatIdentityValue(props.user.phone, "未绑定") }]
|
||||
}
|
||||
if (identityType === STUDENT_USER_IDENTITY) {
|
||||
// 学校与班级共同描述学生的学籍归属,合并展示可减少列表行高。
|
||||
return [
|
||||
{ label: "学号", value: formatIdentityValue(props.user.studentInfo?.studentNo) },
|
||||
{
|
||||
label: "学校 / 班级",
|
||||
value: `${formatIdentityValue(props.user.studentInfo?.schoolName)} / ${formatIdentityValue(props.user.studentInfo?.className)}`,
|
||||
},
|
||||
{ label: "监护人手机号", value: formatIdentityValue(props.user.studentInfo?.guardianPhones) },
|
||||
]
|
||||
}
|
||||
if (identityType === TEACHER_USER_IDENTITY) {
|
||||
return [{ label: "老师学号", value: formatIdentityValue(props.user.teacherInfo?.teacherNo) }]
|
||||
}
|
||||
return [{ label: "身份编码", value: identityType }]
|
||||
}
|
||||
|
||||
/**
|
||||
* 从共享字典取得身份名称,字典尚未加载或遇到未知编码时回退为协议编码。
|
||||
*
|
||||
* @param identityType 前后端共同使用的身份协议编码
|
||||
* @returns 字典中的身份名称或原始编码
|
||||
*/
|
||||
const getIdentityLabel = (identityType: string) => {
|
||||
const option = identityTypeOptions.value?.find((item) => String(item.value) === identityType)
|
||||
return option?.label || identityType
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范并组装用户的全部身份展示项;去除空编码和重复编码,保持接口返回的原始顺序。
|
||||
*/
|
||||
const identityItems = computed<UserIdentityDisplayItem[]>(() => {
|
||||
if (!Array.isArray(props.user.identityTypes)) return []
|
||||
const identityTypes = props.user.identityTypes.map((item) => String(item).trim()).filter(Boolean)
|
||||
return Array.from(new Set(identityTypes)).map((type) => ({
|
||||
type,
|
||||
label: getIdentityLabel(type),
|
||||
details: getIdentityDetails(type),
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-identity-info__item + .user-identity-info__item {
|
||||
padding-top: 4px;
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<el-form-item class="w-[250px] max-md:w-full" label="用户身份" prop="identityTypes">
|
||||
<dict-select v-model="identityTypes" dict-code="user_identity_type" multiple placeholder="请选择用户身份" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-for="fieldConfig in activeFieldConfigs" :key="`${fieldConfig.modelName}.${fieldConfig.field}`"
|
||||
class="w-[250px] max-md:w-full" :label="fieldConfig.label"
|
||||
:prop="`${fieldConfig.modelName}.${fieldConfig.field}`">
|
||||
<dict-select v-if="fieldConfig.control === 'dict-select'" :model-value="getQueryFieldValue(fieldConfig)"
|
||||
:dict-code="fieldConfig.dictCode || ''" :placeholder="fieldConfig.placeholder"
|
||||
@update:model-value="updateQueryFieldValue(fieldConfig, $event)" />
|
||||
<el-input v-else :model-value="getQueryFieldValue(fieldConfig)" clearable :placeholder="fieldConfig.placeholder"
|
||||
@update:model-value="updateQueryFieldValue(fieldConfig, $event)" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="w-[180px] max-md:w-full" label="人脸录入" prop="hasFace">
|
||||
<el-select v-model="hasFace" clearable placeholder="全部">
|
||||
<el-option label="已录入" value="1" />
|
||||
<el-option label="未录入" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="UserIdentityQuery">
|
||||
import { STUDENT_USER_IDENTITY, TEACHER_USER_IDENTITY } from "/@/constants/userIdentity"
|
||||
import {
|
||||
createEmptyUserIdentityStudentQuery,
|
||||
createEmptyUserIdentityTeacherQuery,
|
||||
type UserIdentityStudentQuery,
|
||||
type UserIdentityTeacherQuery,
|
||||
type UserIdentityQueryFieldConfig,
|
||||
type UserIdentityQueryModelName,
|
||||
} from "./types"
|
||||
|
||||
/** 用户身份扩展查询字段配置;新增身份字段时在此扩展对应协议编码、模型和控件。 */
|
||||
const identityQueryFieldConfigs: UserIdentityQueryFieldConfig[] = [
|
||||
// 学生学号使用文本输入,兼容学校已有的字母数字混合编号规则,后端按完整编号匹配。
|
||||
{
|
||||
identityType: STUDENT_USER_IDENTITY,
|
||||
modelName: "studentInfo",
|
||||
field: "studentNo",
|
||||
label: "学号",
|
||||
placeholder: "请输入学生学号",
|
||||
control: "input",
|
||||
},
|
||||
// 学校名称沿用用户编辑表单的统一字典,避免自由输入产生无效学校值。
|
||||
{
|
||||
identityType: STUDENT_USER_IDENTITY,
|
||||
modelName: "studentInfo",
|
||||
field: "schoolName",
|
||||
label: "学校",
|
||||
placeholder: "请选择学校",
|
||||
control: "dict-select",
|
||||
dictCode: "user_student_school",
|
||||
},
|
||||
// 班级名称当前没有公共字典,继续按后端扩展资料原值查询。
|
||||
{
|
||||
identityType: STUDENT_USER_IDENTITY,
|
||||
modelName: "studentInfo",
|
||||
field: "className",
|
||||
label: "班级",
|
||||
placeholder: "请输入班级名称",
|
||||
control: "input",
|
||||
},
|
||||
// 监护人资料存储为逗号分隔手机号集合,输入单个手机号即可包含匹配。
|
||||
{
|
||||
identityType: STUDENT_USER_IDENTITY,
|
||||
modelName: "studentInfo",
|
||||
field: "guardianPhones",
|
||||
label: "监护人",
|
||||
placeholder: "请输入监护人手机号",
|
||||
control: "input",
|
||||
},
|
||||
// 老师身份使用独立扩展表和独立学号字段,不复用学生学号协议。
|
||||
{
|
||||
identityType: TEACHER_USER_IDENTITY,
|
||||
modelName: "teacherInfo",
|
||||
field: "teacherNo",
|
||||
label: "老师学号",
|
||||
placeholder: "请输入老师学号",
|
||||
control: "input",
|
||||
},
|
||||
]
|
||||
|
||||
/** 用户身份类型协议编码集合,由字典值与后端身份表共同约束。 */
|
||||
const identityTypes = defineModel<Array<string | number>>("identityTypes", { default: () => [] })
|
||||
/** 学生身份扩展查询条件,由页面查询模型持有并提交给用户接口。 */
|
||||
const studentInfo = defineModel<UserIdentityStudentQuery>("studentInfo", {
|
||||
default: createEmptyUserIdentityStudentQuery,
|
||||
})
|
||||
/** 老师身份扩展查询条件,由页面查询模型持有并提交给用户接口。 */
|
||||
const teacherInfo = defineModel<UserIdentityTeacherQuery>("teacherInfo", {
|
||||
default: createEmptyUserIdentityTeacherQuery,
|
||||
})
|
||||
/** 人脸录入状态查询条件:1 已录入,0 未录入,空值表示全部。 */
|
||||
const hasFace = defineModel<string>("hasFace", { default: "" })
|
||||
|
||||
/** 当前选中身份对应的扩展查询字段配置,驱动字段控件渲染。 */
|
||||
const activeFieldConfigs = computed(() => {
|
||||
const selected = new Set(identityTypes.value.map((value) => String(value)))
|
||||
return identityQueryFieldConfigs.filter((fieldConfig) => selected.has(fieldConfig.identityType))
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取指定身份扩展查询模型,供配置化字段统一读写。
|
||||
*
|
||||
* @param modelName 字段所属的学生或老师查询模型名称
|
||||
* @returns 当前页面持有的对应身份查询模型
|
||||
*/
|
||||
const getQueryModel = (modelName: UserIdentityQueryModelName): Record<string, string | undefined> => {
|
||||
return (modelName === "studentInfo" ? studentInfo.value : teacherInfo.value) as Record<string, string | undefined>
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取配置字段的当前查询值。
|
||||
*
|
||||
* @param fieldConfig 当前身份扩展字段配置
|
||||
* @returns 当前字段值,未填写时返回空字符串
|
||||
*/
|
||||
const getQueryFieldValue = (fieldConfig: UserIdentityQueryFieldConfig) => {
|
||||
return getQueryModel(fieldConfig.modelName)[fieldConfig.field] || ""
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置字段的查询值,普通输入会去除首尾空格,字典值保持协议原值。
|
||||
*
|
||||
* @param fieldConfig 当前身份扩展字段配置
|
||||
* @param value Element Plus 控件返回的字段值
|
||||
*/
|
||||
const updateQueryFieldValue = (fieldConfig: UserIdentityQueryFieldConfig, value: unknown) => {
|
||||
const nextValue = typeof value === "string" ? value.trim() : String(value || "")
|
||||
if (fieldConfig.modelName === "studentInfo") {
|
||||
studentInfo.value = { ...studentInfo.value, [fieldConfig.field]: nextValue }
|
||||
return
|
||||
}
|
||||
teacherInfo.value = { ...teacherInfo.value, [fieldConfig.field]: nextValue }
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换身份时同步清理未选身份的查询条件,避免用户看不到的条件继续提交。
|
||||
* 默认身份的手机号继续使用用户主查询字段,不在此重复生成同义条件。
|
||||
*/
|
||||
watch(
|
||||
identityTypes,
|
||||
(values) => {
|
||||
const selected = new Set(values.map((value) => String(value)))
|
||||
if (!selected.has(STUDENT_USER_IDENTITY)) studentInfo.value = createEmptyUserIdentityStudentQuery()
|
||||
if (!selected.has(TEACHER_USER_IDENTITY)) teacherInfo.value = createEmptyUserIdentityTeacherQuery()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,62 @@
|
||||
/** 用户身份扩展查询字段所属的前端模型。 */
|
||||
export type UserIdentityQueryModelName = "studentInfo" | "teacherInfo"
|
||||
|
||||
/** 用户身份扩展查询字段使用的表单控件。 */
|
||||
export type UserIdentityQueryControl = "input" | "dict-select"
|
||||
|
||||
/** 单个用户身份扩展查询字段的渲染配置。 */
|
||||
export interface UserIdentityQueryFieldConfig {
|
||||
/** 显示该字段所需的身份协议编码。 */
|
||||
identityType: string
|
||||
/** 字段所属的查询模型名称。 */
|
||||
modelName: UserIdentityQueryModelName
|
||||
/** 前后端 DTO 共同使用的字段名称。 */
|
||||
field: string
|
||||
/** 查询表单展示的字段名称。 */
|
||||
label: string
|
||||
/** 字段未填写时展示的输入提示。 */
|
||||
placeholder: string
|
||||
/** 字段使用的表单控件类型。 */
|
||||
control: UserIdentityQueryControl
|
||||
/** 字典选择控件的数据字典编码;普通输入框不设置。 */
|
||||
dictCode?: string
|
||||
}
|
||||
|
||||
/** 学生身份扩展查询条件;学号和学校使用精确匹配,其余文本字段使用包含匹配。 */
|
||||
export interface UserIdentityStudentQuery {
|
||||
/** 学生学号,来源于学生身份扩展表,按完整学号精确匹配。 */
|
||||
studentNo?: string
|
||||
/** 学校名称,来源于学生身份扩展表,按学校字典值精确匹配。 */
|
||||
schoolName?: string
|
||||
/** 班级名称,来源于学生身份扩展表。 */
|
||||
className?: string
|
||||
/** 监护人手机号,来源于学生身份扩展表中逗号分隔的手机号集合。 */
|
||||
guardianPhones?: string
|
||||
}
|
||||
|
||||
/** 老师身份扩展查询条件。 */
|
||||
export interface UserIdentityTeacherQuery {
|
||||
/** 老师学号,来源于老师身份扩展表,按完整学号精确匹配。 */
|
||||
teacherNo?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户身份通用查询组件的学生扩展字段初始值。
|
||||
*
|
||||
* @returns 所有学生身份查询字段均为空的独立对象
|
||||
*/
|
||||
export const createEmptyUserIdentityStudentQuery = (): UserIdentityStudentQuery => ({
|
||||
studentNo: "",
|
||||
schoolName: "",
|
||||
className: "",
|
||||
guardianPhones: "",
|
||||
})
|
||||
|
||||
/**
|
||||
* 创建用户身份通用查询组件的老师扩展字段初始值。
|
||||
*
|
||||
* @returns 老师学号为空的独立对象
|
||||
*/
|
||||
export const createEmptyUserIdentityTeacherQuery = (): UserIdentityTeacherQuery => ({
|
||||
teacherNo: "",
|
||||
})
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="flex min-w-0 items-center gap-3 text-left">
|
||||
<el-avatar :size="props.avatarSize" :src="props.user.avatar || undefined" :alt="displayName" class="user-info-cell__avatar shrink-0">
|
||||
{{ avatarFallback }}
|
||||
</el-avatar>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex min-w-0 items-center gap-1.5 leading-4">
|
||||
<span class="truncate font-medium text-[var(--el-text-color-primary)]" :title="displayName">
|
||||
{{ displayName }}
|
||||
</span>
|
||||
<span v-if="props.showGender" class="shrink-0 text-xs text-[var(--el-text-color-secondary)]">
|
||||
{{ genderLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<dl v-if="props.showAccount || props.showPhone || props.showDepartment" class="m-0 mt-0.5 grid gap-0 text-xs leading-4">
|
||||
<div v-if="props.showAccount" class="flex min-w-0 gap-1.5">
|
||||
<dt class="w-10 shrink-0 text-[var(--el-text-color-secondary)]">账号:</dt>
|
||||
<dd class="m-0 truncate text-[var(--el-text-color-regular)]" :title="accountTitle">
|
||||
{{ accountText }}
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="props.showPhone" class="flex min-w-0 gap-1.5">
|
||||
<dt class="w-10 shrink-0 text-[var(--el-text-color-secondary)]">手机:</dt>
|
||||
<dd class="m-0 truncate text-[var(--el-text-color-regular)]" :title="phoneTitle">
|
||||
{{ phoneText }}
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="props.showDepartment" class="flex min-w-0 gap-1.5">
|
||||
<dt class="w-10 shrink-0 text-[var(--el-text-color-secondary)]">部门:</dt>
|
||||
<dd class="m-0 truncate text-[var(--el-text-color-regular)]" :title="departmentTitle">
|
||||
{{ departmentText }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="UserInfoCell">
|
||||
/** 可复用用户信息单元格需要的最小用户摘要。 */
|
||||
interface UserInfoCellUser {
|
||||
/** 用户头像地址;为空时组件展示姓名或账号首字。 */
|
||||
avatar?: string | null
|
||||
/** 用户姓名;为空时显示明确的缺失占位。 */
|
||||
name?: string | null
|
||||
/** 用户登录账号;为空时显示未分配。 */
|
||||
username?: string | null
|
||||
/** 用户绑定手机号;为空时显示未绑定。 */
|
||||
phone?: string | null
|
||||
/** 用户性别编码,1 为男、2 为女,其他值统一显示未知。 */
|
||||
gender?: string | number | null
|
||||
/** 用户所属部门名称;为空时显示未分配。 */
|
||||
deptName?: string | null
|
||||
}
|
||||
|
||||
/** 用户信息单元格的展示配置。 */
|
||||
interface UserInfoCellProps {
|
||||
/** 后端接口或上层列表传入的用户摘要。 */
|
||||
user: UserInfoCellUser
|
||||
/** 头像直径,单位为像素。 */
|
||||
avatarSize?: number
|
||||
/** 是否展示性别文本。 */
|
||||
showGender?: boolean
|
||||
/** 是否展示登录账号。 */
|
||||
showAccount?: boolean
|
||||
/** 是否展示绑定手机号。 */
|
||||
showPhone?: boolean
|
||||
/** 是否展示所属部门。 */
|
||||
showDepartment?: boolean
|
||||
}
|
||||
|
||||
/** 当前组件的输入与默认展示策略。 */
|
||||
const props = withDefaults(defineProps<UserInfoCellProps>(), {
|
||||
avatarSize: 38,
|
||||
showGender: true,
|
||||
showAccount: true,
|
||||
showPhone: true,
|
||||
showDepartment: true,
|
||||
})
|
||||
|
||||
/**
|
||||
* 规范接口文本,避免 null、空字符串或纯空白在复合单元格中形成无法识别的留白。
|
||||
*
|
||||
* @param value 上层用户摘要中的原始字段值
|
||||
* @param emptyText 字段缺失时面向管理人员展示的占位文案
|
||||
* @returns 去除首尾空白后的展示文本,或指定占位文案
|
||||
*/
|
||||
const formatText = (value: string | null | undefined, emptyText: string) => {
|
||||
const text = value?.trim()
|
||||
return text || emptyText
|
||||
}
|
||||
|
||||
/** 用户姓名展示文本。 */
|
||||
const displayName = computed(() => formatText(props.user.name, "未填写姓名"))
|
||||
|
||||
/** 头像图片不可用时展示的姓名或账号首字。 */
|
||||
const avatarFallback = computed(() => {
|
||||
const source = props.user.name?.trim() || props.user.username?.trim() || "用户"
|
||||
return source.slice(0, 1)
|
||||
})
|
||||
|
||||
/** 与后端性别编码对应的展示文本。 */
|
||||
const genderLabel = computed(() => {
|
||||
if (Number(props.user.gender) === 1) return "男"
|
||||
if (Number(props.user.gender) === 2) return "女"
|
||||
return "未知"
|
||||
})
|
||||
|
||||
/** 登录账号展示文本。 */
|
||||
const accountText = computed(() => formatText(props.user.username, "未分配"))
|
||||
|
||||
/** 登录账号完整提示文案。 */
|
||||
const accountTitle = computed(() => formatText(props.user.username, "未分配账号"))
|
||||
|
||||
/** 手机号展示文本。 */
|
||||
const phoneText = computed(() => formatText(props.user.phone, "未绑定"))
|
||||
|
||||
/** 手机号完整提示文案。 */
|
||||
const phoneTitle = computed(() => formatText(props.user.phone, "未绑定手机号"))
|
||||
|
||||
/** 所属部门展示文本。 */
|
||||
const departmentText = computed(() => formatText(props.user.deptName, "未分配"))
|
||||
|
||||
/** 所属部门完整提示文案。 */
|
||||
const departmentTitle = computed(() => formatText(props.user.deptName, "未分配部门"))
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-info-cell__avatar {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
border: 1px solid var(--el-color-primary-light-7);
|
||||
}
|
||||
</style>
|
||||
@@ -15,8 +15,14 @@ import TagList from "/@/components/TagList/index.vue"
|
||||
import SvgIcon from "/@/components/SvgIcon/index.vue"
|
||||
import PreviewImage from "/@/components/PreviewImage/index.vue"
|
||||
import PreviewJson from "/@/components/PreviewJson/index.vue"
|
||||
import PreviewText from "/@/components/PreviewText/index.vue"
|
||||
import PreviewXml from "/@/components/PreviewXml/index.vue"
|
||||
import DraggableDepTree from "/@/components/DraggableDepTree/index.vue"
|
||||
import DraggableDepMchTree from "/@/components/DraggableDepMchTree/index.vue"
|
||||
import SearchPanel from "/@/components/SearchPanel/index.vue"
|
||||
import UserInfoCell from "/@/components/UserInfoCell/index.vue"
|
||||
import UserIdentityInfo from "/@/components/UserIdentityInfo/index.vue"
|
||||
import UserIdentityQuery from "/@/components/UserIdentityQuery/index.vue"
|
||||
|
||||
// 业务组件
|
||||
import TreeMultipleSelect from "/@/components/TreeMultipleSelect/index.vue"
|
||||
@@ -80,8 +86,14 @@ export default {
|
||||
app.component("SvgIcon", SvgIcon)
|
||||
app.component("PreviewImage", PreviewImage)
|
||||
app.component("PreviewJson", PreviewJson)
|
||||
app.component("PreviewText", PreviewText)
|
||||
app.component("PreviewXml", PreviewXml)
|
||||
app.component("DraggableDepTree", DraggableDepTree)
|
||||
app.component("DraggableDepMchTree", DraggableDepMchTree)
|
||||
app.component("SearchPanel", SearchPanel)
|
||||
app.component("UserInfoCell", UserInfoCell)
|
||||
app.component("UserIdentityInfo", UserIdentityInfo)
|
||||
app.component("UserIdentityQuery", UserIdentityQuery)
|
||||
app.component("TreeMultipleSelect", TreeMultipleSelect)
|
||||
app.component("SelectHard", SelectHard)
|
||||
app.component("SelectHardSpuCategory", SelectHardSpuCategory)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/** 自动退款规则名称,持久化后由 Wallet、Hardware 和前端共同使用。 */
|
||||
export const AUTO_REFUND_RULE = {
|
||||
/** 以订单实际支付时间加配置小时数作为免审截止。 */
|
||||
PAY_TIME_LIMIT: "PAY_TIME_LIMIT",
|
||||
/** 以当前周期预约批次截止时刻作为免审截止。 */
|
||||
APPT_CYCLE_CLOSE: "APPT_CYCLE_CLOSE",
|
||||
} as const
|
||||
|
||||
/** 自动退款规则名称联合类型。 */
|
||||
export type AutoRefundRule = (typeof AUTO_REFUND_RULE)[keyof typeof AUTO_REFUND_RULE]
|
||||
|
||||
/** 自动退款规则的管理端选择项。 */
|
||||
export const AUTO_REFUND_RULE_OPTIONS: Array<{ label: string; value: AutoRefundRule }> = [
|
||||
{ label: "支付后限时", value: AUTO_REFUND_RULE.PAY_TIME_LIMIT },
|
||||
{ label: "周期预约截止", value: AUTO_REFUND_RULE.APPT_CYCLE_CLOSE },
|
||||
]
|
||||
|
||||
/** 自动退款任务场景编码,与 Hardware 任务表中的 task_type 保持一致。 */
|
||||
export const AUTO_REFUND_TASK_TYPE = {
|
||||
/** 规定时间内用户申请的免审核退款。 */
|
||||
AFTER_SALE: "10",
|
||||
/** 预约订单超过使用期限后的系统自动退款。 */
|
||||
EXPIRED_RESERVE: "20",
|
||||
} as const
|
||||
|
||||
/** 自动退款任务场景的管理端展示配置。 */
|
||||
export const AUTO_REFUND_TASK_TYPE_META: Record<string, { label: string; type: "info" | "primary" | "warning" | "danger" | "success" }> = {
|
||||
/** 管理端标识在商户规定时限内由用户发起且无需人工审核的退款。 */
|
||||
[AUTO_REFUND_TASK_TYPE.AFTER_SALE]: { label: "规定时间内用户免审核退款", type: "success" },
|
||||
/** 管理端标识订单超过预约使用期限后由系统创建的退款。 */
|
||||
[AUTO_REFUND_TASK_TYPE.EXPIRED_RESERVE]: { label: "过期订单自动退款", type: "warning" },
|
||||
}
|
||||
|
||||
/** 自动退款任务状态展示配置。 */
|
||||
export const AUTO_REFUND_TASK_STATUS: Record<string, { label: string; type: "info" | "primary" | "warning" | "danger" | "success" }> = {
|
||||
/** 售后事务已创建任务,等待首次同步执行。 */
|
||||
"10": { label: "待执行", type: "primary" },
|
||||
/** 资金调用失败,等待固定退避时间到期。 */
|
||||
"20": { label: "等待重试", type: "warning" },
|
||||
/** 当前商户开关关闭,暂停且不消耗自动尝试额度。 */
|
||||
"30": { label: "开关关闭暂停", type: "info" },
|
||||
/** 配置缺失、变更或查询异常,等待配置恢复。 */
|
||||
"31": { label: "配置不可用暂停", type: "warning" },
|
||||
/** 首次执行和七次自动重试均失败,只允许技术重试。 */
|
||||
"40": { label: "重试耗尽", type: "danger" },
|
||||
/** 钱包到账及 Hardware 业务状态均已完成。 */
|
||||
"50": { label: "成功", type: "success" },
|
||||
}
|
||||
|
||||
/** 自动退款尝试触发来源展示名称。 */
|
||||
export const AUTO_REFUND_TRIGGER_TYPE: Record<string, string> = {
|
||||
/** 售后申请事务提交后的首次同步尝试。 */
|
||||
"10": "首次同步执行",
|
||||
/** 三十秒调度器扫描到期任务后触发。 */
|
||||
"20": "定时重试",
|
||||
/** 管理端技术人员对耗尽任务额外触发一次。 */
|
||||
"30": "人工技术重试",
|
||||
/** 管理端具备业务退款权限的逐单过期预约退款。 */
|
||||
"40": "后台逐单过期退款",
|
||||
/** 管理端按当前商户配置重新扫描历史订单。 */
|
||||
"50": "后台立即重新扫描",
|
||||
}
|
||||
|
||||
/** 自动退款尝试结果展示配置。 */
|
||||
export const AUTO_REFUND_ATTEMPT_RESULT: Record<string, { label: string; type: "info" | "primary" | "warning" | "danger" | "success" }> = {
|
||||
/** 尝试日志已创建但资金调用尚未完成。 */
|
||||
"10": { label: "执行中", type: "primary" },
|
||||
/** 钱包退款和 Hardware 状态更新均已提交。 */
|
||||
"20": { label: "成功", type: "success" },
|
||||
/** 本次实际资金调用失败。 */
|
||||
"30": { label: "失败", type: "danger" },
|
||||
/** 执行前发现开关关闭,未调用资金接口。 */
|
||||
"40": { label: "开关关闭跳过", type: "info" },
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务状态展示配置,兼容后端未来新增但前端尚未识别的状态。
|
||||
*
|
||||
* @param status 任务状态编码。
|
||||
*/
|
||||
export const getAutoRefundTaskStatus = (status?: string) =>
|
||||
AUTO_REFUND_TASK_STATUS[status || ""] || { label: status || "-", type: "info" as const }
|
||||
|
||||
/**
|
||||
* 获取自动退款任务场景展示配置,未知编码保留原值便于排查协议兼容问题。
|
||||
*
|
||||
* @param taskType 自动退款任务场景编码。
|
||||
*/
|
||||
export const getAutoRefundTaskType = (taskType?: string) =>
|
||||
AUTO_REFUND_TASK_TYPE_META[taskType || ""] || { label: taskType || "-", type: "info" as const }
|
||||
|
||||
/**
|
||||
* 获取尝试结果展示配置,未知状态保留原编码便于技术排查。
|
||||
*
|
||||
* @param status 尝试结果编码。
|
||||
*/
|
||||
export const getAutoRefundAttemptResult = (status?: string) =>
|
||||
AUTO_REFUND_ATTEMPT_RESULT[status || ""] || { label: status || "-", type: "info" as const }
|
||||
@@ -0,0 +1,8 @@
|
||||
/** 默认用户身份在前后端接口与用户身份表中使用的固定编码。 */
|
||||
export const DEFAULT_USER_IDENTITY = "DEFAULT"
|
||||
|
||||
/** 学生身份在前后端接口与学生扩展资料中使用的固定编码。 */
|
||||
export const STUDENT_USER_IDENTITY = "STUDENT"
|
||||
|
||||
/** 老师身份在前后端接口与老师扩展资料中使用的固定编码。 */
|
||||
export const TEACHER_USER_IDENTITY = "TEACHER"
|
||||
+4
-4
@@ -132,13 +132,13 @@ export function useTable(options?: BasicTableProps) {
|
||||
ascs: state.ascs?.join(","),
|
||||
})
|
||||
|
||||
// 设置表格展示的数据数组
|
||||
state.dataList = state.isPage ? res.data[state.props.item] : res.data
|
||||
// 设置表格展示的数据数组;后端对无数据场景可能返回 data:null,统一兜底避免 TypeError
|
||||
state.dataList = state.isPage ? (res.data?.[state.props.item] ?? []) : (res.data ?? [])
|
||||
// 设置分页信息中的总数据条数
|
||||
state.pagination!.total = state.isPage ? res.data[state.props.totalCount] : 0
|
||||
state.pagination!.total = state.isPage ? (res.data?.[state.props.totalCount] ?? 0) : 0
|
||||
} catch (err: any) {
|
||||
// 捕获异常并显示错误提示
|
||||
ElMessage.error(err.msg || err.data.msg)
|
||||
ElMessage.error(err.msg || err.data?.msg || "查询失败")
|
||||
} finally {
|
||||
// 结束加载数据,设置state.loading为false
|
||||
state.loading = false
|
||||
|
||||
@@ -57,6 +57,15 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
|
||||
isHide: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/hard/hard-manager/details",
|
||||
name: "router.hardManagerDetails",
|
||||
component: () => import("/@/views/hard/hard-manager/details.vue"),
|
||||
meta: {
|
||||
isHide: true,
|
||||
isKeepAlive: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row v-show="showSearch">
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item :label="$t('audit.auditName')" prop="auditName">
|
||||
<el-input :placeholder="t('audit.inputAuditNameTip')" v-model="state.queryForm.auditName" style="max-width: 180px" />
|
||||
</el-form-item>
|
||||
@@ -12,13 +21,7 @@
|
||||
<el-form-item :label="$t('audit.createBy')" prop="createBy">
|
||||
<el-input :placeholder="t('audit.inputCreateByTip')" v-model="state.queryForm.createBy" style="max-width: 180px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button formDialogRef icon="search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" formDialogRef @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
|
||||
@@ -2,20 +2,23 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row class="ml10" v-show="showSearch">
|
||||
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item :label="$t('client.clientId')" prop="clientId">
|
||||
<el-input :placeholder="$t('client.clientId')" style="max-width: 180px" v-model="state.queryForm.clientId" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('client.clientSecret')" prop="clientSecret">
|
||||
<el-input :placeholder="$t('client.clientSecret')" style="max-width: 180px" v-model="state.queryForm.clientSecret" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="getDataList" icon="search" type="primary">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button @click="resetQuery" icon="Refresh">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row class="ml10" v-show="showSearch">
|
||||
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item :label="$t('i18n.name')" prop="name">
|
||||
<el-input :placeholder="t('i18n.inputKeyTip')" style="max-width: 180px" v-model="state.queryForm.name" />
|
||||
</el-form-item>
|
||||
@@ -12,21 +21,22 @@
|
||||
<el-form-item :label="$t('i18n.en')" prop="en">
|
||||
<el-input :placeholder="t('i18n.inputEnTip')" style="max-width: 180px" v-model="state.queryForm.en" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="getDataList" formDialogRef icon="search" type="primary">
|
||||
{{ $t('common.queryBtn') }}
|
||||
</el-button>
|
||||
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }} </el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button @click="formDialogRef.openDialog()" class="ml10" formDialogRef icon="folder-add" type="primary" v-auth="'sys_i18n_add'">
|
||||
{{ $t('common.addBtn') }}
|
||||
<el-button
|
||||
@click="formDialogRef.openDialog()"
|
||||
class="ml10"
|
||||
formDialogRef
|
||||
icon="folder-add"
|
||||
type="primary"
|
||||
v-auth="'sys_i18n_add'"
|
||||
>
|
||||
{{ $t("common.addBtn") }}
|
||||
</el-button>
|
||||
<el-button plain @click="handleRefreshCache()" class="ml10" icon="refresh-left" type="primary">
|
||||
{{ $t('common.refreshCacheBtn') }}
|
||||
{{ $t("common.refreshCacheBtn") }}
|
||||
</el-button>
|
||||
<el-button
|
||||
:disabled="multiple"
|
||||
@@ -37,7 +47,7 @@
|
||||
type="primary"
|
||||
v-auth="'sys_i18n_del'"
|
||||
>
|
||||
{{ $t('common.delBtn') }}
|
||||
{{ $t("common.delBtn") }}
|
||||
</el-button>
|
||||
|
||||
<right-toolbar
|
||||
@@ -55,7 +65,7 @@
|
||||
@selection-change="handleSelectionChange"
|
||||
@sort-change="sortChangeHandle"
|
||||
style="width: 100%"
|
||||
row-key="id"
|
||||
row-key="id"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
@@ -71,10 +81,10 @@
|
||||
<el-table-column :label="$t('common.action')" width="150">
|
||||
<template #default="scope">
|
||||
<el-button icon="edit-pen" @click="formDialogRef.openDialog(scope.row.id)" text type="primary" v-auth="'sys_i18n_edit'"
|
||||
>{{ $t('common.editBtn') }}
|
||||
>{{ $t("common.editBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="delete" @click="handleDelete([scope.row.id])" text type="primary" v-auth="'sys_i18n_del'">
|
||||
{{ $t('common.delBtn') }}
|
||||
{{ $t("common.delBtn") }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -88,80 +98,80 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="systemSysI18n" setup>
|
||||
import { BasicTableProps, useTable } from '/@/hooks/table';
|
||||
import { delObj, fetchList, refreshCache } from '/@/api/admin/i18n';
|
||||
import { useMessage, useMessageBox } from '/@/hooks/message';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import { delObj, fetchList, refreshCache } from "/@/api/admin/i18n"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { useI18n } from "vue-i18n"
|
||||
|
||||
// 引入组件
|
||||
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
|
||||
const { t } = useI18n();
|
||||
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
|
||||
const { t } = useI18n()
|
||||
// 定义查询字典
|
||||
|
||||
// 定义变量内容
|
||||
const formDialogRef = ref();
|
||||
const formDialogRef = ref()
|
||||
// 搜索变量
|
||||
const queryRef = ref();
|
||||
const showSearch = ref(true);
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
// 多选变量
|
||||
const selectObjs = ref([]) as any;
|
||||
const multiple = ref(true);
|
||||
const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {
|
||||
name: '',
|
||||
zhCn: '',
|
||||
en: '',
|
||||
name: "",
|
||||
zhCn: "",
|
||||
en: "",
|
||||
},
|
||||
pageList: fetchList,
|
||||
descs: ['create_time'],
|
||||
});
|
||||
descs: ["create_time"],
|
||||
})
|
||||
|
||||
// table hook
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state)
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
// 清空搜索条件
|
||||
queryRef.value.resetFields();
|
||||
queryRef.value.resetFields()
|
||||
// 清空排序规则
|
||||
state.queryForm!.descs = [];
|
||||
state.queryForm!.ascs = [];
|
||||
state.queryForm!.descs = []
|
||||
state.queryForm!.ascs = []
|
||||
// 清空多选
|
||||
selectObjs.value = [];
|
||||
getDataList();
|
||||
};
|
||||
selectObjs.value = []
|
||||
getDataList()
|
||||
}
|
||||
const handleRefreshCache = () => {
|
||||
refreshCache().then(() => {
|
||||
useMessage().success('同步成功');
|
||||
});
|
||||
};
|
||||
useMessage().success("同步成功")
|
||||
})
|
||||
}
|
||||
|
||||
// 导出excel
|
||||
const exportExcel = () => {
|
||||
downBlobFile('/admin/i18n/export', Object.assign(state.queryForm,{ids:selectObjs}), 'i18n.xlsx');
|
||||
};
|
||||
downBlobFile("/admin/i18n/export", Object.assign(state.queryForm, { ids: selectObjs }), "i18n.xlsx")
|
||||
}
|
||||
|
||||
// 多选事件
|
||||
const handleSelectionChange = (objs: { id: string }[]) => {
|
||||
selectObjs.value = objs.map(({ id }) => id);
|
||||
multiple.value = !objs.length;
|
||||
};
|
||||
selectObjs.value = objs.map(({ id }) => id)
|
||||
multiple.value = !objs.length
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (ids: string[]) => {
|
||||
try {
|
||||
await useMessageBox().confirm(t('common.delConfirmText'));
|
||||
await useMessageBox().confirm(t("common.delConfirmText"))
|
||||
} catch {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await delObj(ids);
|
||||
getDataList();
|
||||
useMessage().success(t('common.delSuccessText'));
|
||||
await delObj(ids)
|
||||
getDataList()
|
||||
useMessage().success(t("common.delSuccessText"))
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
useMessage().error(err.msg)
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@@ -5,7 +5,16 @@
|
||||
<log-line-chart />
|
||||
|
||||
<el-row class="mt-4 ml10" v-show="showSearch">
|
||||
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item :label="$t('syslog.logType')" prop="logType">
|
||||
<el-select :placeholder="$t('syslog.inputLogTypeTip')" clearable v-model="state.queryForm.logType">
|
||||
<el-option :key="item.value" :label="item.label" :value="item.value" v-for="item in log_type" />
|
||||
@@ -21,11 +30,7 @@
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="getDataList" icon="Search" type="primary">{{ $t("common.queryBtn") }}</el-button>
|
||||
<el-button @click="resetQuery" icon="Refresh">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row class="ml10" v-show="showSearch">
|
||||
<el-form :inline="true" :model="state.queryForm" ref="queryRef">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item :label="$t('param.publicName')" prop="publicName">
|
||||
<el-input :placeholder="$t('param.inputpublicNameTip')" style="max-width: 180px" v-model="state.queryForm.publicName" />
|
||||
</el-form-item>
|
||||
@@ -14,13 +23,7 @@
|
||||
<el-option :key="index" :label="item.label" :value="item.value" v-for="(item, index) in dict_type"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="getDataList" formDialogRef icon="search" type="primary">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button @click="resetQuery" formDialogRef icon="Refresh">{{ $t("common.resetBtn") }} </el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row v-show="showSearch">
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item label="敏感词" prop="sensitiveWord">
|
||||
<el-input placeholder="请输入敏感词" v-model="state.queryForm.sensitiveWord" />
|
||||
</el-form-item>
|
||||
@@ -11,11 +20,7 @@
|
||||
<el-radio :label="item.value" v-for="(item, index) in sensitive_type" border :key="index">{{ item.label }} </el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="getDataList"> 查询 </el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
|
||||
@@ -2,19 +2,22 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row v-show="showSearch">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item :label="t('social.type')" class="ml2" prop="type">
|
||||
<el-select v-model="state.queryForm.type" :placeholder="t('social.inputTypeTip')">
|
||||
<el-option v-for="(item, index) in social_type" :key="index" :label="item.label" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button formDialogRef icon="search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button formDialogRef icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
|
||||
@@ -2,20 +2,23 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row v-show="showSearch">
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item :label="t('area.pid')" prop="adcode">
|
||||
<china-area :type="3" :placeholder="t('area.inputPidByTip')" v-model="pid" :plus="true" @change="handleChange" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('area.name')" prop="name">
|
||||
<el-input :placeholder="t('area.inputNameByTip')" v-model="state.queryForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
|
||||
@@ -1,90 +1,115 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row v-show="showSearch">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-form-item style="width: 250px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="车牌号" prop="carNumber">
|
||||
<el-input v-model="state.queryForm.carNumber" placeholder="请输入车牌号" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="车辆类型" prop="carType">
|
||||
<dict-select v-model="state.queryForm.carType" dict-code="car_type" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="是否启用" prop="enableStatus">
|
||||
<dict-select v-model="state.queryForm.enableStatus" dict-code="yes_no_type" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">查询</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()"
|
||||
v-auth="'admin_sysUserCar_add'">
|
||||
新 增
|
||||
</el-button>
|
||||
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'admin_sysUserCar_del'"
|
||||
@click="handleDelete(selectObjs)">
|
||||
删除
|
||||
</el-button>
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="'admin_sysUserCar_export'" @exportExcel="exportExcel"
|
||||
class="ml10 mr20" style="float: right;" @queryTable="getDataList"></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table-column type="selection" width="40" align="center" />
|
||||
<el-table-column type="index" label="#" width="80" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="userDeptName" label="部门" />
|
||||
<el-table-column prop="carNumber" label="车牌号">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="primary">{{ row.carNumber }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="carType" label="车辆类型">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="car_type" :value="row.carType" />
|
||||
</template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row v-show="showSearch">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 250px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="车牌号" prop="carNumber">
|
||||
<el-input v-model="state.queryForm.carNumber" placeholder="请输入车牌号" />
|
||||
</el-form-item>
|
||||
|
||||
</el-table-column>
|
||||
<el-table-column prop="enableStatus" label="是否启用">
|
||||
<template #default="{ row }">
|
||||
<el-switch active-value="1" inactive-value="0" v-model="row.enableStatus"
|
||||
@change="enableStatusChange(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button icon="edit-pen" text type="primary" v-auth="'admin_sysUserCar_edit'"
|
||||
@click="formDialogRef.openDialog(scope.row.id)">编辑</el-button>
|
||||
<el-button icon="delete" text type="primary" v-auth="'admin_sysUserCar_del'"
|
||||
@click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
<el-form-item style="width: 250px" label="是否启用" prop="enableStatus">
|
||||
<dict-select v-model="state.queryForm.enableStatus" dict-code="yes_no_type" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
<template #more>
|
||||
<el-form-item style="width: 250px" label="车辆类型" prop="carType">
|
||||
<dict-select v-model="state.queryForm.carType" dict-code="car_type" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()" v-auth="'admin_sysUserCar_add'">
|
||||
新 增
|
||||
</el-button>
|
||||
<el-button
|
||||
plain
|
||||
:disabled="multiple"
|
||||
icon="Delete"
|
||||
type="primary"
|
||||
v-auth="'admin_sysUserCar_del'"
|
||||
@click="handleDelete(selectObjs)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'admin_sysUserCar_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="selection" width="40" align="center" />
|
||||
<el-table-column type="index" label="#" width="80" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="userDeptName" label="部门" />
|
||||
<el-table-column prop="carNumber" label="车牌号">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="primary">{{ row.carNumber }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="carType" label="车辆类型">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="car_type" :value="row.carType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="enableStatus" label="是否启用">
|
||||
<template #default="{ row }">
|
||||
<el-switch active-value="1" inactive-value="0" v-model="row.enableStatus" @change="enableStatusChange(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button icon="edit-pen" text type="primary" v-auth="'admin_sysUserCar_edit'" @click="formDialogRef.openDialog(scope.row.id)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button icon="delete" text type="primary" v-auth="'admin_sysUserCar_del'" @click="handleDelete([scope.row.id])"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="systemSysUserCar">
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table";
|
||||
import * as sysUserCarApi from "/@/api/admin/sysUserCar";
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message";
|
||||
import { useDict } from '/@/hooks/dict';
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import * as sysUserCarApi from "/@/api/admin/sysUserCar"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { useDict } from "/@/hooks/dict"
|
||||
|
||||
// 引入组件
|
||||
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
|
||||
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
|
||||
// 定义查询字典
|
||||
const { car_type } = useDict("car_type")
|
||||
// 定义变量内容
|
||||
@@ -97,65 +122,58 @@ const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {},
|
||||
pageList: sysUserCarApi.fetchList
|
||||
queryForm: {},
|
||||
pageList: sysUserCarApi.fetchList,
|
||||
})
|
||||
|
||||
// table hook
|
||||
const {
|
||||
getDataList,
|
||||
currentChangeHandle,
|
||||
sizeChangeHandle,
|
||||
sortChangeHandle,
|
||||
downBlobFile,
|
||||
tableStyle
|
||||
} = useTable(state)
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state)
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
// 清空搜索条件
|
||||
queryRef.value?.resetFields()
|
||||
// 清空多选
|
||||
selectObjs.value = []
|
||||
getDataList()
|
||||
// 清空搜索条件
|
||||
queryRef.value?.resetFields()
|
||||
// 清空多选
|
||||
selectObjs.value = []
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 导出excel
|
||||
const exportExcel = () => {
|
||||
downBlobFile('/admin/sysUserCar/export', Object.assign(state.queryForm, { ids: selectObjs }), 'sysUserCar.xlsx')
|
||||
downBlobFile("/admin/sysUserCar/export", Object.assign(state.queryForm, { ids: selectObjs }), "sysUserCar.xlsx")
|
||||
}
|
||||
|
||||
// 多选事件
|
||||
const selectionChangHandle = (objs: { id: string }[]) => {
|
||||
selectObjs.value = objs.map(({ id }) => id);
|
||||
multiple.value = !objs.length;
|
||||
};
|
||||
selectObjs.value = objs.map(({ id }) => id)
|
||||
multiple.value = !objs.length
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (ids: string[]) => {
|
||||
try {
|
||||
await useMessageBox().confirm('此操作将永久删除');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await useMessageBox().confirm("此操作将永久删除")
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await sysUserCarApi.delObjs(ids);
|
||||
getDataList();
|
||||
useMessage().success('删除成功');
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
}
|
||||
};
|
||||
try {
|
||||
await sysUserCarApi.delObjs(ids)
|
||||
getDataList()
|
||||
useMessage().success("删除成功")
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg)
|
||||
}
|
||||
}
|
||||
|
||||
// 修改启用状态
|
||||
async function enableStatusChange(row: any) {
|
||||
try {
|
||||
await sysUserCarApi.putObj(row)
|
||||
getDataList()
|
||||
useMessage().success("操作成功")
|
||||
} catch (e) {
|
||||
useMessage().error("操作错误")
|
||||
}
|
||||
try {
|
||||
await sysUserCarApi.putObj(row)
|
||||
getDataList()
|
||||
useMessage().success("操作成功")
|
||||
} catch (e) {
|
||||
useMessage().error("操作错误")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@@ -1,35 +1,46 @@
|
||||
<template>
|
||||
<el-dialog title="菜品统计" width="70%" v-model="visible" :close-on-click-modal="false" draggable>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="state.queryForm.name" placeholder="请输入姓名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label=" 手机号" prop="phone">
|
||||
<el-input v-model="state.queryForm.phone" placeholder="请输入手机号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">查询</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table height="450px" :data="state.dataList" v-loading="state.loading" border
|
||||
:cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
|
||||
:summary-method="getSummaries" :show-summary="true" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table-column type="selection" width="60" align="center" />
|
||||
<el-table-column label="用户名" prop="username" fixed="left" show-overflow-tooltip />
|
||||
<el-table-column label="姓名" prop="name" show-overflow-tooltip min-width="150" />
|
||||
<el-table-column label="手机号" prop="phone" show-overflow-tooltip min-width="150" />
|
||||
<el-table-column label="部门" prop="deptName" show-overflow-tooltip min-width="150" />
|
||||
<el-table-column label="编制" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="user_staffing_type" :value="row.staffingType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="菜品统计" width="70%" v-model="visible" :close-on-click-modal="false" draggable>
|
||||
<SearchPanel
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="state.queryForm.name" placeholder="请输入姓名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label=" 手机号" prop="phone">
|
||||
<el-input v-model="state.queryForm.phone" placeholder="请输入手机号" clearable />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
<el-table
|
||||
height="450px"
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
:summary-method="getSummaries"
|
||||
:show-summary="true"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="selection" width="60" align="center" />
|
||||
<el-table-column label="用户名" prop="username" fixed="left" show-overflow-tooltip />
|
||||
<el-table-column label="姓名" prop="name" show-overflow-tooltip min-width="150" />
|
||||
<el-table-column label="手机号" prop="phone" show-overflow-tooltip min-width="150" />
|
||||
<el-table-column label="部门" prop="deptName" show-overflow-tooltip min-width="150" />
|
||||
<el-table-column label="编制" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="user_staffing_type" :value="row.staffingType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="staffing-total-table">
|
||||
@@ -50,11 +61,11 @@ const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
createdIsNeed: false,
|
||||
queryForm: {
|
||||
deptId: null
|
||||
},
|
||||
pageList: userApi.pageList,
|
||||
createdIsNeed: false,
|
||||
queryForm: {
|
||||
deptId: null,
|
||||
},
|
||||
pageList: userApi.pageList,
|
||||
})
|
||||
|
||||
// table hook
|
||||
@@ -62,53 +73,51 @@ const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, ta
|
||||
|
||||
// 多选事件
|
||||
const selectionChangHandle = (objs: { id: string }[]) => {
|
||||
selectObjs.value = objs.map(({ id }) => id)
|
||||
multiple.value = !objs.length
|
||||
selectObjs.value = objs.map(({ id }) => id)
|
||||
multiple.value = !objs.length
|
||||
}
|
||||
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
queryRef.value?.resetFields()
|
||||
getDataList()
|
||||
queryRef.value?.resetFields()
|
||||
getDataList()
|
||||
}
|
||||
|
||||
|
||||
const openDialog = (row: any, query: any) => {
|
||||
console.log("row:", row);
|
||||
console.log("query:", query);
|
||||
state.queryForm.deptId = null
|
||||
console.log("row:", row)
|
||||
console.log("query:", query)
|
||||
state.queryForm.deptId = null
|
||||
|
||||
if (row && row.deptId) {
|
||||
state.queryForm.deptId = row.deptId
|
||||
}
|
||||
if (row && row.deptId) {
|
||||
state.queryForm.deptId = row.deptId
|
||||
}
|
||||
|
||||
getDataList()
|
||||
visible.value = true
|
||||
getDataList()
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const getSummaries = (param: any) => {
|
||||
const { columns, data } = param
|
||||
const totalColumns = ["count", "totalPrice"]
|
||||
const totalRow = Array.from({ length: columns.length })
|
||||
totalRow[0] = "合计"
|
||||
columns.forEach((i: any, index: number) => {
|
||||
if (totalColumns.includes(i.property)) {
|
||||
const total = data
|
||||
.filter((v: any) => v[i.property] != null)
|
||||
.map((v: any) => Number(v[i.property]))
|
||||
.reduce((pren: number, next: number) => pren + next, 0)
|
||||
if (i.property.includes("Price")) {
|
||||
totalRow[index] = displayPrice(total)
|
||||
} else {
|
||||
totalRow[index] = total
|
||||
}
|
||||
}
|
||||
})
|
||||
return totalRow
|
||||
const { columns, data } = param
|
||||
const totalColumns = ["count", "totalPrice"]
|
||||
const totalRow = Array.from({ length: columns.length })
|
||||
totalRow[0] = "合计"
|
||||
columns.forEach((i: any, index: number) => {
|
||||
if (totalColumns.includes(i.property)) {
|
||||
const total = data
|
||||
.filter((v: any) => v[i.property] != null)
|
||||
.map((v: any) => Number(v[i.property]))
|
||||
.reduce((pren: number, next: number) => pren + next, 0)
|
||||
if (i.property.includes("Price")) {
|
||||
totalRow[index] = displayPrice(total)
|
||||
} else {
|
||||
totalRow[index] = total
|
||||
}
|
||||
}
|
||||
})
|
||||
return totalRow
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openDialog,
|
||||
openDialog,
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
@@ -2,35 +2,46 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row shadow="hover" v-show="showSearch" class="ml10">
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="filter">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="filter"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item prop="deptName" :label="$t('sysdept.name')">
|
||||
<el-input :placeholder="$t('sysdept.inputdeptNameTip')" style="max-width: 180px"
|
||||
v-model="state.queryForm.deptName"></el-input>
|
||||
<el-input :placeholder="$t('sysdept.inputdeptNameTip')" style="max-width: 180px" v-model="state.queryForm.deptName"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="filter">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button @click="resetQuery" icon="Refresh">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="top-right-btn" v-if="!defaultTreeViewRef"
|
||||
v-auth="'sys_dept_add'" @click="handleAdd">
|
||||
<el-button
|
||||
icon="folder-add"
|
||||
type="primary"
|
||||
class="top-right-btn"
|
||||
v-if="!defaultTreeViewRef"
|
||||
v-auth="'sys_dept_add'"
|
||||
@click="handleAdd"
|
||||
>
|
||||
{{ $t("common.addBtn") }}
|
||||
</el-button>
|
||||
<el-button v-if="false" plain icon="upload-filled" type="primary" class="ml10"
|
||||
@click="excelUploadRef.show()">
|
||||
<el-button v-if="false" plain icon="upload-filled" type="primary" class="ml10" @click="excelUploadRef.show()">
|
||||
{{ $t("common.importBtn") }}
|
||||
</el-button>
|
||||
<el-button @click="handleExpand"> {{ $t("common.expandBtn") }}</el-button>
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="'sys_dept_add'" @exportExcel="exportExcel"
|
||||
class="ml10" style="float: right; margin-right: 20px" @queryTable="getDataList">
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'sys_dept_add'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10"
|
||||
style="float: right; margin-right: 20px"
|
||||
@queryTable="getDataList"
|
||||
>
|
||||
<el-tooltip class="item" effect="dark" :content="$t('queryTree.view')" placement="top">
|
||||
<el-button v-if="!stores.userInfos.roleCodes.includes('DEPT_ADMIN')" circle icon="Grid"
|
||||
@click="handleView" />
|
||||
<el-button v-if="!stores.userInfos.roleCodes.includes('DEPT_ADMIN')" circle icon="Grid" @click="handleView" />
|
||||
</el-tooltip>
|
||||
</right-toolbar>
|
||||
</div>
|
||||
@@ -39,8 +50,13 @@
|
||||
<tree-view ref="treeViewRef" v-if="defaultTreeViewRef" />
|
||||
<table-view ref="tableViewRef" v-if="!defaultTreeViewRef" />
|
||||
|
||||
<upload-excel ref="excelUploadRef" :title="$t('sysdept.importTip')" url="/admin/dept/import"
|
||||
temp-url="/admin/sys-file/local/file/dept.xlsx" @refreshDataList="getDataList" />
|
||||
<upload-excel
|
||||
ref="excelUploadRef"
|
||||
:title="$t('sysdept.importTip')"
|
||||
url="/admin/dept/import"
|
||||
temp-url="/admin/sys-file/local/file/dept.xlsx"
|
||||
@refreshDataList="getDataList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -54,7 +70,7 @@ const TreeView = defineAsyncComponent(() => import("./tree-view.vue"))
|
||||
const TableView = defineAsyncComponent(() => import("./table-view.vue"))
|
||||
|
||||
const { t } = useI18n()
|
||||
const stores = useUserInfo();
|
||||
const stores = useUserInfo()
|
||||
|
||||
// 默认树视图展示
|
||||
const defaultTreeViewRef = ref(true)
|
||||
|
||||
@@ -2,37 +2,51 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row v-show="showSearch" class="ml10">
|
||||
<el-form :inline="true" :model="state.queryForm" ref="queryRef">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item :label="$t('sysmenu.name')" prop="menuName">
|
||||
<el-input :placeholder="$t('sysmenu.inputNameTip')" clearable style="max-width: 180px"
|
||||
v-model="state.queryForm.menuName" />
|
||||
<el-input :placeholder="$t('sysmenu.inputNameTip')" clearable style="max-width: 180px" v-model="state.queryForm.menuName" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="query" class="ml10" icon="search" type="primary">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button @click="resetQuery" icon="Refresh">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button @click="onOpenAddMenu" class="ml10" icon="folder-add" type="primary"
|
||||
v-auth="'sys_menu_add'">
|
||||
<el-button @click="onOpenAddMenu" class="ml10" icon="folder-add" type="primary" v-auth="'sys_menu_add'">
|
||||
{{ $t("common.addBtn") }}
|
||||
</el-button>
|
||||
<right-toolbar v-model:showSearch="showSearch" class="ml10" style="float: right; margin-right: 20px"
|
||||
@queryTable="query"></right-toolbar>
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
class="ml10"
|
||||
style="float: right; margin-right: 20px"
|
||||
@queryTable="query"
|
||||
></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table class="el-table" ref="tableRef" :data="tableList" row-key="id" style="width: 100%"
|
||||
v-loading="state.loading" border :header-cell-style="tableStyle?.headerCellStyle"
|
||||
:expand-row-keys="expandRowKeys" lazy :load="load"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }" @expand-change="handleExpandChange">
|
||||
<el-table-column :label="$t('sysmenu.name')" fixed prop="name" align="left" show-overflow-tooltip
|
||||
min-width="200" />
|
||||
<el-table-column :label="$t('sysmenu.sortOrder')" prop="sortOrder" align="center"
|
||||
show-overflow-tooltip />
|
||||
<el-table
|
||||
class="el-table"
|
||||
ref="tableRef"
|
||||
:data="tableList"
|
||||
row-key="id"
|
||||
style="width: 100%"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:header-cell-style="tableStyle?.headerCellStyle"
|
||||
:expand-row-keys="expandRowKeys"
|
||||
lazy
|
||||
:load="load"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
@expand-change="handleExpandChange"
|
||||
>
|
||||
<el-table-column :label="$t('sysmenu.name')" fixed prop="name" align="left" show-overflow-tooltip min-width="200" />
|
||||
<el-table-column :label="$t('sysmenu.sortOrder')" prop="sortOrder" align="center" show-overflow-tooltip />
|
||||
<el-table-column :label="$t('sysmenu.icon')" prop="icon" show-overflow-tooltip align="center">
|
||||
<template #default="scope">
|
||||
<SvgIcon :name="scope.row.meta.icon" />
|
||||
@@ -52,23 +66,26 @@
|
||||
<el-tag type="info" v-else>关闭</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('sysmenu.permission')" :show-overflow-tooltip="true" prop="permission"
|
||||
align="center" />
|
||||
<el-table-column :label="$t('sysmenu.permission')" :show-overflow-tooltip="true" prop="permission" align="center" />
|
||||
<el-table-column :label="$t('common.action')" show-overflow-tooltip width="250" align="center">
|
||||
<template #default="scope">
|
||||
<el-button icon="folder-add" @click="onOpenAddMenu('add', scope.row)" text type="primary"
|
||||
v-auth="'sys_menu_add'">
|
||||
<el-button icon="folder-add" @click="onOpenAddMenu('add', scope.row)" text type="primary" v-auth="'sys_menu_add'">
|
||||
{{ $t("common.addBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="edit-pen" @click="onOpenEditMenu('edit', scope.row)" text type="primary"
|
||||
v-auth="'sys_menu_edit'">{{ $t("common.editBtn") }}
|
||||
<el-button icon="edit-pen" @click="onOpenEditMenu('edit', scope.row)" text type="primary" v-auth="'sys_menu_edit'"
|
||||
>{{ $t("common.editBtn") }}
|
||||
</el-button>
|
||||
|
||||
<el-tooltip icon="delete" :content="$t('sysmenu.deleteDisabledTip')"
|
||||
:disabled="!deleteMenuDisabled(scope.row)" placement="top">
|
||||
<el-tooltip icon="delete" :content="$t('sysmenu.deleteDisabledTip')" :disabled="!deleteMenuDisabled(scope.row)" placement="top">
|
||||
<span style="margin-left: 12px">
|
||||
<el-button icon="delete" :disabled="deleteMenuDisabled(scope.row)"
|
||||
@click="handleDelete(scope.row)" text type="primary" v-auth="'sys_menu_del'">
|
||||
<el-button
|
||||
icon="delete"
|
||||
:disabled="deleteMenuDisabled(scope.row)"
|
||||
@click="handleDelete(scope.row)"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'sys_menu_del'"
|
||||
>
|
||||
{{ $t("common.delBtn") }}
|
||||
</el-button>
|
||||
</span>
|
||||
@@ -211,7 +228,7 @@ const restoreExpandedRows = async () => {
|
||||
row.children = childrenList
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载子节点失败:', error)
|
||||
console.error("加载子节点失败:", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +238,7 @@ const restoreExpandedRows = async () => {
|
||||
// 等待 DOM 更新,确保子节点渲染完成
|
||||
await nextTick()
|
||||
} catch (error) {
|
||||
console.error('展开节点失败:', error)
|
||||
console.error("展开节点失败:", error)
|
||||
}
|
||||
|
||||
// 如果有子节点,递归处理
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row class="ml10" v-show="showSearch">
|
||||
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item :label="$t('post.postName')" prop="postName">
|
||||
<el-input :placeholder="$t('post.inputpostNameTip')" style="max-width: 180px" v-model="state.queryForm.postName" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="getDataList" icon="search" type="primary">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button @click="resetQuery" icon="Refresh">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row shadow="hover" v-show="showSearch" class="ml10">
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item :label="$t('sysrole.roleName')" prop="roleName">
|
||||
<el-input :placeholder="$t('sysrole.inputRoleNameTip')" v-model="state.queryForm.roleName" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
|
||||
@@ -2,59 +2,85 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 250px" label="调动用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="调动类型" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="user_transaction" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="自动转账到消费钱包" prop="isWalletTransaction">
|
||||
<dict-select v-model="state.queryForm.isWalletTransaction" dict-code="yes_no_type" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="width: 450px" label="申请时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="datetimerange" />
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="审核时间" prop="processTimeList">
|
||||
<el-date-picker v-model="state.queryForm.processTimeList" value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="datetimerange" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="自动转账时间" prop="walletTransactionTimeList">
|
||||
<el-date-picker v-model="state.queryForm.walletTransactionTimeList"
|
||||
value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #more>
|
||||
<el-form-item style="width: 300px" label="自动转账到消费钱包" prop="isWalletTransaction">
|
||||
<dict-select v-model="state.queryForm.isWalletTransaction" dict-code="yes_no_type" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="审核时间" prop="processTimeList">
|
||||
<el-date-picker v-model="state.queryForm.processTimeList" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="自动转账时间" prop="walletTransactionTimeList">
|
||||
<el-date-picker v-model="state.queryForm.walletTransactionTimeList" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()"
|
||||
v-auth="'admin_sysUserTransaction_add'">
|
||||
<el-button
|
||||
icon="folder-add"
|
||||
type="primary"
|
||||
class="ml10"
|
||||
@click="formDialogRef.openDialog()"
|
||||
v-auth="'admin_sysUserTransaction_add'"
|
||||
>
|
||||
新 增
|
||||
</el-button>
|
||||
<el-button plain :disabled="multiple" icon="Delete" type="primary"
|
||||
v-auth="'admin_sysUserTransaction_del'" @click="handleDelete(selectObjs)">
|
||||
<el-button
|
||||
plain
|
||||
:disabled="multiple"
|
||||
icon="Delete"
|
||||
type="primary"
|
||||
v-auth="'admin_sysUserTransaction_del'"
|
||||
@click="handleDelete(selectObjs)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<div class="flex justify-between items-center">
|
||||
<el-tabs class="flex-1" v-model="activeName" @tab-click="handleClickTabItem">
|
||||
<el-tab-pane v-for="v in tabItems" :key="v.id" :label="v.label" :name="v.value" />
|
||||
</el-tabs>
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="'admin_sysUserTransaction_export'"
|
||||
@exportExcel="exportExcel" class="ml10 mr20" style="float: right"
|
||||
@queryTable="getDataList" />
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'admin_sysUserTransaction_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="selection" width="40" align="center" />
|
||||
<el-table-column type="index" label="#" width="80" />
|
||||
<el-table-column prop="applyUsername" label="申请人" />
|
||||
@@ -89,26 +115,47 @@
|
||||
<span v-else class="color-danger">未审核</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="250"
|
||||
v-auths="['admin_sysUserTransaction_edit', 'admin_sysUserTransaction_check', 'admin_sysUserTransaction_del']">
|
||||
<el-table-column
|
||||
label="操作"
|
||||
fixed="right"
|
||||
width="250"
|
||||
v-auths="['admin_sysUserTransaction_edit', 'admin_sysUserTransaction_check', 'admin_sysUserTransaction_del']"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button icon="edit-pen" v-if="row.processStatus == 1" text type="primary"
|
||||
v-auth="'admin_sysUserTransaction_edit'" @click="formDialogRef.openDialog(row.id)">
|
||||
<el-button
|
||||
icon="edit-pen"
|
||||
v-if="row.processStatus == 1"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'admin_sysUserTransaction_edit'"
|
||||
@click="formDialogRef.openDialog(row.id)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button icon="VideoPlay" v-if="row.processStatus == 1" text type="primary"
|
||||
v-auth="'admin_sysUserTransaction_check'" @click="deal(row)">
|
||||
<el-button
|
||||
icon="VideoPlay"
|
||||
v-if="row.processStatus == 1"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'admin_sysUserTransaction_check'"
|
||||
@click="deal(row)"
|
||||
>
|
||||
开始处理
|
||||
</el-button>
|
||||
<el-button icon="delete" v-if="row.processStatus == 1" text type="primary"
|
||||
v-auth="'admin_sysUserTransaction_del'" @click="handleDelete([row.id])">
|
||||
<el-button
|
||||
icon="delete"
|
||||
v-if="row.processStatus == 1"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'admin_sysUserTransaction_del'"
|
||||
@click="handleDelete([row.id])"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
<!-- 审批抽屉 -->
|
||||
<process-drawer ref="processDrawerRef" @task-submit-event="taskSubmitEvent" />
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false">
|
||||
<DraggableDepTree
|
||||
ref="draggableDepTreeRef"
|
||||
placeholder="请输入部门名称"
|
||||
:show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode"
|
||||
switch-inactive-text="包含"
|
||||
switch-active-text="不包含"
|
||||
:defaultTreeWidth="20"
|
||||
:nodeClickable="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-row v-show="showSearch">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 250px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" clearable />
|
||||
</el-form-item>
|
||||
@@ -16,35 +32,47 @@
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="state.queryForm.phone" placeholder="请输入手机号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="部门" prop="deptId">
|
||||
<select-dept v-model="state.queryForm.deptId" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="角色" prop="role">
|
||||
<select-role v-model="state.queryForm.role" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="编制情况" prop="staffingType">
|
||||
<dict-select v-model="state.queryForm.staffingType" dict-code="user_staffing_type" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">查询</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #more>
|
||||
<el-form-item style="width: 250px" label="部门" prop="deptId">
|
||||
<select-dept v-model="state.queryForm.deptId" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="角色" prop="role">
|
||||
<select-role v-model="state.queryForm.role" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="编制情况" prop="staffingType">
|
||||
<dict-select v-model="state.queryForm.staffingType" dict-code="user_staffing_type" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button v-auth="'sys_user_add'" icon="folder-add" type="primary"
|
||||
@click="userDialogRef.openDialog()">新增</el-button>
|
||||
<el-button plain v-auth="'sys_user_export'" class="ml10" icon="upload-filled" type="primary"
|
||||
@click="excelUploadRef.show()">
|
||||
<el-button v-auth="'sys_user_add'" icon="folder-add" type="primary" @click="userDialogRef.openDialog()">新增</el-button>
|
||||
<el-button plain v-auth="'sys_user_export'" class="ml10" icon="upload-filled" type="primary" @click="excelUploadRef.show()">
|
||||
导入
|
||||
</el-button>
|
||||
<el-button plain v-auth="'sys_user_del'" :disabled="multiple" class="ml10" icon="Delete"
|
||||
type="primary" @click="handleDelete(selectObjs)">
|
||||
<el-button
|
||||
plain
|
||||
v-auth="'sys_user_del'"
|
||||
:disabled="multiple"
|
||||
class="ml10"
|
||||
icon="Delete"
|
||||
type="primary"
|
||||
@click="handleDelete(selectObjs)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<el-button plain v-auth="'sys_user_del'" v-if="activeName == '1'" :disabled="multiple"
|
||||
class="ml10" icon="VideoPlay" type="primary" @click="doBatchDeal">
|
||||
<el-button
|
||||
plain
|
||||
v-auth="'sys_user_del'"
|
||||
v-if="activeName == '1'"
|
||||
:disabled="multiple"
|
||||
class="ml10"
|
||||
icon="VideoPlay"
|
||||
type="primary"
|
||||
@click="doBatchDeal"
|
||||
>
|
||||
审批
|
||||
</el-button>
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -52,15 +80,26 @@
|
||||
<el-tab-pane v-for="v in tabItems" :key="v.id" :label="v.label" :name="v.value" />
|
||||
</el-tabs>
|
||||
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="'sys_user_export'"
|
||||
@exportExcel="exportExcel" @queryTable="getDataList" class="ml10"
|
||||
style="float: right" />
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'sys_user_export'"
|
||||
@exportExcel="exportExcel"
|
||||
@queryTable="getDataList"
|
||||
class="ml10"
|
||||
style="float: right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table v-loading="state.loading" :data="state.dataList" @selection-change="handleSelectionChange"
|
||||
row-key="userId" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle">
|
||||
<el-table
|
||||
v-loading="state.loading"
|
||||
:data="state.dataList"
|
||||
@selection-change="handleSelectionChange"
|
||||
row-key="userId"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
>
|
||||
<el-table-column :selectable="handleSelectable" type="selection" width="40" />
|
||||
<el-table-column label="#" type="index" width="60" fixed="left" />
|
||||
<el-table-column label="用户名" prop="username" fixed="left" show-overflow-tooltip />
|
||||
@@ -69,9 +108,13 @@
|
||||
<el-table-column label="部门" prop="deptName" show-overflow-tooltip min-width="150" />
|
||||
<el-table-column label="启用" show-overflow-tooltip min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-switch :disabled="!auth('sys_user_edit') || row.processStatus != '2'"
|
||||
v-model="row.lockFlag" @change="changeSwitch(row)" active-value="0"
|
||||
inactive-value="9"></el-switch>
|
||||
<el-switch
|
||||
:disabled="!auth('sys_user_edit') || row.processStatus != '2'"
|
||||
v-model="row.lockFlag"
|
||||
@change="changeSwitch(row)"
|
||||
active-value="0"
|
||||
inactive-value="9"
|
||||
></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="编制" min-width="80">
|
||||
@@ -81,8 +124,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="角色" show-overflow-tooltip min-width="150">
|
||||
<template #default="scope">
|
||||
<el-tag v-for="(item, index) in scope.row.roleList" :key="index">
|
||||
{{ item.roleName }}</el-tag>
|
||||
<el-tag v-for="(item, index) in scope.row.roleList" :key="index"> {{ item.roleName }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="人脸">
|
||||
@@ -92,44 +134,68 @@
|
||||
</el-table-column>
|
||||
<el-table-column v-if="false" label="全局人脸" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-switch :disabled="!auth('userFace_sysUserFace_global_face')" v-if="row.isGlobalFace"
|
||||
v-model="row.isGlobalFace" @change="changeGlobalFaceSwitch(row)" active-value="1"
|
||||
inactive-value="0" />
|
||||
<el-switch
|
||||
:disabled="!auth('userFace_sysUserFace_global_face')"
|
||||
v-if="row.isGlobalFace"
|
||||
v-model="row.isGlobalFace"
|
||||
@change="changeGlobalFaceSwitch(row)"
|
||||
active-value="1"
|
||||
inactive-value="0"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="所属商户" prop="merchantName" show-overflow-tooltip min-width="150" />
|
||||
<el-table-column v-auths="['sys_user_edit', 'userFace_sysUserFace_view', 'sys_user_del']"
|
||||
label="操作" min-width="300" width="300" fixed="right">
|
||||
<el-table-column
|
||||
v-auths="['sys_user_edit', 'userFace_sysUserFace_view', 'sys_user_del']"
|
||||
label="操作"
|
||||
min-width="300"
|
||||
width="300"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center">
|
||||
<!-- 重置密码 -->
|
||||
<popover-input v-if="row.processStatus == '2'" v-model="inputPassword"
|
||||
@confirm="changePassword(row)">
|
||||
<popover-input v-if="row.processStatus == '2'" v-model="inputPassword" @confirm="changePassword(row)">
|
||||
<template #default>
|
||||
<el-button v-auth="'sys_user_edit'" icon="RefreshLeft" text type="primary"
|
||||
class="mr-4">密码</el-button>
|
||||
<el-button v-auth="'sys_user_edit'" icon="RefreshLeft" text type="primary" class="mr-4">密码</el-button>
|
||||
</template>
|
||||
</popover-input>
|
||||
<!-- 修改信息 -->
|
||||
<el-button v-if="row.processStatus == '2'" v-auth="'sys_user_edit'" icon="edit-pen"
|
||||
text type="primary" @click="userDialogRef.openDialog(row.userId)">
|
||||
<el-button
|
||||
v-if="row.processStatus == '2'"
|
||||
v-auth="'sys_user_edit'"
|
||||
icon="edit-pen"
|
||||
text
|
||||
type="primary"
|
||||
@click="userDialogRef.openDialog(row.userId)"
|
||||
>
|
||||
修改
|
||||
</el-button>
|
||||
<!-- 人脸管理 -->
|
||||
<el-button v-if="row.processStatus == '2'" v-auth="'userFace_sysUserFace_view'"
|
||||
icon="User" text type="primary" @click="faceManageRef.openDialog(row.userId)">
|
||||
<el-button
|
||||
v-if="row.processStatus == '2'"
|
||||
v-auth="'userFace_sysUserFace_view'"
|
||||
icon="User"
|
||||
text
|
||||
type="primary"
|
||||
@click="faceManageRef.openDialog(row.userId)"
|
||||
>
|
||||
人脸管理
|
||||
</el-button>
|
||||
<el-button v-if="row.processStatus == 1" type="primary" size="small" link
|
||||
icon="VideoPlay" @click="deal(row)">
|
||||
<el-button v-if="row.processStatus == 1" type="primary" size="small" link icon="VideoPlay" @click="deal(row)">
|
||||
开始处理
|
||||
</el-button>
|
||||
<!-- 删除用户 -->
|
||||
<el-tooltip content="admin 不允许被删除" :disabled="row.userId !== '1'" placement="top">
|
||||
<span style="margin-left: 12px">
|
||||
<el-button icon="delete" v-auth="'sys_user_del'"
|
||||
:disabled="row.username === 'admin'" text type="primary"
|
||||
@click="handleDelete([row.userId])">
|
||||
<el-button
|
||||
icon="delete"
|
||||
v-auth="'sys_user_del'"
|
||||
:disabled="row.username === 'admin'"
|
||||
text
|
||||
type="primary"
|
||||
@click="handleDelete([row.userId])"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</span>
|
||||
@@ -138,16 +204,20 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination v-bind="state.pagination" @current-change="currentChangeHandle"
|
||||
@size-change="sizeChangeHandle" />
|
||||
<pagination v-bind="state.pagination" @current-change="currentChangeHandle" @size-change="sizeChangeHandle" />
|
||||
</template>
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
|
||||
<user-form ref="userDialogRef" @refresh="getDataList(false)" :deptId="state.queryForm.deptId" />
|
||||
<face-manage ref="faceManageRef" @refresh="getDataList(false)" />
|
||||
<upload-excel ref="excelUploadRef" title="用户导入" temp-url="/admin/sys-file/local/file/user.xlsx"
|
||||
url="/admin/user/import-face" @refreshDataList="getDataList" />
|
||||
<upload-excel
|
||||
ref="excelUploadRef"
|
||||
title="用户导入"
|
||||
temp-url="/admin/sys-file/local/file/user.xlsx"
|
||||
url="/admin/user/import-face"
|
||||
@refreshDataList="getDataList"
|
||||
/>
|
||||
|
||||
<!-- 审批抽屉 -->
|
||||
<process-drawer ref="processDrawerRef" @task-submit-event="taskSubmitEvent" />
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false">
|
||||
<DraggableDepTree
|
||||
ref="draggableDepTreeRef"
|
||||
placeholder="请输入部门名称"
|
||||
:show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode"
|
||||
switch-inactive-text="包含"
|
||||
switch-active-text="不包含"
|
||||
:defaultTreeWidth="20"
|
||||
:nodeClickable="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-row v-show="showSearch">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 250px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" clearable />
|
||||
</el-form-item>
|
||||
@@ -16,20 +32,41 @@
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="state.queryForm.phone" placeholder="请输入手机号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">查询</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
<el-form-item style="width: 180px" label="人脸质量" prop="qualityCategory">
|
||||
<el-select v-model="state.queryForm.qualityCategory" placeholder="全部" clearable>
|
||||
<el-option
|
||||
v-for="option in FACE_QUALITY_FILTER_OPTIONS"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button plain v-auth="'userFace_sysUserFace_del'" :disabled="multiple" class="ml10"
|
||||
icon="Delete" type="primary" @click="handleDelete(selectIds)">
|
||||
<el-button
|
||||
plain
|
||||
v-auth="'userFace_sysUserFace_del'"
|
||||
:disabled="multiple"
|
||||
class="ml10"
|
||||
icon="Delete"
|
||||
type="primary"
|
||||
@click="handleDelete(selectIds)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<el-button plain v-auth="'sys_user_del'" v-if="activeName == '1'" :disabled="multiple"
|
||||
class="ml10" icon="VideoPlay" type="primary" @click="doBatchDeal">
|
||||
<el-button
|
||||
plain
|
||||
v-auth="'sys_user_del'"
|
||||
v-if="activeName == '1'"
|
||||
:disabled="multiple"
|
||||
class="ml10"
|
||||
icon="VideoPlay"
|
||||
type="primary"
|
||||
@click="doBatchDeal"
|
||||
>
|
||||
审批
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -37,9 +74,15 @@
|
||||
<el-tabs class="flex-1" v-model="activeName" @tab-click="handleClickTabItem">
|
||||
<el-tab-pane v-for="v in tabItems" :key="v.id" :label="v.label" :name="v.value" />
|
||||
</el-tabs>
|
||||
<el-table v-loading="state.loading" :data="state.dataList" row-key="userId"
|
||||
@selection-change="handleSelectionChange" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle">
|
||||
<el-table
|
||||
v-loading="state.loading"
|
||||
:data="state.dataList"
|
||||
row-key="userId"
|
||||
@selection-change="handleSelectionChange"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
>
|
||||
<el-table-column :selectable="handleSelectable" type="selection" width="40" />
|
||||
<el-table-column label="#" type="index" width="60" fixed="left" />
|
||||
<el-table-column label="姓名" prop="username" fixed="left" show-overflow-tooltip />
|
||||
@@ -50,6 +93,27 @@
|
||||
<preview-image :src="row.faceImg" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="人脸质量" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="getFaceQualityTooltip(row)" placement="top" :show-after="250">
|
||||
<span
|
||||
tabindex="0"
|
||||
role="status"
|
||||
:aria-label="getFaceQualityTooltip(row)"
|
||||
class="inline-flex cursor-help rounded-sm focus:outline-none focus:ring-2 focus:ring-primary-light-5"
|
||||
>
|
||||
<el-tag
|
||||
:type="getFaceQualityTagType(row)"
|
||||
size="small"
|
||||
effect="plain"
|
||||
class="min-w-[112px] justify-center whitespace-nowrap tabular-nums"
|
||||
>
|
||||
{{ getFaceQualityLabel(row) }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核状态" prop="processStatus" show-overflow-tooltip min-width="100">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="check_status" :value="row.processStatus" />
|
||||
@@ -59,17 +123,21 @@
|
||||
<el-table-column label="审核时间" prop="processTime" show-overflow-tooltip min-width="160" />
|
||||
<el-table-column label="操作" min-width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" v-if="row.isCheckUser && row.processStatus != '2'"
|
||||
size="small" link icon="VideoPlay" @click="deal(row)">
|
||||
<el-button
|
||||
type="primary"
|
||||
v-if="row.isCheckUser && row.processStatus != '2'"
|
||||
size="small"
|
||||
link
|
||||
icon="VideoPlay"
|
||||
@click="deal(row)"
|
||||
>
|
||||
开始处理
|
||||
</el-button>
|
||||
<el-button v-auth="'userFace_sysUserFace_del'" link type="danger" icon="Delete"
|
||||
@click="delFace(row)">删除</el-button>
|
||||
<el-button v-auth="'userFace_sysUserFace_del'" link type="danger" icon="Delete" @click="delFace(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination v-bind="state.pagination" @current-change="currentChangeHandle"
|
||||
@size-change="sizeChangeHandle" />
|
||||
<pagination v-bind="state.pagination" @current-change="currentChangeHandle" @size-change="sizeChangeHandle" />
|
||||
</template>
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
@@ -104,6 +172,104 @@ const processDrawerRef = ref()
|
||||
const batchDealRef = ref()
|
||||
const loading = ref()
|
||||
|
||||
/** PC 当前阶段用于判定低质量人脸的固定阈值,与终端默认在库阈值保持一致。 */
|
||||
const LIBRARY_FACE_QUALITY_THRESHOLD = 0.6
|
||||
|
||||
/** 人脸质量查询分类,与后端分页接口的 qualityCategory 协议值保持一致。 */
|
||||
const FACE_QUALITY_FILTER_OPTIONS = [
|
||||
{ label: "合格", value: "qualified" },
|
||||
{ label: "低质量", value: "low" },
|
||||
{ label: "检测失败", value: "failed" },
|
||||
{ label: "未检测", value: "pending" },
|
||||
] as const
|
||||
|
||||
/** Element Plus 小号状态标签允许使用的语义类型。 */
|
||||
type FaceQualityTagType = "success" | "warning" | "danger" | "info"
|
||||
|
||||
/** 审批列表中与服务端人脸质量字段有关的最小行结构。 */
|
||||
interface UserFaceQualityRow {
|
||||
/** 虹软综合质量分,数据库 decimal 可能由不同网关序列化为数字或文本。 */
|
||||
qualityScore?: number | string | null
|
||||
/** 服务端质量状态;历史未检测记录为空。 */
|
||||
qualityStatus?: string | null
|
||||
/** 终端完成质量检测的时间。 */
|
||||
qualityCheckedAt?: string | null
|
||||
/** 虹软 SDK 或终端插件错误码。 */
|
||||
qualityErrorCode?: number | string | null
|
||||
/** 最后一次成功写入质量结果的终端设备主键。 */
|
||||
qualityDeviceId?: number | string | null
|
||||
}
|
||||
|
||||
/** 人脸质量单元格的紧凑展示模型。 */
|
||||
interface FaceQualityDisplay {
|
||||
/** 标签内可直接理解的状态与分数文案。 */
|
||||
label: string
|
||||
/** 与状态对应的 Element Plus 语义颜色。 */
|
||||
tagType: FaceQualityTagType
|
||||
/** 包含阈值、时间、来源和排查动作的完整辅助说明。 */
|
||||
tooltip: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 将服务端人脸质量字段归一化为不依赖颜色的表格状态。
|
||||
*
|
||||
* @param row 审批列表人脸记录
|
||||
* @returns 标签文案、语义颜色和完整诊断说明
|
||||
*/
|
||||
const resolveFaceQuality = (row: UserFaceQualityRow): FaceQualityDisplay => {
|
||||
const checkedAt = row.qualityCheckedAt || "未知"
|
||||
const deviceId = row.qualityDeviceId ?? "未知"
|
||||
if (row.qualityStatus === "detected") {
|
||||
const score = Number(row.qualityScore)
|
||||
if (row.qualityScore == null || row.qualityScore === "" || !Number.isFinite(score) || score < 0 || score > 1) {
|
||||
return {
|
||||
label: "数据异常",
|
||||
tagType: "danger",
|
||||
tooltip: `服务端质量状态为已检测,但质量分无效。检测时间:${checkedAt};来源设备:${deviceId}`,
|
||||
}
|
||||
}
|
||||
const scoreText = score.toFixed(2)
|
||||
if (score < LIBRARY_FACE_QUALITY_THRESHOLD) {
|
||||
return {
|
||||
label: `低质量 ${scoreText}`,
|
||||
tagType: "warning",
|
||||
tooltip: `质量分 ${scoreText},低于当前阈值 ${LIBRARY_FACE_QUALITY_THRESHOLD.toFixed(
|
||||
2,
|
||||
)}。请排查照片清晰度、光照、人脸角度及人脸区域大小。检测时间:${checkedAt};来源设备:${deviceId}`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: `合格 ${scoreText}`,
|
||||
tagType: "success",
|
||||
tooltip: `质量分 ${scoreText},达到当前阈值 ${LIBRARY_FACE_QUALITY_THRESHOLD.toFixed(
|
||||
2,
|
||||
)}。检测时间:${checkedAt};来源设备:${deviceId}`,
|
||||
}
|
||||
}
|
||||
if (row.qualityStatus === "failed") {
|
||||
const errorCode = row.qualityErrorCode == null ? "未知" : row.qualityErrorCode
|
||||
return {
|
||||
label: "检测失败",
|
||||
tagType: "danger",
|
||||
tooltip: `质量检测失败,错误码:${errorCode}。请确认终端虹软质量授权和本地图片有效性。检测时间:${checkedAt};来源设备:${deviceId}`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: "未检测",
|
||||
tagType: "info",
|
||||
tooltip: "尚未收到终端质量检测结果,可在终端人脸管理中执行存量检测。",
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回人脸质量标签文案。 */
|
||||
const getFaceQualityLabel = (row: UserFaceQualityRow) => resolveFaceQuality(row).label
|
||||
|
||||
/** 返回人脸质量标签语义类型。 */
|
||||
const getFaceQualityTagType = (row: UserFaceQualityRow) => resolveFaceQuality(row).tagType
|
||||
|
||||
/** 返回人脸质量 Tooltip 与无障碍说明。 */
|
||||
const getFaceQualityTooltip = (row: UserFaceQualityRow) => resolveFaceQuality(row).tooltip
|
||||
|
||||
// 多选rows
|
||||
const selectObjs = ref([]) as any
|
||||
const selectIds = ref([]) as any
|
||||
@@ -116,6 +282,7 @@ const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
deptId: "",
|
||||
username: "",
|
||||
phone: "",
|
||||
qualityCategory: "",
|
||||
isDeepDept: false,
|
||||
processStatus: "2",
|
||||
},
|
||||
|
||||
@@ -2,23 +2,35 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row class="ml10" v-show="showSearch">
|
||||
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item :label="$t('systoken.username')" prop="username">
|
||||
<el-input :placeholder="$t('systoken.inputUsernameTip')" v-model="state.queryForm.username"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户id" prop="username">
|
||||
<el-input :placeholder="$t('systoken.inputUsernameTip')" v-model="state.queryForm.userId"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="getDataList" icon="Search" type="primary">{{ $t('common.queryBtn') }} </el-button>
|
||||
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
|
||||
<el-form-item label="用户id" prop="username">
|
||||
<el-input :placeholder="$t('systoken.inputUsernameTip')" v-model="state.queryForm.userId"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button :disabled="multiple" @click="handleDelete(selectObjs)" class="ml10" icon="Delete" type="primary" v-auth="'sys_user_del'">
|
||||
{{ $t('systoken.offlineBtn') }}
|
||||
<el-button
|
||||
:disabled="multiple"
|
||||
@click="handleDelete(selectObjs)"
|
||||
class="ml10"
|
||||
icon="Delete"
|
||||
type="primary"
|
||||
v-auth="'sys_user_del'"
|
||||
>
|
||||
{{ $t("systoken.offlineBtn") }}
|
||||
</el-button>
|
||||
<right-toolbar
|
||||
@queryTable="getDataList"
|
||||
@@ -52,8 +64,15 @@
|
||||
<el-table-column :label="$t('systoken.expiresAt')" prop="expiresAt" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column :label="$t('common.action')" width="100">
|
||||
<template #default="scope">
|
||||
<el-button icon="delete" @click="handleDelete([scope.row.accessToken])" size="small" text type="primary" v-auth="'sys_user_del'">
|
||||
{{ $t('systoken.offlineBtn') }}
|
||||
<el-button
|
||||
icon="delete"
|
||||
@click="handleDelete([scope.row.accessToken])"
|
||||
size="small"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'sys_user_del'"
|
||||
>
|
||||
{{ $t("systoken.offlineBtn") }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -65,62 +84,62 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { BasicTableProps, useTable } from '/@/hooks/table';
|
||||
import { delObj, fetchList } from '/@/api/admin/token';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMessage, useMessageBox } from '/@/hooks/message';
|
||||
import { Session } from '/@/utils/storage';
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import { delObj, fetchList } from "/@/api/admin/token"
|
||||
import { useI18n } from "vue-i18n"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { Session } from "/@/utils/storage"
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
// 定义变量内容
|
||||
const queryRef = ref();
|
||||
const showSearch = ref(true);
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
// 多选rows
|
||||
const selectObjs = ref([]) as any;
|
||||
const selectObjs = ref([]) as any
|
||||
// 是否可以多选
|
||||
const multiple = ref(true);
|
||||
const multiple = ref(true)
|
||||
|
||||
// table hook
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {
|
||||
username: '',
|
||||
userId: '',
|
||||
username: "",
|
||||
userId: "",
|
||||
},
|
||||
pageList: fetchList,
|
||||
});
|
||||
const { getDataList, currentChangeHandle, sortChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
|
||||
})
|
||||
const { getDataList, currentChangeHandle, sortChangeHandle, sizeChangeHandle, tableStyle } = useTable(state)
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
queryRef.value?.resetFields();
|
||||
getDataList();
|
||||
};
|
||||
queryRef.value?.resetFields()
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 多选事件
|
||||
const handleSelectionChange = (objs: { accessToken: string }[]) => {
|
||||
selectObjs.value = objs.map(({ accessToken }) => accessToken);
|
||||
multiple.value = !objs.length;
|
||||
};
|
||||
selectObjs.value = objs.map(({ accessToken }) => accessToken)
|
||||
multiple.value = !objs.length
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (accessTokens: string[]) => {
|
||||
try {
|
||||
await useMessageBox().confirm(t('systoken.offlineConfirmText'));
|
||||
await useMessageBox().confirm(t("systoken.offlineConfirmText"))
|
||||
} catch {
|
||||
return; // 取消删除则直接跳过此方法
|
||||
return // 取消删除则直接跳过此方法
|
||||
}
|
||||
|
||||
try {
|
||||
await delObj(accessTokens);
|
||||
getDataList();
|
||||
useMessage().success(t('systoken.offlineSuccessText'));
|
||||
await delObj(accessTokens)
|
||||
getDataList()
|
||||
useMessage().success(t("systoken.offlineSuccessText"))
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
useMessage().error(err.msg)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 判断当前token 是否和登录token一致
|
||||
const filterOwnToken = (row: any) => {
|
||||
return Session.getToken() === row.accessToken;
|
||||
};
|
||||
return Session.getToken() === row.accessToken
|
||||
}
|
||||
</script>
|
||||
@@ -2,15 +2,20 @@
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row>
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item label="手机号" prop="mobilePhone">
|
||||
<el-input placeholder="请输入手机号" v-model="state.queryForm.mobilePhone" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="getDataList">查询</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
|
||||
@@ -1,114 +1,115 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<el-descriptions v-if="detail" title="智能柜日志">
|
||||
<el-descriptions-item labelClassName="no-colon">
|
||||
<el-timeline>
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">用户授权</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.createTime }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flow_coll">
|
||||
<span>授权用户:</span>
|
||||
<span>{{ detail.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-loading="loading" class="door-log">
|
||||
<template v-if="detail && detail.id">
|
||||
<div class="door-log__head">
|
||||
<span class="door-log__title">智能柜流程日志</span>
|
||||
<el-tag v-if="flowStatusText" size="small" :type="flowStatusTagType" effect="plain">当前节点:{{ flowStatusText }}</el-tag>
|
||||
<el-tag v-if="detail.skuSubmitRecogId" size="small" type="info" effect="plain">识别ID:{{ detail.skuSubmitRecogId }}</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="door-log__flow">
|
||||
<div
|
||||
v-for="node in flowNodes"
|
||||
:key="node.key"
|
||||
class="flow-node"
|
||||
:class="[`is-${node.status}`, { 'is-idle': !node.time }]"
|
||||
>
|
||||
<div class="flow-node__rail">
|
||||
<span class="flow-node__dot" />
|
||||
</div>
|
||||
<div class="flow-node__card">
|
||||
<div class="flow-node__head">
|
||||
<span class="flow-node__title">{{ node.title }}</span>
|
||||
<el-tag v-if="node.tagText" size="small" :type="node.tagType" effect="light">{{ node.tagText }}</el-tag>
|
||||
<span class="flow-node__time">{{ node.time || "--" }}</span>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.requestOpenTime">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">智能柜申请开门</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.requestOpenTime }}</span>
|
||||
</div>
|
||||
<div v-if="node.desc" class="flow-node__desc">{{ node.desc }}</div>
|
||||
<div v-if="node.hint" class="flow-node__hint">{{ node.hint }}</div>
|
||||
<div v-if="node.payloads?.length" class="flow-node__payloads">
|
||||
<el-button
|
||||
v-for="payload in node.payloads"
|
||||
:key="payload.label"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openPayload(node.title + ' - ' + payload.label, payload.json)"
|
||||
>
|
||||
{{ payload.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.openCallbackTime">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">智能柜开门响应</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.openCallbackTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.videoCallbackTime">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">智能柜视频回调</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.videoCallbackTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.skuSubmitTime">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">商品提交识别</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.skuSubmitTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.skuCallbackTime">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">商品识别响应</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.skuCallbackTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.payTime">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<div>
|
||||
<el-tag type="info" style="width: 120px">订单结算</el-tag>
|
||||
<span class="color-primary ml-2">{{ detail.payTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail.error && detail.errorDesc">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<el-tag type="danger" style="width: 120px">错误</el-tag>
|
||||
<span class="color-danger ml-2">{{ detail.errorDesc }}</span>
|
||||
<el-button v-if="!detail?.hardAuth?.manualChangeUserId" class="ml-2" link
|
||||
@click="visibleErrorChangeDiaog = true">
|
||||
<!-- 错误节点:人工处理入口 -->
|
||||
<div v-if="node.key === 'error'" class="flow-node__actions">
|
||||
<el-button v-if="!detail?.hardAuth?.manualChangeUserId" type="primary" link size="small" @click="visibleErrorChangeDiaog = true">
|
||||
处理
|
||||
</el-button>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-timeline v-if="detail?.hardAuth?.manualChangeUserId">
|
||||
<el-timeline-item placement="top">
|
||||
<div class="el-timeline-right-content">
|
||||
<el-tag type="warning" style="width: 120px">手动处理智能柜订单</el-tag>
|
||||
<span class="color-warning ml-2">{{ detail.hardAuth.manualChangeTime }}</span>
|
||||
<span class="color-warning ml-2">{{ detail.hardAuth.manualChangeRemark }}</span>
|
||||
<div class="ml-2" v-if="detail.hardAuth.manualChangePic">
|
||||
<preview-image width="40px" height="40px" :src="detail.hardAuth.manualChangePic" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手动结算/取消操作快照 -->
|
||||
<div v-if="manualVo" class="flow-node is-manual">
|
||||
<div class="flow-node__rail">
|
||||
<span class="flow-node__dot" />
|
||||
</div>
|
||||
<div class="flow-node__card">
|
||||
<div class="flow-node__head">
|
||||
<span class="flow-node__title">{{ manualVo.manualCancel ? "手动取消订单" : "手动结算" }}</span>
|
||||
<el-tag size="small" type="warning" effect="light">{{ manualVo.manualCancel ? "人工取消" : "人工结算" }}</el-tag>
|
||||
<span class="flow-node__time">{{ manualVo.time || "--" }}</span>
|
||||
</div>
|
||||
<div class="flow-node__desc">
|
||||
操作人:{{ manualVo.operator || "--" }}
|
||||
<template v-if="manualVo.reason"> 备注:{{ manualVo.reason }}</template>
|
||||
</div>
|
||||
<div v-if="!manualVo.manualCancel && manualItems.length" class="flow-node__cart">
|
||||
<div v-for="(item, index) in manualItems" :key="index" class="flow-node__cart-row">
|
||||
<span class="flow-node__cart-name">{{ item.name }}</span>
|
||||
<span class="flow-node__cart-count">{{ displayPrice(item.price) }} × {{ item.productCount }}</span>
|
||||
<span class="flow-node__cart-price">{{ displayPrice(item.totalPrice) }}</span>
|
||||
</div>
|
||||
<div class="flow-node__cart-total">
|
||||
<span>合计:<span class="flow-node__cart-price">{{ displayPrice(manualVo.totalPrice) }}</span></span>
|
||||
<span v-if="manualVo.payChannel" class="flow-node__cart-channel">({{ manualVo.payChannel }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="flow-node__payloads">
|
||||
<el-button link type="primary" size="small" @click="openPayload((manualVo.manualCancel ? '手动取消订单' : '手动结算') + ' - 快照报文', manualRaw)">
|
||||
快照报文
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog title="手动处理智能柜订单" width="30%" @close="dialogClose" v-model="visibleErrorChangeDiaog"
|
||||
:close-on-click-modal="false" draggable>
|
||||
<el-form ref="formRef" :model="form" :rules="dataRules" formDialogRef label-width="90px"
|
||||
v-loading="formLoading">
|
||||
<!-- 手动处理智能柜订单记录 -->
|
||||
<div v-if="detail?.hardAuth?.manualChangeUserId" class="flow-node is-manual">
|
||||
<div class="flow-node__rail">
|
||||
<span class="flow-node__dot" />
|
||||
</div>
|
||||
<div class="flow-node__card">
|
||||
<div class="flow-node__head">
|
||||
<span class="flow-node__title">手动处理智能柜订单</span>
|
||||
<el-tag size="small" type="warning" effect="light">人工处理</el-tag>
|
||||
<span class="flow-node__time">{{ detail.hardAuth.manualChangeTime }}</span>
|
||||
</div>
|
||||
<div v-if="detail.hardAuth.manualChangeRemark" class="flow-node__desc">{{ detail.hardAuth.manualChangeRemark }}</div>
|
||||
<div v-if="detail.hardAuth.manualChangePic" class="flow-node__desc">
|
||||
<preview-image width="40px" height="40px" :src="detail.hardAuth.manualChangePic" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else-if="!loading" description="暂无智能柜流程日志" :image-size="60" />
|
||||
|
||||
<!-- 报文查看抽屉 -->
|
||||
<el-drawer v-model="payloadVisible" :title="payloadTitle" size="45%" append-to-body>
|
||||
<vue-json-pretty v-if="payloadData !== null" :data="payloadData" showLength showLineNumber showIcon :collapsedOnClickBrackets="false" />
|
||||
<pre v-else class="door-log__raw">{{ payloadRaw }}</pre>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 手动处理智能柜订单 -->
|
||||
<el-dialog title="手动处理智能柜订单" width="30%" @close="dialogClose" v-model="visibleErrorChangeDiaog" :close-on-click-modal="false" draggable>
|
||||
<el-form ref="formRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="formLoading">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="设备名" prop="image">
|
||||
@@ -117,8 +118,7 @@
|
||||
</el-col>
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="处理原因" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="请输入处理原因" type="textarea"
|
||||
:autosize="{ minRows: 6 }" />
|
||||
<el-input v-model="form.remark" placeholder="请输入处理原因" type="textarea" :autosize="{ minRows: 6 }" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -133,8 +133,12 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="hard-door-log">
|
||||
import VueJsonPretty from "vue-json-pretty"
|
||||
import "vue-json-pretty/lib/styles.css"
|
||||
import * as hardFlowManager from "/@/api/hard/hard-door-flow"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { auth } from "/@/utils/authFunction"
|
||||
import { displayPrice } from "/@/utils/formatMoney"
|
||||
|
||||
type Props = {
|
||||
orderId?: string
|
||||
@@ -149,6 +153,12 @@ const detail = ref<any>({})
|
||||
const loading = ref<boolean>(false)
|
||||
const formLoading = ref<boolean>(false)
|
||||
|
||||
// 报文抽屉
|
||||
const payloadVisible = ref(false)
|
||||
const payloadTitle = ref("")
|
||||
const payloadRaw = ref("")
|
||||
const payloadData = ref<any>(null)
|
||||
|
||||
// 提交表单数据
|
||||
const form = reactive({
|
||||
flowId: "",
|
||||
@@ -161,15 +171,189 @@ const dataRules = ref({
|
||||
remark: [{ required: true, message: "处理原因不能为空", trigger: "blur" }],
|
||||
})
|
||||
|
||||
/** 流程节点状态。 */
|
||||
type FlowNodeStatus = "done" | "waiting" | "warning" | "error"
|
||||
|
||||
/** 一个流程节点;payloads 为可查看的请求/回调报文。 */
|
||||
interface FlowNode {
|
||||
key: string
|
||||
title: string
|
||||
time?: string
|
||||
status: FlowNodeStatus
|
||||
desc?: string
|
||||
hint?: string
|
||||
payloads?: { label: string; json?: string }[]
|
||||
tagText?: string
|
||||
tagType?: "success" | "info" | "warning" | "danger"
|
||||
}
|
||||
|
||||
/** 流程状态编码 → 名称。 */
|
||||
const FLOW_STATUS_MAP: Record<string, string> = {
|
||||
"10": "用户授权",
|
||||
"20": "申请开门",
|
||||
"30": "开门响应",
|
||||
"40": "视频回调",
|
||||
"50": "已提交识别",
|
||||
"60": "识别响应",
|
||||
"70": "订单结算",
|
||||
}
|
||||
|
||||
const flowStatusText = computed(() => FLOW_STATUS_MAP[String(detail.value?.flowStatus || "")] || "")
|
||||
|
||||
const flowStatusTagType = computed(() => {
|
||||
if (detail.value?.payTime) return "success"
|
||||
if (detail.value?.error) return "danger"
|
||||
return "warning"
|
||||
})
|
||||
|
||||
/** 手动结算/取消快照原始报文:优先独立快照字段,兼容旧版本写入 skuCallbackVo 的手动标记。 */
|
||||
const manualRaw = computed<string>(() => {
|
||||
const dedicated = detail.value?.manualSettleVo
|
||||
if (dedicated) return dedicated
|
||||
const legacy = detail.value?.skuCallbackVo
|
||||
if (!legacy) return ""
|
||||
try {
|
||||
const vo = typeof legacy === "string" ? JSON.parse(legacy) : legacy
|
||||
return vo?.manual === true ? (typeof legacy === "string" ? legacy : JSON.stringify(legacy)) : ""
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
})
|
||||
|
||||
/** 手动结算/取消快照对象。 */
|
||||
const manualVo = computed<any>(() => {
|
||||
if (!manualRaw.value) return null
|
||||
try {
|
||||
const vo = typeof manualRaw.value === "string" ? JSON.parse(manualRaw.value) : manualRaw.value
|
||||
return vo?.manual === true ? vo : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
/** 快照中的购物车明细。 */
|
||||
const manualItems = computed<any[]>(() => (Array.isArray(manualVo.value?.items) ? manualVo.value.items : []))
|
||||
|
||||
const STATUS_TAGS: Record<FlowNodeStatus, { text: string; type: "success" | "info" | "warning" | "danger" }> = {
|
||||
done: { text: "成功", type: "success" },
|
||||
waiting: { text: "等待中", type: "info" },
|
||||
warning: { text: "未响应", type: "warning" },
|
||||
error: { text: "错误", type: "danger" },
|
||||
}
|
||||
|
||||
/** 按数据到达情况推导各节点状态与可查看报文。 */
|
||||
const flowNodes = computed<FlowNode[]>(() => {
|
||||
const d = detail.value || {}
|
||||
const withTag = (node: FlowNode): FlowNode => {
|
||||
if (node.time) {
|
||||
node.tagText = STATUS_TAGS[node.status].text
|
||||
node.tagType = STATUS_TAGS[node.status].type
|
||||
}
|
||||
// 只保留有报文数据的入口
|
||||
node.payloads = node.payloads?.filter((payload) => !!payload.json)
|
||||
return node
|
||||
}
|
||||
const nodes: FlowNode[] = [
|
||||
withTag({
|
||||
key: "auth",
|
||||
title: "用户授权",
|
||||
time: d.createTime,
|
||||
status: "done",
|
||||
desc: d.username ? `授权用户:${d.username}` : "",
|
||||
}),
|
||||
withTag({
|
||||
key: "apply",
|
||||
title: "智能柜申请开门",
|
||||
time: d.requestOpenTime,
|
||||
status: d.requestOpenTime ? "done" : "waiting",
|
||||
payloads: [
|
||||
{ label: "请求参数", json: d.requestOpenDto },
|
||||
{ label: "请求返回", json: d.requestOpenVo },
|
||||
],
|
||||
}),
|
||||
withTag({
|
||||
key: "open",
|
||||
title: "智能柜开门响应",
|
||||
time: d.openCallbackTime,
|
||||
status: d.openCallbackTime ? "done" : "waiting",
|
||||
desc: d.isOpen != null && d.isOpen !== "" ? `开门结果:${String(d.isOpen) === "1" ? "已开门" : "未开门"}` : "",
|
||||
payloads: [{ label: "回调报文", json: d.openCallbackVo }],
|
||||
}),
|
||||
withTag({
|
||||
key: "video",
|
||||
title: "智能柜视频回调",
|
||||
time: d.videoCallbackTime,
|
||||
status: d.videoCallbackTime ? "done" : "waiting",
|
||||
payloads: [{ label: "回调报文", json: d.videoCallbackVo }],
|
||||
}),
|
||||
withTag({
|
||||
key: "submit",
|
||||
title: "提交商品识别",
|
||||
time: d.skuSubmitTime,
|
||||
status: d.skuSubmitTime ? "done" : "warning",
|
||||
payloads: [
|
||||
{ label: "提交参数", json: d.skuSubmitDto },
|
||||
{ label: "提交返回", json: d.skuSubmitVo },
|
||||
],
|
||||
}),
|
||||
withTag({
|
||||
key: "recog",
|
||||
title: "商品识别响应",
|
||||
time: d.skuCallbackTime,
|
||||
status: d.skuCallbackTime ? "done" : d.skuSubmitTime ? "warning" : "waiting",
|
||||
desc: !d.skuCallbackTime && manualVo.value ? `已通过${manualVo.value.manualCancel ? "手动取消" : "手动结算"}人工处理,见下方快照节点` : "",
|
||||
hint:
|
||||
!d.skuCallbackTime && d.skuSubmitTime && !manualVo.value
|
||||
? "第三方识别服务未响应(服务可能不可用),可使用「手动结算」人工核对视频后结算"
|
||||
: "",
|
||||
payloads: [{ label: "识别结果", json: d.skuCallbackVo }],
|
||||
}),
|
||||
withTag({
|
||||
key: "pay",
|
||||
title: "订单结算",
|
||||
time: d.payTime,
|
||||
status: d.payTime ? "done" : "waiting",
|
||||
}),
|
||||
]
|
||||
if (d.error && d.errorDesc) {
|
||||
nodes.push({
|
||||
key: "error",
|
||||
title: "流程错误",
|
||||
time: d.updateTime,
|
||||
status: "error",
|
||||
desc: d.errorDesc,
|
||||
tagText: "错误",
|
||||
tagType: "danger",
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
})
|
||||
|
||||
const openPayload = (title: string, json?: string) => {
|
||||
payloadTitle.value = title
|
||||
payloadRaw.value = json || ""
|
||||
payloadData.value = null
|
||||
if (json) {
|
||||
try {
|
||||
payloadData.value = typeof json === "string" ? JSON.parse(json) : json
|
||||
} catch {
|
||||
payloadData.value = null
|
||||
}
|
||||
}
|
||||
payloadVisible.value = true
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.orderId,
|
||||
() => {
|
||||
getFlow()
|
||||
},
|
||||
() => getFlow(),
|
||||
)
|
||||
|
||||
const getFlow = async () => {
|
||||
if (!props.orderId) return
|
||||
// 判断是否有"dzc_hardDoorFlow_view"权限
|
||||
if (!auth("dzc_hardDoorFlow_view")) {
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const _pa = { orderId: props.orderId }
|
||||
@@ -188,7 +372,7 @@ const dialogClose = async () => {
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
const valid = await formRef.value.validate().catch(() => { })
|
||||
const valid = await formRef.value.validate().catch(() => {})
|
||||
if (!valid) return false
|
||||
try {
|
||||
formLoading.value = true
|
||||
@@ -208,77 +392,210 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flow_coll {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
.door-log {
|
||||
&__head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
span:nth-child(1) {
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
text-align: left;
|
||||
&__title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
min-width: 50px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
&__raw {
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
background-color: var(--el-fill-color-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 时间线样式调整
|
||||
:deep(.el-timeline) {
|
||||
margin: 10px 0 0 160px;
|
||||
.flow-node {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.el-timeline-item__wrapper {
|
||||
&:last-of-type .flow-node__rail::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__rail {
|
||||
position: relative;
|
||||
top: -20px;
|
||||
|
||||
.el-timeline-item__timestamp {
|
||||
position: absolute !important;
|
||||
top: 10px;
|
||||
left: -150px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-timeline-right-content {
|
||||
display: flex;
|
||||
flex: 0 0 14px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-height: 30px;
|
||||
padding: 10px;
|
||||
background-color: #b0d8ff;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-top: 16px;
|
||||
|
||||
&::before {
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 13px;
|
||||
border-color: transparent #b0d8ff transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 8px;
|
||||
top: 30px;
|
||||
bottom: -16px;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
content: "";
|
||||
background-color: var(--el-border-color-lighter);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.el-timeline-item__node {
|
||||
position: absolute;
|
||||
left: -5px;
|
||||
top: -50%;
|
||||
transform: translateY(100%);
|
||||
display: flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 10px;
|
||||
background: #b0d8ff;
|
||||
&__dot {
|
||||
z-index: 1;
|
||||
box-sizing: content-box;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--el-color-info);
|
||||
border: 3px solid #fff;
|
||||
border-radius: 50%;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 0 2px currentColor;
|
||||
color: var(--el-color-info);
|
||||
}
|
||||
|
||||
&__card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
transition: box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
}
|
||||
}
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
&__time {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
&__hint {
|
||||
margin-top: 6px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: var(--el-color-warning);
|
||||
background-color: var(--el-color-warning-light-9);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// 手动结算快照的购物车明细
|
||||
&__cart {
|
||||
margin-top: 6px;
|
||||
padding: 8px 10px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&__cart-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
&__cart-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__cart-count {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
&__cart-price {
|
||||
min-width: 64px;
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__cart-total {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
padding-top: 6px;
|
||||
font-size: 13px;
|
||||
border-top: 1px dashed var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
&__cart-channel {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
&__payloads,
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
// 未发生节点弱化
|
||||
&.is-idle .flow-node__card {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
&.is-done .flow-node__dot {
|
||||
color: var(--el-color-success);
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&.is-warning .flow-node__dot {
|
||||
color: var(--el-color-warning);
|
||||
background-color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
&.is-error {
|
||||
.flow-node__dot {
|
||||
color: var(--el-color-danger);
|
||||
background-color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.flow-node__card {
|
||||
border-color: var(--el-color-danger-light-7);
|
||||
background-color: var(--el-color-danger-light-9);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-manual .flow-node__dot {
|
||||
color: var(--el-color-warning);
|
||||
background-color: var(--el-color-warning);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,526 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="manual-settlement">
|
||||
<el-alert type="warning" :closable="false" show-icon
|
||||
title="第三方商品识别服务不可用,请核对开门视频,人工选择用户拿走的商品后手动结算;若用户未拿商品请取消订单。" />
|
||||
|
||||
<div class="settle-layout">
|
||||
<!-- 左侧:柜内商品选择 -->
|
||||
<div class="settle-goods">
|
||||
<div class="settle-head">
|
||||
<span class="settle-head__title">柜内商品</span>
|
||||
<el-tag v-if="cabinetNoLabel" size="small" type="info" effect="plain">{{ cabinetNoLabel }}</el-tag>
|
||||
</div>
|
||||
<div class="settle-goods__scroll">
|
||||
<template v-if="floors.length">
|
||||
<div v-for="floor in floors" :key="floor.floorNo" class="settle-floor">
|
||||
<div class="settle-floor__tag">第 {{ floor.floorNo }} 层</div>
|
||||
<div v-for="product in floor.products" :key="product.productId" class="settle-product"
|
||||
:class="{ 'is-selected': Number(cartCount[product.productId] || 0) > 0 }">
|
||||
<preview-image width="56px" height="56px" :src="product.pic" />
|
||||
<div class="settle-product__info">
|
||||
<el-tooltip effect="dark" :content="product.name">
|
||||
<div class="settle-product__name">{{ product.name }}</div>
|
||||
</el-tooltip>
|
||||
<div class="settle-product__meta">
|
||||
<span class="settle-product__price">{{ displayPrice(product.price) }}</span>
|
||||
<span class="settle-product__stock">库存 {{ product.stock }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-input-number v-model="cartCount[product.productId]" class="settle-product__stepper"
|
||||
:min="0" controls-position="right" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else-if="!loading" description="未找到该柜的商品配置" :image-size="60" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:开门视频 + 已选购物车 + 操作 -->
|
||||
<div class="settle-side">
|
||||
<div class="settle-card">
|
||||
<div class="settle-head">
|
||||
<span class="settle-head__title">开门视频</span>
|
||||
<span class="settle-head__extra">共 {{ videos.length }} 路,同步播放</span>
|
||||
</div>
|
||||
<div v-if="videos.length" class="settle-video__grid" :class="{ 'is-single': videos.length === 1 }">
|
||||
<div v-for="(video, index) in videos" :key="index" class="settle-video__item">
|
||||
<el-tag size="small" effect="plain" :type="index === 0 ? 'primary' : 'info'"
|
||||
class="settle-video__label">
|
||||
{{ video.label }}
|
||||
</el-tag>
|
||||
<video-player :src="video.url" width="100%" height="320px" />
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="无开门视频(视频未回调或回调失败)" :image-size="60" />
|
||||
</div>
|
||||
|
||||
<div class="settle-card">
|
||||
<div class="settle-head">
|
||||
<span class="settle-head__title">已选商品</span>
|
||||
<span v-if="cartItems.length" class="settle-head__extra">共 {{ cartCountTotal }} 件</span>
|
||||
</div>
|
||||
<div v-if="cartItems.length" class="settle-cart">
|
||||
<div class="settle-cart__head">
|
||||
<span>商品</span>
|
||||
<span>数量</span>
|
||||
<span>小计</span>
|
||||
</div>
|
||||
<div v-for="item in cartItems" :key="item.productId" class="settle-cart__row">
|
||||
<span class="settle-cart__name">{{ item.name }}</span>
|
||||
<span class="settle-cart__count">× {{ item.count }}</span>
|
||||
<span class="settle-cart__price">{{ displayPrice(item.price * item.count) }}</span>
|
||||
</div>
|
||||
<div class="settle-cart__total">
|
||||
<span>合计</span>
|
||||
<span class="settle-cart__total-price">{{ displayPrice(cartTotalPrice) }}</span>
|
||||
</div>
|
||||
<div class="settle-cart__tip">金额仅供参考,以实际结算结果为准</div>
|
||||
</div>
|
||||
<el-empty v-else description="请从左侧选择用户拿走的商品" :image-size="60" />
|
||||
<div class="settle-actions">
|
||||
<el-button v-auth="'hard_hardwareOrder_manualSettle'" type="danger" plain :loading="cancelling"
|
||||
@click="onCancel">
|
||||
未拿商品,取消订单
|
||||
</el-button>
|
||||
<el-button v-auth="'hard_hardwareOrder_manualSettle'" type="primary"
|
||||
:disabled="!cartItems.length" :loading="submitting" @click="onSettle">
|
||||
手动结算
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="manual-settlement">
|
||||
import { h } from "vue"
|
||||
import { ElMessageBox } from "element-plus"
|
||||
import * as hardFlowManager from "/@/api/hard/hard-door-flow"
|
||||
import { getByCabinetNo } from "/@/api/hard/hard-spu"
|
||||
import { manualCancel, manualSettle } from "/@/api/hard/hard-order"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { displayPrice } from "/@/utils/formatMoney"
|
||||
|
||||
const VideoPlayer = defineAsyncComponent(() => import("/@/components/VideoPlayer/index.vue"))
|
||||
|
||||
type Props = {
|
||||
orderId?: string
|
||||
hardId?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), { orderId: "", hardId: "" })
|
||||
const emit = defineEmits<{ (e: "refresh"): void }>()
|
||||
|
||||
/** 柜内单层中一个可选商品(同层多槽位同商品已合并)。 */
|
||||
interface FloorProduct {
|
||||
productId: string
|
||||
name: string
|
||||
pic?: string
|
||||
price: number
|
||||
stock: number
|
||||
}
|
||||
|
||||
/** 一层商品。 */
|
||||
interface Floor {
|
||||
floorNo: number
|
||||
products: FloorProduct[]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const cancelling = ref(false)
|
||||
const floors = ref<Floor[]>([])
|
||||
const cabinetNoLabel = ref("")
|
||||
/** 商品 id -> 手动选择的数量。 */
|
||||
const cartCount = reactive<Record<string, number>>({})
|
||||
const videos = ref<{ label: string; url: string }[]>([])
|
||||
|
||||
const cartItems = computed(() => {
|
||||
const items: { productId: string; name: string; price: number; count: number }[] = []
|
||||
for (const floor of floors.value) {
|
||||
for (const product of floor.products) {
|
||||
const count = Number(cartCount[product.productId] || 0)
|
||||
if (count > 0) items.push({ productId: product.productId, name: product.name, price: product.price, count })
|
||||
}
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
const cartCountTotal = computed(() => cartItems.value.reduce((sum, item) => sum + item.count, 0))
|
||||
|
||||
const cartTotalPrice = computed(() => cartItems.value.reduce((sum, item) => sum + item.price * item.count, 0))
|
||||
|
||||
const load = async () => {
|
||||
if (!props.orderId || !props.hardId) return
|
||||
loading.value = true
|
||||
floors.value = []
|
||||
videos.value = []
|
||||
cabinetNoLabel.value = ""
|
||||
Object.keys(cartCount).forEach((key) => delete cartCount[key])
|
||||
try {
|
||||
const { data: flow } = await hardFlowManager.getByOrderId({ orderId: props.orderId })
|
||||
if (flow?.cabinetNo != null) {
|
||||
cabinetNoLabel.value = `${flow.cabinetNo} 号柜`
|
||||
const res = await getByCabinetNo({ hardId: props.hardId, cabinetNo: flow.cabinetNo })
|
||||
floors.value = parseFloors(res.data)
|
||||
}
|
||||
videos.value = parseVideos(flow?.videoCallbackVo)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.orderId, props.hardId],
|
||||
() => load(),
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
/** 把 /hard-spu/get-by-cabinet-no 的层结构转成层数组,同层同商品多槽位合并、库存求和。 */
|
||||
const parseFloors = (cabinetData: any): Floor[] => {
|
||||
if (!Array.isArray(cabinetData)) return []
|
||||
const result: Floor[] = []
|
||||
cabinetData.forEach((floorItem: any, floorIndex: number) => {
|
||||
const floorNo = floorIndex + 1
|
||||
const slots: any[] = floorItem?.[floorNo] || floorItem?.[String(floorNo)] || []
|
||||
const productMap = new Map<string, FloorProduct>()
|
||||
for (const slot of slots) {
|
||||
if (!slot?.spuId || !slot?.spuDetail) continue
|
||||
const spu = slot.spuDetail
|
||||
const productId = String(spu.id ?? slot.spuId)
|
||||
const joinStock = Number(slot.repertoryCount ?? 0)
|
||||
const spuStock = Number(spu.repertoryCount ?? 0)
|
||||
const exist = productMap.get(productId)
|
||||
if (exist) {
|
||||
exist.stock += joinStock
|
||||
} else {
|
||||
productMap.set(productId, {
|
||||
productId,
|
||||
name: spu.name || "未知商品",
|
||||
pic: spu.pic,
|
||||
price: Number(spu.price ?? 0),
|
||||
// 槽位库存是本柜的可拿数量;未初始化时回落到商品总库存
|
||||
stock: joinStock > 0 ? joinStock : spuStock,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (productMap.size) result.push({ floorNo, products: [...productMap.values()] })
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/** 从视频回调报文 videoCallbackVo 中解析 videoList 的视频地址。 */
|
||||
const parseVideos = (raw: unknown): { label: string; url: string }[] => {
|
||||
if (!raw) return []
|
||||
try {
|
||||
const vo = typeof raw === "string" ? JSON.parse(raw) : raw
|
||||
const list = Array.isArray(vo?.videoList) ? vo.videoList : []
|
||||
return list
|
||||
.filter((video: any) => video?.videoUrl)
|
||||
.map((video: any, index: number) => ({
|
||||
label: Number(video.cameraNo) === 1 ? "主摄" : Number(video.cameraNo) === 2 ? "辅摄" : `视频${index + 1}`,
|
||||
url: String(video.videoUrl),
|
||||
}))
|
||||
} catch (e) {
|
||||
console.error("解析视频回调报文失败", e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const onSettle = async () => {
|
||||
const items = cartItems.value
|
||||
if (!items.length || submitting.value) return
|
||||
const summary = h("div", { style: "margin:8px 0;line-height:22px" }, [
|
||||
h("div", null, `已选商品:${items.map((item) => `${item.name}×${item.count}`).join(";")}`),
|
||||
h("div", null, `合计金额:${displayPrice(cartTotalPrice.value)}(实际金额以结算结果为准)`),
|
||||
h("div", { style: "color:var(--el-color-danger)" }, "确认后将从用户钱包强制扣款或通过支付宝免密扣款,请谨慎操作。"),
|
||||
])
|
||||
let reason = ""
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt(summary, "手动结算确认", {
|
||||
confirmButtonText: "确认结算",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
inputPlaceholder: "操作备注(选填):记录判断依据,如 视频02:35拿走一瓶,便于事后复核",
|
||||
inputPattern: /^[\s\S]{0,200}$/,
|
||||
inputErrorMessage: "备注最多 200 字",
|
||||
})
|
||||
reason = (value || "").trim()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await manualSettle({
|
||||
orderId: props.orderId,
|
||||
items: items.map((item) => ({ productId: item.productId, productCount: item.count })),
|
||||
reason,
|
||||
})
|
||||
useMessage().success("手动结算成功")
|
||||
emit("refresh")
|
||||
} catch (e: any) {
|
||||
useMessage().error(e?.msg || "手动结算失败")
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = async () => {
|
||||
if (cancelling.value) return
|
||||
let reason = ""
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt("确认该订单用户未拿任何商品?取消后订单状态置为已取消,不可恢复。", "取消订单确认", {
|
||||
confirmButtonText: "确认取消订单",
|
||||
cancelButtonText: "再想想",
|
||||
type: "warning",
|
||||
inputPlaceholder: "操作备注(选填):记录判断依据,便于事后复核",
|
||||
inputPattern: /^[\s\S]{0,200}$/,
|
||||
inputErrorMessage: "备注最多 200 字",
|
||||
})
|
||||
reason = (value || "").trim()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
cancelling.value = true
|
||||
try {
|
||||
await manualCancel({ orderId: props.orderId, reason })
|
||||
useMessage().success("订单已取消")
|
||||
emit("refresh")
|
||||
} catch (e: any) {
|
||||
useMessage().error(e?.msg || "取消订单失败")
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.manual-settlement {
|
||||
.settle-layout {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-top: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.settle-head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&__title {
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&__extra {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
// 左侧商品列:适度固定宽,超出内部滚动,避免把视频挤小
|
||||
.settle-goods {
|
||||
display: flex;
|
||||
flex: 0 0 420px;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
|
||||
&__scroll {
|
||||
flex: 1;
|
||||
max-height: 560px;
|
||||
padding-right: 4px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.settle-floor {
|
||||
&:not(:first-child) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
padding-left: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-secondary);
|
||||
border-left: 3px solid var(--el-color-primary);
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.settle-product {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
|
||||
&+.settle-product {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
border-color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__price {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__stock {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
// 数量步进器固定紧凑宽度,不随弹性布局拉伸
|
||||
&__stepper {
|
||||
flex-shrink: 0;
|
||||
width: 112px;
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧视频 + 购物车占满剩余宽度
|
||||
.settle-side {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settle-card {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
// 多路视频同屏并排播放;单路时占满整行
|
||||
.settle-video__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settle-video__item {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
||||
.settle-video__label {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.settle-cart {
|
||||
|
||||
&__head,
|
||||
&__row,
|
||||
&__total {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 72px 96px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__head {
|
||||
padding-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
span:nth-child(2),
|
||||
span:nth-child(3) {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&__row {
|
||||
padding: 6px 0;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px dashed var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
&__name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__count,
|
||||
&__price {
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__price {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__total {
|
||||
padding-top: 8px;
|
||||
font-weight: 600;
|
||||
|
||||
span:first-child {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
&__total-price {
|
||||
grid-column: 3;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__tip {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.settle-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" width="520px" draggable :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" v-loading="loading">
|
||||
<el-form-item label="订单号">
|
||||
<el-input :model-value="orderLabel" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="凭证类型">
|
||||
<el-tag :type="options.printType === '2' ? 'warning' : 'primary'">
|
||||
{{ options.printType === "2" ? "兑换券" : "发票" }}
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showSourceSelection" label="来源终端" prop="sourceHardId">
|
||||
<el-select v-model="form.sourceHardId" class="w-full" filterable placeholder="请选择已绑定尔印打印机的业务终端">
|
||||
<el-option v-for="source in sources" :key="source.id" :label="source.name" :value="String(source.id)" />
|
||||
</el-select>
|
||||
<div v-if="sources.length === 0" class="mt-1 text-xs text-[var(--el-color-danger)]">
|
||||
该商户没有已绑定尔印打印机的业务终端,请先在设备管理完成绑定。
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="requiresReason" label="重打原因" prop="reprintReason">
|
||||
<el-input v-model="form.reprintReason" type="textarea" :rows="3" maxlength="200" show-word-limit
|
||||
placeholder="请填写本次人工重打原因" />
|
||||
</el-form-item>
|
||||
<el-alert v-if="options.action === 'retry'" type="info" :closable="false" show-icon
|
||||
title="重试原任务不会增加打印序号;终端只处理新的重试版本。" />
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" :disabled="sourceUnavailable" @click="submit">
|
||||
提交打印任务
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="HardPrintTaskDialog">
|
||||
import * as hardInvoiceApi from "/@/api/hard/hard-invoice"
|
||||
import * as printerApi from "/@/api/hard/hard-printer"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
|
||||
type PrintAction = "create" | "retry" | "reprint"
|
||||
|
||||
interface PrintDialogOptions {
|
||||
action: PrintAction
|
||||
printType: "1" | "2"
|
||||
orderId: string | number
|
||||
merchantId?: string | number
|
||||
taskId?: string | number
|
||||
sourceHardId?: string | number | null
|
||||
orderNo?: string
|
||||
printNo?: string | number | null
|
||||
}
|
||||
|
||||
const emit = defineEmits(["submitted"])
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref()
|
||||
const sources = ref<any[]>([])
|
||||
const options = reactive<PrintDialogOptions>({ action: "create", printType: "1", orderId: "" })
|
||||
/** 来源设备使用字符串保存,避免选择或自动回填时破坏后端 Long 主键精度。 */
|
||||
const form = reactive<{ sourceHardId: printerApi.HardwareId | null; reprintReason: string }>({
|
||||
sourceHardId: null,
|
||||
reprintReason: "",
|
||||
})
|
||||
|
||||
/** 已打印任务再次出纸必须记录原因;失败任务重试不改变打印次数。 */
|
||||
const requiresReason = computed(() => options.action === "reprint" || Boolean(options.printNo && options.action === "create"))
|
||||
const showSourceSelection = computed(() => options.action !== "reprint" && !(options.action === "retry" && options.sourceHardId))
|
||||
const sourceUnavailable = computed(() => showSourceSelection.value && !loading.value && sources.value.length === 0)
|
||||
const orderLabel = computed(() => options.orderNo || String(options.orderId || "--"))
|
||||
const title = computed(() => {
|
||||
if (options.action === "retry") return "重试失败打印任务"
|
||||
if (options.action === "reprint") return `重打${options.printType === "2" ? "兑换券" : "发票"}`
|
||||
return `${requiresReason.value ? "再次打印" : "打印"}${options.printType === "2" ? "兑换券" : "发票"}`
|
||||
})
|
||||
const rules = computed(() => ({
|
||||
sourceHardId: showSourceSelection.value ? [{ required: true, message: "请选择来源业务终端", trigger: "change" }] : [],
|
||||
reprintReason: requiresReason.value ? [{ required: true, message: "请填写重打原因", trigger: "blur" }] : [],
|
||||
}))
|
||||
|
||||
/** 打开任务弹窗,并根据商户的有效绑定自动选择唯一来源终端。 */
|
||||
const open = async (input: PrintDialogOptions) => {
|
||||
Object.assign(options, input)
|
||||
form.sourceHardId = null
|
||||
form.reprintReason = ""
|
||||
sources.value = []
|
||||
visible.value = true
|
||||
await nextTick()
|
||||
formRef.value?.clearValidate?.()
|
||||
if (!showSourceSelection.value) return
|
||||
if (!input.merchantId) {
|
||||
useMessage().error("订单缺少商户信息,无法选择打印终端")
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const response: any = await printerApi.listBoundSources(String(input.merchantId))
|
||||
sources.value = response.data || []
|
||||
const preferred = sources.value.find((source) => input.sourceHardId != null
|
||||
&& String(source.id) === String(input.sourceHardId))
|
||||
if (preferred) form.sourceHardId = String(preferred.id)
|
||||
else if (sources.value.length === 1) form.sourceHardId = String(sources.value[0].id)
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "读取可用打印终端失败")
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 校验、二次确认并提交任务;响应只代表任务受理,不代表打印机已经出纸。 */
|
||||
const submit = async () => {
|
||||
const valid = await formRef.value?.validate?.().catch(() => false)
|
||||
if (valid === false) return
|
||||
if (requiresReason.value) {
|
||||
try {
|
||||
await useMessageBox().confirm(`确认提交第 ${Number(options.printNo || 0) + 1} 次打印?提交后将保留操作原因。`)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
if (options.action === "retry") {
|
||||
await hardInvoiceApi.retry({ historyId: options.taskId, sourceHardId: form.sourceHardId || undefined })
|
||||
} else if (options.action === "reprint") {
|
||||
await hardInvoiceApi.reprint({ historyId: options.taskId, reprintReason: form.reprintReason.trim() })
|
||||
} else {
|
||||
await hardInvoiceApi.print({
|
||||
orderId: options.orderId,
|
||||
printType: options.printType,
|
||||
sourceHardId: form.sourceHardId || undefined,
|
||||
reprintReason: form.reprintReason.trim() || undefined,
|
||||
})
|
||||
}
|
||||
visible.value = false
|
||||
useMessage().success("打印任务已提交")
|
||||
emit("submitted")
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "打印任务提交失败")
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -72,6 +72,9 @@ const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
pageList: hardAfterSaleApi.pageJoin,
|
||||
})
|
||||
|
||||
// table hook
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, tableStyle } = useTable(state)
|
||||
|
||||
watch(
|
||||
() => props.orderId,
|
||||
(newVal, oldVal) => {
|
||||
@@ -82,9 +85,6 @@ watch(
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
// table hook
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, tableStyle } = useTable(state)
|
||||
|
||||
// 定义查询字典
|
||||
const { hard_after_sale_type, hard_after_sale_way } = useDict("hard_after_sale_type", "hard_after_sale_way")
|
||||
|
||||
|
||||
@@ -1,78 +1,128 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 250px" label="用户" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="退款状态" prop="status">
|
||||
<dict-select v-model="state.queryForm.status" dict-code="hard_sale_status" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="申请时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
|
||||
type="daterange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">{{ $t("common.queryBtn")
|
||||
}}</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table-column type="selection" width="40" align="center" />
|
||||
<el-table-column type="index" label="#" width="40" />
|
||||
<el-table-column prop="username" label="用户" />
|
||||
<el-table-column prop="userAvatar" label="头像">
|
||||
<template #default="{ row }">
|
||||
<preview-image :src="row.userAvatar" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="userMobile" width="120" label="手机号" />
|
||||
<el-table-column label="支付金额" align="center" prop="practicalTotalPrice">
|
||||
<template #default="{ row }">
|
||||
<span class="color-primary">
|
||||
{{ displayPrice(row.orderPayPrice) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="退款金额" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ displayPrice(row.refundPrice) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="退款状态" align="center" min-width="120px">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="hard_sale_status" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="申请时间" align="center" width="180px" prop="createTime" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button icon="ShoppingCartFull" text type="primary"
|
||||
@click="shopCarDialogRef.openDialog(row.orderId)">购物车</el-button>
|
||||
<el-button icon="WalletFilled" v-auth="'hard_hardAfterSale_detail_view'" link type="primary"
|
||||
@click="doRefund(row)">
|
||||
退款
|
||||
</el-button>
|
||||
<el-button v-if="false" icon="edit-pen" text type="primary" v-auth="'hard_hardAfterSale_edit'"
|
||||
@click="formDialogRef.openDialog(row.id)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button v-if="false" icon="delete" text type="primary" v-auth="'hard_hardAfterSale_del'"
|
||||
@click="handleDelete([row.id])">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<el-tabs class="after-sale-tabs">
|
||||
<el-tab-pane label="售后订单" name="AFTER_SALE">
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<SearchPanel v-show="showSearch" ref="queryRef" :model="state.queryForm"
|
||||
:loading="Boolean(state.loading)" :search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')" @search="getDataList" @reset="resetQuery">
|
||||
<el-form-item style="width: 250px" label="退款订单人" prop="orderUserIds">
|
||||
<select-user v-model="state.queryForm.orderUserIds" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="发起退款人" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="退款状态" prop="status">
|
||||
<dict-select v-model="state.queryForm.status" dict-code="hard_sale_status" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="订单状态" prop="orderStatusCodes">
|
||||
<dict-select v-model="state.queryForm.orderStatusCodes" dict-code="hard_order_status"
|
||||
multiple />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="申请时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
|
||||
type="daterange" />
|
||||
</el-form-item>
|
||||
<template #more>
|
||||
<el-form-item style="width: 250px" label="用餐时间段" prop="hourTypeCodes">
|
||||
<dict-select v-model="state.queryForm.hourTypeCodes" dict-code="daily_menu"
|
||||
multiple />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="预约时间" prop="apptUseDateTimeList">
|
||||
<el-date-picker v-model="state.queryForm.apptUseDateTimeList"
|
||||
value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
|
||||
:default-time="dateRangeDefaultTime" />
|
||||
</el-form-item>
|
||||
<user-identity-query v-model:identity-types="state.queryForm.identityTypes"
|
||||
v-model:student-info="state.queryForm.studentInfo"
|
||||
v-model:teacher-info="state.queryForm.teacherInfo"
|
||||
v-model:has-face="state.queryForm.hasFace" />
|
||||
</template>
|
||||
</SearchPanel>
|
||||
<el-tabs v-if="orderTypeTabs.length" v-model="activeOrderType" class="order-type-tabs shrink-0"
|
||||
@tab-click="handleOrderTypeClick">
|
||||
<el-tab-pane v-for="item in orderTypeTabs" :key="String(item.value)" :label="item.label"
|
||||
:name="item.value" />
|
||||
</el-tabs>
|
||||
<el-table class="min-h-0 flex-1" height="100%" :data="state.dataList" v-loading="state.loading"
|
||||
border :cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column label="用户信息" min-width="280">
|
||||
<template #default="{ row }"><user-info-cell :user="row"
|
||||
:show-gender="false" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="身份信息" min-width="300">
|
||||
<template #default="{ row }"><user-identity-info :user="row" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付金额" min-width="120" align="center" prop="practicalTotalPrice">
|
||||
<template #default="{ row }">
|
||||
<span class="color-primary">
|
||||
{{ displayPrice(row.orderPayPrice) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column min-width="120" label="退款金额" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ displayPrice(row.refundPrice) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column min-width="120" label="退款状态" align="center">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="hard_sale_status" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="退款类型" align="center" min-width="240">
|
||||
<template #default>
|
||||
<span>人工审核退款</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处理方式" align="center" min-width="150">
|
||||
<template #default>
|
||||
<span>人工审核</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付时间" align="center" width="250px" prop="orderPayTime">
|
||||
<template #default="{ row }">
|
||||
<div class="status">
|
||||
<span>支付时间</span>
|
||||
<span>{{ row.orderPayTime || "-" }}</span>
|
||||
</div>
|
||||
<div class="status">
|
||||
<span>申请时间</span>
|
||||
<span>{{ row.createTime || "-" }}</span>
|
||||
</div>
|
||||
<div v-if="row.orderApptUseDate" class="status">
|
||||
<span>预约时间</span>
|
||||
<span>{{ row.orderApptUseDate }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="flex flex-wrap items-start -mx-1">
|
||||
<div class="w-1/2 h-[30px] flex justify-start items-center">
|
||||
<el-button icon="ShoppingCartFull" text type="primary"
|
||||
@click="shopCarDialogRef.openDialog(row.orderId)">购物车</el-button>
|
||||
</div>
|
||||
<div class="w-1/2 h-[30px] flex justify-start items-center"
|
||||
v-auth="'hard_hardAfterSale_detail_view'">
|
||||
<el-button icon="WalletFilled" link type="primary" @click="doRefund(row)">
|
||||
退款
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
@@ -86,7 +136,12 @@ import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import { pageJoin, delObjs } from "/@/api/hard/hard-after-sale"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { useDict } from "/@/hooks/dict"
|
||||
import { getDicts } from "/@/api/admin/dict"
|
||||
import { displayPrice } from "/@/utils/formatMoney"
|
||||
import {
|
||||
createEmptyUserIdentityStudentQuery,
|
||||
createEmptyUserIdentityTeacherQuery,
|
||||
} from "/@/components/UserIdentityQuery/types"
|
||||
|
||||
// 引入组件
|
||||
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
|
||||
@@ -99,6 +154,17 @@ const shopCarDialogRef = ref()
|
||||
// 搜索变量
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
/** 订单类型页签数据结构,value 为空时表示不限制订单类型。 */
|
||||
interface OrderTypeTab {
|
||||
/** 后台订单类型字典展示名称。 */
|
||||
label: string
|
||||
/** 后台订单类型字典编码;null 专用于“全部”页签。 */
|
||||
value: string | null
|
||||
}
|
||||
/** 当前选中的订单类型字典编码;null 表示查询全部订单类型。 */
|
||||
const activeOrderType = ref<string | null>(null)
|
||||
/** 表格上方的订单类型页签,字典加载前仍保留“全部”入口。 */
|
||||
const orderTypeTabs = ref<OrderTypeTab[]>([{ label: "全部", value: null }])
|
||||
// 多选变量
|
||||
const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
@@ -106,14 +172,52 @@ const draggableDepTreeRef = ref() // 树组件ref引用
|
||||
|
||||
const { push } = useRouter()
|
||||
|
||||
/** 日期范围默认覆盖开始日零点至结束日最后一秒,避免遗漏结束日期内的预约订单。 */
|
||||
const dateRangeDefaultTime = [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {},
|
||||
queryForm: {
|
||||
orderStatusCodes: [],
|
||||
hourTypeCodes: [],
|
||||
identityTypes: [],
|
||||
studentInfo: createEmptyUserIdentityStudentQuery(),
|
||||
teacherInfo: createEmptyUserIdentityTeacherQuery(),
|
||||
hasFace: "",
|
||||
},
|
||||
pageList: pageJoin,
|
||||
})
|
||||
|
||||
// table hook
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state)
|
||||
|
||||
/**
|
||||
* 加载订单类型字典并组装表格次级页签。
|
||||
* 请求失败时保留默认“全部”页签,页面仍可继续查询售后订单。
|
||||
*/
|
||||
const loadOrderTypeTabs = async () => {
|
||||
try {
|
||||
const { data = [] } = await getDicts("hard_order_type")
|
||||
orderTypeTabs.value = [
|
||||
{ label: "全部", value: null },
|
||||
...data.map((item: any) => ({ label: item.label, value: item.value })),
|
||||
]
|
||||
} catch {
|
||||
orderTypeTabs.value = [{ label: "全部", value: null }]
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadOrderTypeTabs)
|
||||
|
||||
/**
|
||||
* 切换订单类型后从第一页重新查询售后列表。
|
||||
*
|
||||
* @param pane Element Plus 当前点击的订单类型页签上下文
|
||||
*/
|
||||
const handleOrderTypeClick = (pane: any) => {
|
||||
state.queryForm.orderType = orderTypeTabs.value[Number(pane.index)]?.value ?? null
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
// 清空搜索条件
|
||||
@@ -122,7 +226,7 @@ const resetQuery = () => {
|
||||
selectObjs.value = []
|
||||
state.queryForm.deptId = ""
|
||||
state.queryForm.deptIds = []
|
||||
draggableDepTreeRef.value.resetData()
|
||||
draggableDepTreeRef.value?.resetData()
|
||||
getDataList()
|
||||
}
|
||||
|
||||
@@ -146,7 +250,10 @@ const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepT
|
||||
}
|
||||
|
||||
const doRefund = (row: any) => {
|
||||
push({ path: "/hard/hard-after-sale/order-refund", query: { id: row.orderId, type: "apply" } })
|
||||
push({
|
||||
path: "/hard/hard-after-sale/order-refund",
|
||||
query: { id: row.orderId, type: "apply" },
|
||||
})
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
@@ -166,3 +273,56 @@ const handleDelete = async (ids: string[]) => {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.after-sale-tabs {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.el-tabs__content),
|
||||
:deep(.el-tab-pane) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.order-type-tabs {
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
span:nth-child(1) {
|
||||
min-width: 60px;
|
||||
max-width: 100px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
flex: 1;
|
||||
min-width: 50px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -43,7 +43,11 @@
|
||||
<dict-tag :options="hard_door_flow" :value="row.flowStatus" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="errorDesc" label="错误描述" width="120" />
|
||||
<el-table-column prop="errorDesc" label="错误描述" width="120">
|
||||
<template #default="{ row }">
|
||||
<preview-text :text="row.errorDesc" :width="96" title="错误描述" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
<el-table-column label="购买用户">
|
||||
<el-table-column prop="username" label="姓名" width="120" />
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
<template>
|
||||
<el-dialog title="智能柜设置" v-model="visible" width="80%" :close-on-click-modal="true" draggable>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="商品名" prop="name">
|
||||
<el-input v-model="state.queryForm.name" placeholder="商品名" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="商品类型" prop="skuType">
|
||||
<dict-select v-model="state.queryForm.skuType" dict-code="hard_ks_sku_type" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="商品分类" prop="spuCategoryId">
|
||||
<select-hard-spu-category v-model="state.queryForm.spuCategoryId" placeholder="请输入商品分类" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">查询</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<SearchPanel ref="queryRef" :model="state.queryForm" :loading="Boolean(state.loading)" @search="getDataList" @reset="resetQuery">
|
||||
<el-form-item style="width: 300px" label="商品名" prop="name">
|
||||
<el-input v-model="state.queryForm.name" placeholder="商品名" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="商品类型" prop="skuType">
|
||||
<dict-select v-model="state.queryForm.skuType" dict-code="hard_ks_sku_type" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="商品分类" prop="spuCategoryId">
|
||||
<select-hard-spu-category v-model="state.queryForm.spuCategoryId" placeholder="请输入商品分类" />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
|
||||
@@ -287,7 +287,7 @@ const refreshInfo = () => {
|
||||
// 音量设置
|
||||
const doVolumeSetting = async () => {
|
||||
if (volumeSettingRef.value) {
|
||||
volumeSettingRef.value.openDialog(state.deviceDetail.id, state.deviceDetail.mediaVolume, state.deviceDetail.systemVolume)
|
||||
volumeSettingRef.value.openDialog(state.deviceDetail.hardId, state.deviceDetail.mediaVolume, state.deviceDetail.systemVolume)
|
||||
}
|
||||
}
|
||||
const refreshVolume = (data: { hardId: string; mediaVolume: number; systemVolume: number }) => {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,34 +1,44 @@
|
||||
<template>
|
||||
<el-dialog :title="form.id ? '编辑' : '新增'" width="40%" v-model="visible" :close-on-click-modal="false" draggable>
|
||||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="120px"
|
||||
v-loading="loading">
|
||||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="120px" v-loading="loading">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="设备名" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入设备名" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="商户" prop="merchantId">
|
||||
<select-mch :apiParams="{
|
||||
parentIsNull: false
|
||||
}" v-model="form.merchantId" placeholder="请选择商户" />
|
||||
<select-mch v-model="form.merchantId" placeholder="请选择商户" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col v-if="['9', '10', '11'].includes(form.type)" :span="24" class="mb20">
|
||||
<el-form-item label="设备唯一编号" prop="uniqueCode">
|
||||
<el-input v-model="form.uniqueCode" placeholder="请输入设备唯一编号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col v-if="false" :span="24" class="mb20">
|
||||
<el-form-item label="分组" prop="hardGroupId">
|
||||
<select-hard-group v-model="form.hardGroupId" placeholder="分组" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="设备类型" prop="type">
|
||||
<dict-select v-model="form.type" dict-code="hard_type" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="['10', '11'].includes(form.type)" :span="24" class="mb20">
|
||||
<el-form-item label="唯一编号" prop="uniqueCode">
|
||||
<el-input v-model="form.uniqueCode" placeholder="设备唯一编号" />
|
||||
</el-form-item>
|
||||
|
||||
<el-col v-if="form.type === '12'" :span="24" class="mb20">
|
||||
<el-alert
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
title="尔印打印机是无屏 USB 外设,不需要设备唯一编号、账号或独立心跳;新增后请将它绑定到同商户、同部门的食堂或超市终端。"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
@@ -42,10 +52,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="HardManagerDialog">
|
||||
import { useDict } from "/@/hooks/dict"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { getObj, addObj, putObj } from "/@/api/hard/hard-manager"
|
||||
import { rule } from "/@/utils/validate"
|
||||
const emit = defineEmits(["refresh"])
|
||||
|
||||
// 定义变量内容
|
||||
@@ -55,11 +63,11 @@ const loading = ref(false)
|
||||
// 定义字典
|
||||
|
||||
// 提交表单数据
|
||||
const form = reactive({
|
||||
const form = reactive<any>({
|
||||
id: "",
|
||||
uniqueCode: "",
|
||||
merchantId: "",
|
||||
deptId: "",
|
||||
uniqueCode: "",
|
||||
name: "",
|
||||
hardGroupId: "",
|
||||
type: "",
|
||||
@@ -68,9 +76,9 @@ const form = reactive({
|
||||
// 定义校验规则
|
||||
const dataRules = ref({
|
||||
name: [{ required: true, message: "设备名不能为空", trigger: "blur" }],
|
||||
uniqueCode: [{ required: true, message: "设备唯一编号不能为空", trigger: "blur" }],
|
||||
merchantId: [{ required: true, message: "商户不能为空", trigger: "change" }],
|
||||
hardGroupId: [{ required: true, message: "分组不能为空", trigger: "change" }],
|
||||
uniqueCode: [{ required: true, message: "设备唯一编号不能为空", trigger: "change" }],
|
||||
type: [{ required: true, message: "设备类型不能为空", trigger: "change" }],
|
||||
})
|
||||
|
||||
@@ -93,12 +101,16 @@ const openDialog = (id: string) => {
|
||||
|
||||
// 提交
|
||||
const onSubmit = async () => {
|
||||
const valid = await dataFormRef.value.validate().catch(() => { })
|
||||
const valid = await dataFormRef.value.validate().catch(() => {})
|
||||
if (!valid) return false
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
form.id ? await putObj(form) : await addObj(form)
|
||||
const _params = { ...form }
|
||||
if (!["9", "10", "11"].includes(_params.type)) {
|
||||
_params.uniqueCode = null
|
||||
}
|
||||
form.id ? await putObj(_params) : await addObj(_params)
|
||||
useMessage().success(form.id ? "修改成功" : "添加成功")
|
||||
visible.value = false
|
||||
emit("refresh")
|
||||
|
||||
@@ -1,94 +1,167 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 200px" label="设备名" prop="name">
|
||||
<el-input v-model="state.queryForm.name" placeholder="设备名" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="设备类型" prop="type">
|
||||
<dict-select v-model="state.queryForm.type" dict-code="hard_type" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 220px" label="商户" prop="merchantIds">
|
||||
<select-mch v-model="state.queryForm.merchantIds" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">{{ $t("common.queryBtn")
|
||||
}}</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()"
|
||||
v-auth="'hard_hardManager_add'">
|
||||
新 增
|
||||
</el-button>
|
||||
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'hard_hardManager_del'"
|
||||
@click="handleDelete(selectObjs)">
|
||||
删除
|
||||
</el-button>
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="'hard_hardManager_export'"
|
||||
@exportExcel="exportExcel" class="ml10 mr20" style="float: right"
|
||||
@queryTable="getDataList"></right-toolbar>
|
||||
<div class="layout-padding-auto layout-padding-view hard-manager-page">
|
||||
<header class="page-header">
|
||||
<div class="min-w-0">
|
||||
<h1>设备管理</h1>
|
||||
<p>
|
||||
共 {{ Number(state.pagination?.total || 0) }} 台设备,当前页
|
||||
<span class="font-medium text-[var(--el-color-success)]">{{ currentOnlineCount }} 台在线</span>
|
||||
<span class="mx-1 text-[var(--el-border-color)]">/</span>
|
||||
{{ currentOfflineCount }} 台离线
|
||||
</p>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table-column type="selection" width="40" align="center" />
|
||||
<el-table-column type="index" label="#" width="80" />
|
||||
<el-table-column prop="name" width="200" label="设备名" />
|
||||
<el-table-column prop="type" width="100" label="设备类型">
|
||||
<el-button type="primary" icon="Plus" @click="formDialogRef?.openDialog()" v-auth="'hard_hardManager_add'"> 新增设备 </el-button>
|
||||
</header>
|
||||
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
class="hard-manager-search"
|
||||
title="设备筛选"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 240px" label="设备名称" prop="name">
|
||||
<el-input v-model="state.queryForm.name" clearable placeholder="请输入设备名称" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="唯一编号" prop="uniqueCode">
|
||||
<el-input v-model="state.queryForm.uniqueCode" clearable placeholder="请输入设备唯一编号" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 230px" label="设备类型" prop="type">
|
||||
<dict-select v-model="state.queryForm.type" dict-code="hard_type" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 260px" label="所属商户" prop="merchantIds">
|
||||
<select-mch v-model="state.queryForm.merchantIds" placeholder="请选择商户" />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
|
||||
<div class="table-toolbar">
|
||||
<div class="table-toolbar__title">
|
||||
<span aria-hidden="true"></span>
|
||||
<h2>设备列表</h2>
|
||||
<small>共 {{ Number(state.pagination?.total || 0) }} 台</small>
|
||||
</div>
|
||||
<div class="table-toolbar__actions">
|
||||
<el-button
|
||||
plain
|
||||
type="danger"
|
||||
icon="Delete"
|
||||
:disabled="multiple"
|
||||
v-auth="'hard_hardManager_del'"
|
||||
@click="handleDelete(selectObjs)"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
<span v-if="selectObjs.length" class="text-sm text-[var(--el-text-color-secondary)]">
|
||||
已选择 {{ selectObjs.length }} 台设备
|
||||
</span>
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'hard_hardManager_export'"
|
||||
@exportExcel="exportExcel"
|
||||
@queryTable="getDataList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
stripe
|
||||
class="device-table"
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
row-key="id"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
@row-dblclick="openDetails"
|
||||
>
|
||||
<el-table-column type="selection" width="42" align="center" />
|
||||
<el-table-column label="设备" min-width="235">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="hard_type" :value="row.type" />
|
||||
<div class="device-identity">
|
||||
<button type="button" class="device-name" @click="openDetails(row)">
|
||||
{{ row.name || "未命名设备" }}
|
||||
</button>
|
||||
<div class="device-code" :title="isPrinterAsset(row) ? String(row.id) : row.uniqueCode || ''">
|
||||
<span>{{ isPrinterAsset(row) ? "资产 ID" : "编号" }}</span>
|
||||
<strong>{{ isPrinterAsset(row) ? row.id : row.uniqueCode || "尚未绑定" }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="hardGroupName" width="200" label="分类" />
|
||||
<el-table-column prop="merchantName" width="150" label="商户" />
|
||||
<el-table-column prop="deptName" width="200" label="部门" />
|
||||
<el-table-column prop="uniqueCode" width="200" label="设备唯一编号" />
|
||||
<el-table-column prop="mainTopic" width="350" label="topic" />
|
||||
<el-table-column prop="isTakeFood" width="200" label="是否为当前商户的取餐机器">
|
||||
<el-table-column label="类型 / 分组" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<el-switch :disabled="!useUserInfo().userInfos.roleCodes.includes('ROLE_ADMIN')"
|
||||
:loading="switchTakeFoodLoading" active-text="是" inactive-text="否" active-value="1"
|
||||
inactive-value="0" v-model="row.isTakeFood" @change="changeTakeFood($event, row)" />
|
||||
<div class="cell-stack">
|
||||
<dict-tag :options="hard_type" :value="row.type" />
|
||||
<span>{{ row.hardGroupName || "未分组" }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" width="200" label="创建时间" />
|
||||
<el-table-column v-auths="['hard_hardManager_edit', 'hard_hardManager_del']" label="操作" fixed="right"
|
||||
width="400">
|
||||
<el-table-column label="归属" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.type == '4'" icon="InfoFilled" text type="primary"
|
||||
v-auth="'hard_hardManager_edit'" @click="doInfo(row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button icon="edit-pen" text type="primary" v-auth="'hard_hardManager_edit'"
|
||||
@click="formDialogRef.openDialog(row.id)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button v-if="!['10', '11'].includes(row.type)" icon="TrendCharts" link type="primary"
|
||||
v-auth="'hard_hardManager_edit'" @click="mqformDialogRef.openDialog(row.id)">
|
||||
mq设置
|
||||
</el-button>
|
||||
<el-button v-if="!['10', '11'].includes(row.type)" icon="SetUp" link type="primary"
|
||||
v-auth="'hard_hardManager_edit'" @click="faceFormDialogRef.openDialog(row.id)">
|
||||
人脸sdk设置
|
||||
</el-button>
|
||||
<el-button icon="delete" text type="primary" v-auth="'hard_hardManager_del'"
|
||||
@click="handleDelete([row.id])">删除</el-button>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ row.merchantName || "未关联商户" }}</strong>
|
||||
<span>{{ row.deptName || "未关联部门" }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运行状态" min-width="245" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="isTerminalStatusSupported(row)" class="runtime-cell">
|
||||
<div class="runtime-cell__status">
|
||||
<span class="status-dot" :class="row.online ? 'is-online' : 'is-offline'" aria-hidden="true"></span>
|
||||
<strong>{{ row.online ? "在线" : "离线" }}</strong>
|
||||
<span v-if="row.apkVersionName" class="runtime-version">v{{ row.apkVersionName }}</span>
|
||||
</div>
|
||||
<span>{{ row.lastOnlineTime || "尚未上报心跳" }}</span>
|
||||
</div>
|
||||
<div v-else-if="isPrinterAsset(row)" class="runtime-cell">
|
||||
<strong>无屏 USB 外设</strong>
|
||||
<span>由绑定的业务终端代理</span>
|
||||
</div>
|
||||
<span v-else class="text-sm text-[var(--el-text-color-secondary)]">该类型暂不支持状态上报</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="能力" min-width="250">
|
||||
<template #default="{ row }">
|
||||
<div class="capability-list">
|
||||
<span v-if="supportsMq(row)" class="capability-chip" :class="getConfigState(row, 'mq')">MQ</span>
|
||||
<span v-if="supportsFace(row)" class="capability-chip" :class="getConfigState(row, 'face')">人脸</span>
|
||||
<span v-if="supportsPrinter(row)" class="capability-chip" :class="getConfigState(row, 'printer')">打印</span>
|
||||
<span v-if="isSmartCabinet(row)" class="capability-chip is-ready">智能柜</span>
|
||||
<span v-if="row.isTakeFood === '1'" class="capability-chip is-info">取餐机</span>
|
||||
<span v-if="!hasVisibleCapability(row)" class="text-sm text-[var(--el-text-color-placeholder)]">无扩展配置</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="175" sortable="custom" />
|
||||
<el-table-column label="操作" fixed="right" width="248" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="row-actions">
|
||||
<el-button link type="primary" icon="View" @click="openDetails(row)">查看详情</el-button>
|
||||
<el-button v-if="canEdit" link type="primary" icon="EditPen" @click="formDialogRef?.openDialog(String(row.id))"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button v-if="canDelete" link type="danger" icon="Delete" @click="handleDelete([String(row.id)])">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无符合条件的设备" />
|
||||
</template>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
<!-- 编辑、新增 -->
|
||||
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
<face-form-dialog ref="faceFormDialogRef" @refresh="getDataList(false)" />
|
||||
<mq-form-dialog ref="mqformDialogRef" @refresh="getDataList(false)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -97,93 +170,457 @@ import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import * as hardManageApi from "/@/api/hard/hard-manager"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { useDict } from "/@/hooks/dict"
|
||||
import { useUserInfo } from "/@/stores/userInfo"
|
||||
import { auth } from "/@/utils/authFunction"
|
||||
|
||||
const { push } = useRouter()
|
||||
/** 设备分页行,仅声明当前列表实际读取的后端字段。 */
|
||||
interface HardManagerRow {
|
||||
/** 设备雪花主键,路由和接口传输时始终保持字符串形态。 */
|
||||
id: string | number
|
||||
/** 设备展示名称。 */
|
||||
name?: string
|
||||
/** hard_type 字典中的设备类型编码。 */
|
||||
type?: string
|
||||
/** 终端绑定或上报的唯一编号。 */
|
||||
uniqueCode?: string
|
||||
/** 所属硬件分组名称。 */
|
||||
hardGroupName?: string
|
||||
/** 所属商户名称。 */
|
||||
merchantName?: string
|
||||
/** 所属部门名称。 */
|
||||
deptName?: string
|
||||
/** 支持心跳的终端在线状态,不支持的类型返回空值。 */
|
||||
online?: boolean | null
|
||||
/** 服务端最近收到终端心跳的时间。 */
|
||||
lastOnlineTime?: string
|
||||
/** 终端最近上报的 APK 展示版本。 */
|
||||
apkVersionName?: string
|
||||
/** MQTT 服务地址。 */
|
||||
mqSocketAddress?: string
|
||||
/** MQTT 服务端口。 */
|
||||
mqSocketPort?: string
|
||||
/** MQTT 主订阅主题。 */
|
||||
mainTopic?: string
|
||||
/** 人脸 SDK 应用编号。 */
|
||||
faceAppId?: string
|
||||
/** 人脸 SDK 授权密钥。 */
|
||||
faceSdkKey?: string
|
||||
/** 人脸 SDK 激活密钥。 */
|
||||
faceActiveKey?: string
|
||||
/** 业务终端绑定的打印机主键。 */
|
||||
boundPrinterHardId?: string | number
|
||||
/** 打印机资产反向绑定的业务终端主键。 */
|
||||
boundSourceHardId?: string | number
|
||||
/** USB 打印机型号。 */
|
||||
printerModel?: string
|
||||
/** 当前设备是否为商户取餐机,1 是、0 否。 */
|
||||
isTakeFood?: string
|
||||
/** 设备创建时间。 */
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
// 引入组件
|
||||
/** 列表中的配置能力类型,用于统一计算配置完整度。 */
|
||||
type ConfigurationCapability = "mq" | "face" | "printer"
|
||||
|
||||
/** 新增和编辑设备沿用现有业务弹窗。 */
|
||||
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
|
||||
const FaceFormDialog = defineAsyncComponent(() => import("./face-form.vue"))
|
||||
const MqFormDialog = defineAsyncComponent(() => import("./mq-form.vue"))
|
||||
// 定义查询字典
|
||||
/** 设备类型字典用于列表标签展示。 */
|
||||
const { hard_type } = useDict("hard_type")
|
||||
// 定义变量内容
|
||||
const formDialogRef = ref()
|
||||
const faceFormDialogRef = ref()
|
||||
const mqformDialogRef = ref()
|
||||
const switchTakeFoodLoading = ref(false)
|
||||
// 搜索变量
|
||||
/** 页面路由实例用于打开可深链访问的设备详情。 */
|
||||
const router = useRouter()
|
||||
/** 新增、编辑设备弹窗实例。 */
|
||||
const formDialogRef = ref<InstanceType<typeof FormDialog>>()
|
||||
/** 查询面板表单实例。 */
|
||||
const queryRef = ref()
|
||||
/** 查询区域是否展开。 */
|
||||
const showSearch = ref(true)
|
||||
// 多选变量
|
||||
const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
/** 当前批量选中的设备主键,保持后端 Long 的原始字符串精度。 */
|
||||
const selectObjs = ref<string[]>([])
|
||||
/** 未选择设备时禁用批量删除按钮。 */
|
||||
const multiple = computed(() => selectObjs.value.length === 0)
|
||||
/** 支持终端心跳与 APK 状态上报的设备类型编码。 */
|
||||
const terminalStatusSupportedTypes = ["1", "2"]
|
||||
/** 不适用 MQTT 和人脸终端配置的设备类型编码。 */
|
||||
const terminalConfigurationExcludedTypes = ["9", "10", "11", "12"]
|
||||
|
||||
/** 设备列表分页状态与查询函数。 */
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {},
|
||||
pageList: hardManageApi.pageJoin,
|
||||
})
|
||||
|
||||
// table hook
|
||||
/** 列表通用分页、排序、导出和表格样式能力。 */
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state)
|
||||
/** 当前页在线且支持心跳的设备数量。 */
|
||||
const currentOnlineCount = computed(
|
||||
() => ((state.dataList || []) as HardManagerRow[]).filter((row) => isTerminalStatusSupported(row) && row.online === true).length,
|
||||
)
|
||||
/** 当前页离线且支持心跳的设备数量。 */
|
||||
const currentOfflineCount = computed(
|
||||
() => ((state.dataList || []) as HardManagerRow[]).filter((row) => isTerminalStatusSupported(row) && row.online === false).length,
|
||||
)
|
||||
/** 当前账号是否可以编辑设备。 */
|
||||
const canEdit = computed(() => auth("hard_hardManager_edit"))
|
||||
/** 当前账号是否可以删除设备。 */
|
||||
const canDelete = computed(() => auth("hard_hardManager_del"))
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
// 清空搜索条件
|
||||
/** 判断当前设备是否支持在线、版本和终端日志状态展示。 */
|
||||
function isTerminalStatusSupported(row: HardManagerRow): boolean {
|
||||
return terminalStatusSupportedTypes.includes(String(row.type))
|
||||
}
|
||||
|
||||
/** 判断当前设备是否提供 MQTT 运维配置。 */
|
||||
function supportsMq(row: HardManagerRow): boolean {
|
||||
return !terminalConfigurationExcludedTypes.includes(String(row.type))
|
||||
}
|
||||
|
||||
/** 判断当前设备是否提供人脸 SDK 配置。 */
|
||||
function supportsFace(row: HardManagerRow): boolean {
|
||||
return !terminalConfigurationExcludedTypes.includes(String(row.type))
|
||||
}
|
||||
|
||||
/** 判断当前设备是否为无屏 EY_PRINT 打印机资产。 */
|
||||
function isPrinterAsset(row: HardManagerRow): boolean {
|
||||
return String(row.type) === "12"
|
||||
}
|
||||
|
||||
/** 判断设备是否属于可绑定打印机的业务终端或尔印打印机资产。 */
|
||||
function supportsPrinter(row: HardManagerRow): boolean {
|
||||
return ["1", "2", "12"].includes(String(row.type))
|
||||
}
|
||||
|
||||
/** 判断设备是否为生鲜智能柜。 */
|
||||
function isSmartCabinet(row: HardManagerRow): boolean {
|
||||
return String(row.type) === "4"
|
||||
}
|
||||
|
||||
/** 判断能力区是否至少有一个可见标记。 */
|
||||
function hasVisibleCapability(row: HardManagerRow): boolean {
|
||||
return supportsMq(row) || supportsFace(row) || supportsPrinter(row) || isSmartCabinet(row) || row.isTakeFood === "1"
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算能力标签的配置状态,仅判断后端已有字段是否完整,不展示任何密钥原文。
|
||||
*
|
||||
* @param row 当前设备分页行
|
||||
* @param capability 待检查的配置能力
|
||||
* @returns is-ready 表示关键项完整,is-pending 表示仍需配置
|
||||
*/
|
||||
function getConfigState(row: HardManagerRow, capability: ConfigurationCapability): "is-ready" | "is-pending" {
|
||||
if (capability === "mq") {
|
||||
return row.mqSocketAddress && row.mqSocketPort && row.mainTopic ? "is-ready" : "is-pending"
|
||||
}
|
||||
if (capability === "face") {
|
||||
return row.faceAppId && row.faceSdkKey && row.faceActiveKey ? "is-ready" : "is-pending"
|
||||
}
|
||||
return row.boundPrinterHardId || row.boundSourceHardId || row.printerModel ? "is-ready" : "is-pending"
|
||||
}
|
||||
|
||||
/** 重置查询条件、批量选择和当前分页。 */
|
||||
function resetQuery(): void {
|
||||
queryRef.value?.resetFields()
|
||||
// 清空多选
|
||||
selectObjs.value = []
|
||||
getDataList()
|
||||
}
|
||||
|
||||
const changeTakeFood = async (e: any, row: any) => {
|
||||
switchTakeFoodLoading.value = true
|
||||
/** 导出当前查询条件或已选择设备,避免修改响应式查询对象本身。 */
|
||||
function exportExcel(): void {
|
||||
downBlobFile("/hard/hardManager/export", { ...state.queryForm, ids: selectObjs.value }, "设备列表.xlsx")
|
||||
}
|
||||
|
||||
/** 同步表格选择的设备主键。 */
|
||||
function selectionChangHandle(rows: HardManagerRow[]): void {
|
||||
selectObjs.value = rows.map((row) => String(row.id))
|
||||
}
|
||||
|
||||
/** 打开设备详情页,双击表格行与“查看详情”按钮共用该入口。 */
|
||||
function openDetails(row: HardManagerRow): void {
|
||||
router.push({ path: "/hard/hard-manager/details", query: { id: String(row.id) } })
|
||||
}
|
||||
|
||||
/** 二次确认后删除设备,后端仍会执行打印机绑定保护校验。 */
|
||||
async function handleDelete(ids: string[]): Promise<void> {
|
||||
try {
|
||||
const pa = {
|
||||
id: row.id,
|
||||
merchantId: row.merchantId,
|
||||
isTakeFood: e,
|
||||
}
|
||||
await hardManageApi.putObj(pa)
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
useMessage().error(error.msg)
|
||||
} finally {
|
||||
switchTakeFoodLoading.value = false
|
||||
getDataList()
|
||||
}
|
||||
}
|
||||
|
||||
// 导出excel
|
||||
const exportExcel = () => {
|
||||
downBlobFile("/hard/hardManager/export", Object.assign(state.queryForm, { ids: selectObjs }), "设备列表.xlsx")
|
||||
}
|
||||
|
||||
// 多选事件
|
||||
const selectionChangHandle = (objs: { id: string }[]) => {
|
||||
selectObjs.value = objs.map(({ id }) => id)
|
||||
multiple.value = !objs.length
|
||||
}
|
||||
|
||||
// 硬件详情
|
||||
const doInfo = (row: any) => {
|
||||
push({ path: "/hard/hard-door-manager/index", query: { id: row.id } })
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (ids: string[]) => {
|
||||
try {
|
||||
await useMessageBox().confirm("此操作将永久删除")
|
||||
await useMessageBox().confirm("确认删除选中的 " + ids.length + " 台设备?删除后无法恢复。")
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await hardManageApi.delObjs(ids)
|
||||
getDataList()
|
||||
useMessage().success("删除成功")
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg)
|
||||
selectObjs.value = []
|
||||
getDataList()
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "删除失败")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hard-manager-page {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
background: var(--el-fill-color-blank);
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 4px 0 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
min-height: 52px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.table-toolbar__title,
|
||||
.table-toolbar__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.table-toolbar__title {
|
||||
min-width: 0;
|
||||
|
||||
> span {
|
||||
width: 2px;
|
||||
height: 14px;
|
||||
flex: none;
|
||||
border-radius: 1px;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.hard-manager-search.search-panel) {
|
||||
margin-bottom: 0;
|
||||
padding: 12px 20px 4px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:deep(.hard-manager-search .search-panel__header) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.hard-manager-search .search-panel__row) {
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
:deep(.hard-manager-search .el-form-item) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.device-identity,
|
||||
.cell-stack,
|
||||
.runtime-cell {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
line-height: 20px;
|
||||
|
||||
> span,
|
||||
.runtime-version {
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
strong {
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.device-name {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--el-color-primary);
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary-light-3);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border-radius: 2px;
|
||||
outline: 2px solid var(--el-color-primary-light-5);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.device-code {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
> span {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.runtime-cell {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.runtime-cell__status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
flex: none;
|
||||
border-radius: 50%;
|
||||
background: var(--el-text-color-placeholder);
|
||||
|
||||
&.is-online {
|
||||
background: var(--el-color-success);
|
||||
box-shadow: 0 0 0 3px var(--el-color-success-light-9);
|
||||
}
|
||||
|
||||
&.is-offline {
|
||||
background: var(--el-color-danger);
|
||||
box-shadow: 0 0 0 3px var(--el-color-danger-light-9);
|
||||
}
|
||||
}
|
||||
|
||||
.capability-list {
|
||||
display: flex;
|
||||
min-height: 26px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.capability-chip {
|
||||
display: inline-flex;
|
||||
height: 24px;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 4px;
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
|
||||
&.is-ready {
|
||||
border-color: var(--el-color-success-light-7);
|
||||
background: var(--el-color-success-light-9);
|
||||
color: var(--el-color-success-dark-2);
|
||||
}
|
||||
|
||||
&.is-pending {
|
||||
border-style: dashed;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
&.is-info {
|
||||
border-color: var(--el-color-primary-light-7);
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
|
||||
:deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header,
|
||||
.table-toolbar {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page-header > .el-button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.table-toolbar__actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="dialogTitle" width="560px" draggable :close-on-click-modal="false">
|
||||
<el-form v-if="mode === 'bind'" ref="bindFormRef" :model="bindForm" label-width="120px">
|
||||
<el-form-item label="业务终端">
|
||||
<el-input :model-value="sourceName" disabled />
|
||||
</el-form-item>
|
||||
<el-alert class="mb-5" type="info" :closable="false" show-icon>
|
||||
<template #title
|
||||
>此处建立“业务终端 → 尔印 USB 打印机”的一对一关系。打印机没有独立屏幕、账号和心跳;绑定的食堂或超市终端负责发现 USB
|
||||
设备、接收任务、打印并回传结果。</template
|
||||
>
|
||||
</el-alert>
|
||||
<el-form-item label="尔印打印机" prop="printerHardId" :rules="[{ required: true, message: '请选择打印机' }]">
|
||||
<el-select
|
||||
v-model="bindForm.printerHardId"
|
||||
class="w-full"
|
||||
filterable
|
||||
:loading="loading"
|
||||
placeholder="请选择同商户同部门的空闲打印机"
|
||||
>
|
||||
<el-option
|
||||
v-for="printer in availablePrinters"
|
||||
:key="printer.id"
|
||||
:label="`${printer.name || '未命名打印机'}(ID:${printer.id})`"
|
||||
:value="String(printer.id)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<p v-if="!loading && availablePrinters.length === 0" class="text-sm text-[var(--el-text-color-secondary)]">
|
||||
当前部门没有可绑定的空闲尔印打印机资产,请先新增并分配商户部门。
|
||||
</p>
|
||||
</el-form>
|
||||
<el-form v-else ref="configFormRef" :model="configForm" :rules="configRules" label-width="150px">
|
||||
<el-alert class="mb-5" type="info" :closable="false" show-icon>
|
||||
<template #title
|
||||
>推荐在已绑定的食堂或超市终端打开“打印机信息”,点击“上报当前设备”自动回填型号、VID 和
|
||||
PID;管理端手填仅用于现场无法上报时。</template
|
||||
>
|
||||
</el-alert>
|
||||
<el-form-item prop="printerModel">
|
||||
<template #label>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
打印机型号
|
||||
<el-tooltip
|
||||
content="用于运维人员识别实际 USB 打印机。优先由绑定的食堂或超市终端自动上报;手工填写示例:GEZHI / micro-printer。"
|
||||
placement="top"
|
||||
popper-class="max-w-[360px] leading-5"
|
||||
>
|
||||
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]" tabindex="0" aria-label="打印机型号填写说明"
|
||||
><InfoFilled
|
||||
/></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="configForm.printerModel" placeholder="推荐由绑定的业务终端自动上报" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="usbVendorId">
|
||||
<template #label>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
USB VID
|
||||
<el-tooltip
|
||||
content="USB 厂商编号,管理端填写十进制值,范围 0~65535。当前联调设备识别为 10473(十六进制 0x28E9);不确定时请勿猜测,使用终端自动上报。"
|
||||
placement="top"
|
||||
popper-class="max-w-[360px] leading-5"
|
||||
>
|
||||
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]" tabindex="0" aria-label="USB VID 填写说明"
|
||||
><InfoFilled
|
||||
/></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input-number
|
||||
v-model="configForm.usbVendorId"
|
||||
:min="0"
|
||||
:max="65535"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
placeholder="例如 10473"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="usbProductId">
|
||||
<template #label>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
USB PID
|
||||
<el-tooltip
|
||||
content="USB 产品编号,管理端填写十进制值,必须与 VID 同时填写或同时留空。当前联调设备识别为 649(十六进制 0x0289);推荐使用终端自动上报。"
|
||||
placement="top"
|
||||
popper-class="max-w-[360px] leading-5"
|
||||
>
|
||||
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]" tabindex="0" aria-label="USB PID 填写说明"
|
||||
><InfoFilled
|
||||
/></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input-number
|
||||
v-model="configForm.usbProductId"
|
||||
:min="0"
|
||||
:max="65535"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
placeholder="例如 649"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="paperWidth">
|
||||
<template #label>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
纸宽
|
||||
<el-tooltip
|
||||
content="首期只支持 80mm 热敏纸,对应后端图片宽度 576px。此项固定为 80,无需修改。"
|
||||
placement="top"
|
||||
popper-class="max-w-[360px] leading-5"
|
||||
>
|
||||
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]" tabindex="0" aria-label="纸宽配置说明"
|
||||
><InfoFilled
|
||||
/></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input-number v-model="configForm.paperWidth" :min="80" :max="80" controls-position="right" class="w-full" />
|
||||
<span class="ml-2">mm(首期固定)</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
自动切刀
|
||||
<el-tooltip
|
||||
content="打印完成后发送切纸命令。打印机带自动切刀时保持开启;若型号没有切刀,或测试打印在切纸阶段报错,再关闭此项。"
|
||||
placement="top"
|
||||
popper-class="max-w-[360px] leading-5"
|
||||
>
|
||||
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]" tabindex="0" aria-label="自动切刀配置说明"
|
||||
><InfoFilled
|
||||
/></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-switch v-model="configForm.autoCut" active-value="1" inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
图片指令模式
|
||||
<el-tooltip
|
||||
content="控制 PNG 图片转换成哪种 ESC/POS 指令。优先使用 GS v 0;只有真机测试出现白纸、缺行或图片不完整时,才切换 ESC * 兼容模式并重新测试。"
|
||||
placement="top"
|
||||
popper-class="max-w-[360px] leading-5"
|
||||
>
|
||||
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]" tabindex="0" aria-label="图片指令模式说明"
|
||||
><InfoFilled
|
||||
/></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-radio-group v-model="configForm.imageMode"
|
||||
><el-radio label="GS_V0">GS v 0(默认)</el-radio><el-radio label="ESC_STAR">ESC *(兼容回退)</el-radio></el-radio-group
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" :disabled="mode === 'bind' && availablePrinters.length === 0" @click="submit"
|
||||
>保存</el-button
|
||||
>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="HardPrinterDialog">
|
||||
import { InfoFilled } from "@element-plus/icons-vue"
|
||||
import * as printerApi from "/@/api/hard/hard-printer"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
|
||||
const emit = defineEmits(["refresh"])
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const mode = ref<"bind" | "config">("bind")
|
||||
const sourceName = ref("")
|
||||
/** 来源业务终端雪花 ID,始终按字符串传递以保留完整精度。 */
|
||||
const sourceHardId = ref<printerApi.HardwareId | null>(null)
|
||||
/** 目标打印机资产雪花 ID,始终按字符串传递以保留完整精度。 */
|
||||
const printerHardId = ref<printerApi.HardwareId | null>(null)
|
||||
const availablePrinters = ref<any[]>([])
|
||||
const bindFormRef = ref()
|
||||
const configFormRef = ref()
|
||||
/** 当前待提交的一对一打印机绑定。 */
|
||||
const bindForm = reactive<{ printerHardId: printerApi.HardwareId | null }>({ printerHardId: null })
|
||||
const configForm = reactive<printerApi.HardPrinterConfig>({
|
||||
printerHardId: "",
|
||||
printerModel: "",
|
||||
usbVendorId: undefined,
|
||||
usbProductId: undefined,
|
||||
paperWidth: 80,
|
||||
autoCut: "1",
|
||||
imageMode: "GS_V0",
|
||||
})
|
||||
const validateUsbPair = (_rule: unknown, _value: unknown, callback: (error?: Error) => void) => {
|
||||
const hasVid = configForm.usbVendorId !== undefined && configForm.usbVendorId !== null
|
||||
const hasPid = configForm.usbProductId !== undefined && configForm.usbProductId !== null
|
||||
callback(hasVid === hasPid ? undefined : new Error("USB VID 和 PID 必须同时填写或同时留空"))
|
||||
}
|
||||
const configRules = {
|
||||
usbVendorId: [{ validator: validateUsbPair, trigger: "change" }],
|
||||
usbProductId: [{ validator: validateUsbPair, trigger: "change" }],
|
||||
paperWidth: [{ required: true, message: "纸宽必须为80mm", trigger: "change" }],
|
||||
}
|
||||
const dialogTitle = computed(() => (mode.value === "bind" ? "绑定尔印打印机" : "打印机配置"))
|
||||
|
||||
/** 恢复打印配置默认值,避免连续打开不同设备时残留上一台打印机的参数。 */
|
||||
const resetConfig = (hardId: printerApi.HardwareId) => {
|
||||
Object.assign(configForm, {
|
||||
id: undefined,
|
||||
printerHardId: hardId,
|
||||
printerModel: "",
|
||||
usbVendorId: undefined,
|
||||
usbProductId: undefined,
|
||||
paperWidth: 80,
|
||||
autoCut: "1",
|
||||
imageMode: "GS_V0",
|
||||
})
|
||||
}
|
||||
|
||||
/** 打开来源终端绑定弹窗,并使用未经过数值转换的雪花 ID 查询候选打印机。 */
|
||||
const openBind = async (row: any) => {
|
||||
mode.value = "bind"
|
||||
const hardId = String(row.id)
|
||||
sourceHardId.value = hardId
|
||||
sourceName.value = row.name || `设备 ${row.id}`
|
||||
bindForm.printerHardId = null
|
||||
visible.value = true
|
||||
loading.value = true
|
||||
try {
|
||||
const response: any = await printerApi.listAvailable(hardId)
|
||||
availablePrinters.value = response.data || []
|
||||
} catch (error: any) {
|
||||
availablePrinters.value = []
|
||||
useMessage().error(error?.msg || "读取可绑定打印机失败")
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开 EY_PRINT 参数弹窗,并保持目标设备雪花 ID 的字符串形态。 */
|
||||
const openConfig = async (row: any) => {
|
||||
mode.value = "config"
|
||||
const hardId = String(row.id)
|
||||
printerHardId.value = hardId
|
||||
resetConfig(hardId)
|
||||
visible.value = true
|
||||
loading.value = true
|
||||
try {
|
||||
const response: any = await printerApi.getConfig(hardId)
|
||||
Object.assign(configForm, response.data || {}, { printerHardId: hardId })
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "读取打印机配置失败")
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 校验并提交绑定或配置,设备主键保持字符串以便后端无损转换为 Long。 */
|
||||
const submit = async () => {
|
||||
const formRef = mode.value === "bind" ? bindFormRef.value : configFormRef.value
|
||||
const valid = await formRef?.validate?.().catch(() => false)
|
||||
if (valid === false) return
|
||||
loading.value = true
|
||||
try {
|
||||
if (mode.value === "bind") {
|
||||
if (!sourceHardId.value || !bindForm.printerHardId) {
|
||||
useMessage().warning("请选择尔印打印机")
|
||||
return
|
||||
}
|
||||
await printerApi.bind({ sourceHardId: sourceHardId.value, printerHardId: bindForm.printerHardId })
|
||||
useMessage().success("绑定成功,请在该业务终端的“打印机信息”页面刷新并上报 USB 设备")
|
||||
} else {
|
||||
await printerApi.saveConfig({ ...configForm, printerHardId: printerHardId.value || configForm.printerHardId })
|
||||
useMessage().success("配置保存成功")
|
||||
}
|
||||
visible.value = false
|
||||
emit("refresh")
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "保存失败")
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ openBind, openConfig })
|
||||
</script>
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
<DraggableDepMchTree ref="draggableDepMchTreeRef" placeholder="请输入用户部门名称"
|
||||
mch-placeholder="请输入商户部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false" :props="{
|
||||
label: 'name',
|
||||
@@ -58,7 +59,7 @@
|
||||
<hard-sh-by-day v-if="activeTab === 'MCH'" ref="hardShByDayRef" />
|
||||
<hard-sh-by-month v-if="activeTab === 'MONTH'" ref="hardShByMonthRef" />
|
||||
</template>
|
||||
</DraggableDepTree>
|
||||
</DraggableDepMchTree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -81,7 +82,7 @@ const hardShByMonthRef = ref()
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
|
||||
const draggableDepTreeRef = ref()
|
||||
const draggableDepMchTreeRef = ref()
|
||||
|
||||
// tab变量
|
||||
const tabPane = ref([
|
||||
@@ -179,15 +180,23 @@ const resetQuery = () => {
|
||||
state.queryForm = { ...defaultQueryForm }
|
||||
state.queryForm.deptId = ""
|
||||
state.queryForm.userDeptIds = []
|
||||
draggableDepTreeRef.value.resetData()
|
||||
state.queryForm.merchantDeptIds = []
|
||||
draggableDepMchTreeRef.value.resetData()
|
||||
cancel()
|
||||
}
|
||||
|
||||
// 树节点选择时
|
||||
const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepTree: any) => {
|
||||
state.queryForm.deptId = curClickedNode.id
|
||||
state.queryForm.userDeptIds = checkedTreeNodes.map((item: any) => item.id)
|
||||
state.queryForm.isInclude = !isDeepTree
|
||||
const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepTree: any, tab?: string) => {
|
||||
if (tab === "MCH_DEPT") {
|
||||
// 商户部门tab:勾选的商户部门ids + 商户部门包含/不包含
|
||||
state.queryForm.merchantDeptIds = checkedTreeNodes.map((item: any) => item.id)
|
||||
state.queryForm.isIncludeMerchantDept = !isDeepTree
|
||||
} else {
|
||||
// 用户部门tab:点击的部门 + 勾选的部门ids + 用户部门包含/不包含
|
||||
state.queryForm.deptId = curClickedNode.id
|
||||
state.queryForm.userDeptIds = checkedTreeNodes.map((item: any) => item.id)
|
||||
state.queryForm.isInclude = !isDeepTree
|
||||
}
|
||||
search()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<section class="flex h-full min-h-0 flex-col" aria-label="终端设备日志列表">
|
||||
<header class="flex items-center justify-between border-b border-br-light px-3 py-2">
|
||||
<div>
|
||||
<div class="font-medium text-[var(--el-text-color-primary)]">终端设备</div>
|
||||
<div class="mt-0.5 text-xs text-[var(--el-text-color-secondary)]">共 {{ total }} 台有日志设备</div>
|
||||
</div>
|
||||
<el-button text icon="Refresh" :loading="loading" aria-label="刷新设备列表" @click="$emit('refresh')" />
|
||||
</header>
|
||||
|
||||
<el-scrollbar v-loading="loading" class="min-h-0 flex-1">
|
||||
<div v-if="devices.length" class="space-y-2 p-2">
|
||||
<button
|
||||
v-for="device in devices"
|
||||
:key="device.hardId"
|
||||
type="button"
|
||||
class="device-card w-full rounded border p-3 text-left transition-colors"
|
||||
:class="{ 'is-active': String(device.hardId) === String(selectedHardId) }"
|
||||
@click="$emit('select', device)"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<span class="truncate font-medium text-[var(--el-text-color-primary)]">{{ device.hardName || "未命名设备" }}</span>
|
||||
<el-tag size="small" effect="plain">{{ device.fileCount }} 个文件</el-tag>
|
||||
</div>
|
||||
<div class="mt-1 truncate text-xs text-[var(--el-text-color-regular)]">编号:{{ device.uniqueCode || "-" }}</div>
|
||||
<div class="mt-2 text-xs text-[var(--el-text-color-secondary)]">最近上传:{{ formatTime(device.latestUploadTime) }}</div>
|
||||
</button>
|
||||
</div>
|
||||
<el-empty v-else :image-size="72" description="暂无符合条件的终端日志" />
|
||||
</el-scrollbar>
|
||||
|
||||
<footer v-if="total > size" class="flex justify-center border-t border-br-light px-2 py-2">
|
||||
<el-pagination
|
||||
small
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:page-size="size"
|
||||
:current-page="current"
|
||||
:total="total"
|
||||
@current-change="$emit('page-change', $event)"
|
||||
/>
|
||||
</footer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TerminalLogDevice } from "/@/api/hard/hard-terminal-log-file"
|
||||
import { formatDate } from "/@/utils/formatTime"
|
||||
|
||||
defineProps<{
|
||||
devices: TerminalLogDevice[]
|
||||
loading: boolean
|
||||
total: number
|
||||
current: number
|
||||
size: number
|
||||
selectedHardId?: string | number
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(event: "select", device: TerminalLogDevice): void
|
||||
(event: "refresh"): void
|
||||
(event: "page-change", page: number): void
|
||||
}>()
|
||||
|
||||
function formatTime(value?: string) {
|
||||
if (!value) return "暂无"
|
||||
const date = new Date(value)
|
||||
return Number.isNaN(date.getTime()) ? value : formatDate(date, "YYYY-mm-dd HH:MM:SS")
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.device-card {
|
||||
border-color: var(--el-border-color-light);
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.device-card:hover {
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.device-card.is-active {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary-light-7);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<section class="flex h-full min-h-0 flex-col" aria-label="终端日志文件树">
|
||||
<header class="flex items-center justify-between border-b border-br-light px-3 py-2">
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium text-[var(--el-text-color-primary)]">日志文件</div>
|
||||
<div class="mt-0.5 truncate text-xs text-[var(--el-text-color-secondary)]">{{ deviceName || "请先选择设备" }}</div>
|
||||
</div>
|
||||
<el-button text icon="Refresh" :disabled="!hardId" :loading="loading" aria-label="刷新日志文件树" @click="$emit('refresh')" />
|
||||
</header>
|
||||
|
||||
<el-scrollbar v-loading="loading" class="min-h-0 flex-1">
|
||||
<el-tree
|
||||
v-if="hardId && tree.length"
|
||||
class="terminal-log-tree px-2 py-2"
|
||||
:data="tree"
|
||||
node-key="key"
|
||||
:current-node-key="selectedKey"
|
||||
:expand-on-click-node="false"
|
||||
:highlight-current="true"
|
||||
:default-expand-all="true"
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2 pr-2">
|
||||
<el-icon class="shrink-0 text-[var(--el-text-color-secondary)]">
|
||||
<Folder v-if="data.directory" />
|
||||
<Document v-else />
|
||||
</el-icon>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate">{{ data.label }}</div>
|
||||
<div v-if="!data.directory" class="truncate text-xs text-[var(--el-text-color-secondary)]">
|
||||
{{ formatSize(data.fileSize) }} · {{ formatTime(data.uploadTime) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
<el-empty v-else :image-size="72" :description="hardId ? '该设备暂无符合条件的日志' : '请先选择终端设备'" />
|
||||
</el-scrollbar>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TerminalLogTreeNode } from "/@/api/hard/hard-terminal-log-file"
|
||||
import { formatDate } from "/@/utils/formatTime"
|
||||
|
||||
defineProps<{
|
||||
tree: TerminalLogTreeNode[]
|
||||
loading: boolean
|
||||
hardId?: string | number
|
||||
deviceName?: string
|
||||
selectedKey?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "select", node: TerminalLogTreeNode): void
|
||||
(event: "refresh"): void
|
||||
}>()
|
||||
|
||||
function handleNodeClick(node: TerminalLogTreeNode) {
|
||||
if (!node.directory && node.fileId != null) emit("select", node)
|
||||
}
|
||||
|
||||
function formatSize(bytes?: number) {
|
||||
if (bytes == null) return "-"
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function formatTime(value?: string) {
|
||||
if (!value) return "-"
|
||||
const date = new Date(value)
|
||||
return Number.isNaN(date.getTime()) ? value : formatDate(date, "YYYY-mm-dd HH:MM:SS")
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.terminal-log-tree {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.terminal-log-tree:deep(.el-tree-node__content) {
|
||||
height: auto;
|
||||
min-height: 40px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :close-on-click-modal="false" draggable>
|
||||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="设备ID(hard_manager.id)" prop="hardId">
|
||||
<el-input v-model="form.hardId" placeholder="请输入设备ID(hard_manager.id)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="上传时设备名称快照" prop="hardName">
|
||||
<el-input v-model="form.hardName" placeholder="请输入上传时设备名称快照" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="上传时设备唯一编号快照" prop="uniqueCode">
|
||||
<el-input v-model="form.uniqueCode" placeholder="请输入上传时设备唯一编号快照" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="上传时设备类型快照" prop="hardType">
|
||||
<el-input v-model="form.hardType" placeholder="请输入上传时设备类型快照" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="上传时商户ID快照" prop="merchantId">
|
||||
<el-input v-model="form.merchantId" placeholder="请输入上传时商户ID快照" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="上传时部门ID快照" prop="deptId">
|
||||
<el-input v-model="form.deptId" placeholder="请输入上传时部门ID快照" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="日志所属日期" prop="logDate">
|
||||
<el-input v-model="form.logDate" placeholder="请输入日志所属日期" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="终端日志根目录下的相对路径" prop="relativePath">
|
||||
<el-input v-model="form.relativePath" placeholder="请输入终端日志根目录下的相对路径" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="原日志文件名" prop="originalName">
|
||||
<el-input v-model="form.originalName" placeholder="请输入原日志文件名" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="文件大小(字节)" prop="fileSize">
|
||||
<el-input v-model="form.fileSize" placeholder="请输入文件大小(字节)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="文件内容哈希(当前使用MD5)" prop="fileHash">
|
||||
<el-input v-model="form.fileHash" placeholder="请输入文件内容哈希(当前使用MD5)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="终端本地文件最后修改时间" prop="fileModifiedTime">
|
||||
<el-input v-model="form.fileModifiedTime" placeholder="请输入终端本地文件最后修改时间" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="文件中心sys_file记录ID" prop="sysFileId">
|
||||
<el-input v-model="form.sysFileId" placeholder="请输入文件中心sys_file记录ID" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="OSS对象文件名" prop="storageFileName">
|
||||
<el-input v-model="form.storageFileName" placeholder="请输入OSS对象文件名" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="文件访问相对地址" prop="storageUrl">
|
||||
<el-input v-model="form.storageUrl" placeholder="请输入文件访问相对地址" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="上传时应用版本" prop="appVersion">
|
||||
<el-input v-model="form.appVersion" placeholder="请输入上传时应用版本" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="日志脱敏规则版本" prop="sanitizeVersion">
|
||||
<el-input v-model="form.sanitizeVersion" placeholder="请输入日志脱敏规则版本" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="最近成功上传时间" prop="uploadTime">
|
||||
<el-input v-model="form.uploadTime" placeholder="请输入最近成功上传时间" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="成功上传次数" prop="uploadCount">
|
||||
<el-input v-model="form.uploadCount" placeholder="请输入成功上传次数" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="HardTerminalLogFileDialog">
|
||||
import { useDict } from '/@/hooks/dict';
|
||||
import { useMessage } from "/@/hooks/message";
|
||||
import * as hardTerminalLogFile from "/@/api/hard/hard-terminal-log-file";
|
||||
import { rule } from '/@/utils/validate';
|
||||
const emit = defineEmits(['refresh']);
|
||||
|
||||
// 定义变量内容
|
||||
const dataFormRef = ref();
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
// 定义字典
|
||||
|
||||
// 提交表单数据
|
||||
const form = reactive({
|
||||
id: '',
|
||||
hardId: '',
|
||||
hardName: '',
|
||||
uniqueCode: '',
|
||||
hardType: '',
|
||||
merchantId: '',
|
||||
deptId: '',
|
||||
logDate: '',
|
||||
relativePath: '',
|
||||
originalName: '',
|
||||
fileSize: '',
|
||||
fileHash: '',
|
||||
fileModifiedTime: '',
|
||||
sysFileId: '',
|
||||
storageFileName: '',
|
||||
storageUrl: '',
|
||||
appVersion: '',
|
||||
sanitizeVersion: '',
|
||||
uploadTime: '',
|
||||
uploadCount: '',
|
||||
remark: '',
|
||||
});
|
||||
|
||||
// 定义校验规则
|
||||
const dataRules = ref({
|
||||
})
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = (id: string) => {
|
||||
visible.value = true
|
||||
form.id = ''
|
||||
|
||||
// 重置表单数据
|
||||
nextTick(() => {
|
||||
dataFormRef.value?.resetFields();
|
||||
});
|
||||
|
||||
// 获取hardTerminalLogFile信息
|
||||
if (id) {
|
||||
form.id = id
|
||||
gethardTerminalLogFileData(id)
|
||||
}
|
||||
};
|
||||
|
||||
// 提交
|
||||
const onSubmit = async () => {
|
||||
const valid = await dataFormRef.value.validate().catch(() => { });
|
||||
if (!valid) return false;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
form.id ? await hardTerminalLogFile.putObj(form) : await hardTerminalLogFile.addObj(form);
|
||||
useMessage().success(form.id ? '修改成功' : '添加成功');
|
||||
visible.value = false;
|
||||
emit('refresh');
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 初始化表单数据
|
||||
const gethardTerminalLogFileData = (id: string) => {
|
||||
// 获取数据
|
||||
loading.value = true
|
||||
getObj(id).then((res: any) => {
|
||||
Object.assign(form, res.data)
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
};
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openDialog
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,521 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto">
|
||||
<div class="layout-padding-view flex min-h-0 flex-col">
|
||||
<section class="border-b border-br-light px-3 py-3">
|
||||
<el-form
|
||||
ref="queryRef"
|
||||
:model="queryForm"
|
||||
label-width="72px"
|
||||
class="terminal-log-query grid grid-cols-12 items-start gap-x-4 gap-y-2"
|
||||
>
|
||||
<el-form-item class="col-span-12 md:col-span-6 lg:col-span-3" label="设备名称" prop="hardName">
|
||||
<el-input v-model="queryForm.hardName" clearable placeholder="请输入设备名称" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item class="col-span-12 md:col-span-6 lg:col-span-3" label="唯一编号" prop="uniqueCode">
|
||||
<el-input v-model="queryForm.uniqueCode" clearable placeholder="请输入设备唯一编号" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item class="col-span-12 md:col-span-6 lg:col-span-3" label="商户" prop="merchantId">
|
||||
<select-mch v-model="queryForm.merchantId" placeholder="请选择商户" />
|
||||
</el-form-item>
|
||||
<el-form-item class="col-span-12 md:col-span-6 lg:col-span-3" label="部门" prop="deptId">
|
||||
<select-dept v-model="queryForm.deptId" placeholder="请选择部门" />
|
||||
</el-form-item>
|
||||
<el-form-item class="col-span-12 md:col-span-6 lg:col-span-4" label="日志日期" prop="logDateRange">
|
||||
<el-date-picker
|
||||
v-model="queryForm.logDateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="col-span-12 md:col-span-6 lg:col-span-5" label="上传时间" prop="uploadTimeRange">
|
||||
<el-date-picker
|
||||
v-model="queryForm.uploadTimeRange"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="terminal-log-query__actions col-span-12 lg:col-span-3">
|
||||
<el-button type="primary" icon="Search" :loading="deviceLoading" @click="handleQuery">查询</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="mt-1 flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-button v-if="isNarrow" icon="Monitor" @click="deviceDrawerVisible = true">选择设备</el-button>
|
||||
<el-button v-if="isNarrow" icon="Files" :disabled="!selectedDevice" @click="fileDrawerVisible = true">文件树</el-button>
|
||||
<el-tag v-if="selectedDevice" type="info" effect="plain">
|
||||
当前设备:{{ selectedDevice.hardName }}({{ selectedDevice.uniqueCode }})
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-button v-auth="'hard_hardTerminalLogFile_pull'" icon="Aim" @click="openPullDialog">选择其他设备拉取</el-button>
|
||||
<el-button
|
||||
v-auth="'hard_hardTerminalLogFile_pull'"
|
||||
type="primary"
|
||||
icon="Promotion"
|
||||
:disabled="!selectedDevice"
|
||||
:loading="pulling"
|
||||
@click="pullSelectedDevice"
|
||||
>
|
||||
立即拉取当前日志
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="min-h-0 flex-1">
|
||||
<splitpanes v-if="!isNarrow" class="terminal-log-split">
|
||||
<pane :size="17" :min-size="15" :max-size="22">
|
||||
<device-panel
|
||||
:devices="devices"
|
||||
:loading="deviceLoading"
|
||||
:total="deviceTotal"
|
||||
:current="devicePage"
|
||||
:size="devicePageSize"
|
||||
:selected-hard-id="selectedDevice?.hardId"
|
||||
@select="selectDevice"
|
||||
@refresh="loadDevices(true)"
|
||||
@page-change="changeDevicePage"
|
||||
/>
|
||||
</pane>
|
||||
<pane :size="20" :min-size="17" :max-size="26">
|
||||
<file-tree-panel
|
||||
:tree="fileTree"
|
||||
:loading="treeLoading"
|
||||
:hard-id="selectedDevice?.hardId"
|
||||
:device-name="selectedDevice?.hardName"
|
||||
:selected-key="selectedFile?.key"
|
||||
@select="selectFile"
|
||||
@refresh="loadFileTree"
|
||||
/>
|
||||
</pane>
|
||||
<pane :size="63" :min-size="48">
|
||||
<preview-panel
|
||||
v-model:keyword="keyword"
|
||||
:preview="preview"
|
||||
:loading="previewLoading"
|
||||
:can-preview="canPreview"
|
||||
:has-device="Boolean(selectedDevice)"
|
||||
:has-file="Boolean(selectedFile)"
|
||||
@copy="copyPreview"
|
||||
@refresh="refreshPreview"
|
||||
@download="downloadCurrent"
|
||||
/>
|
||||
</pane>
|
||||
</splitpanes>
|
||||
<preview-panel
|
||||
v-else
|
||||
v-model:keyword="keyword"
|
||||
:preview="preview"
|
||||
:loading="previewLoading"
|
||||
:can-preview="canPreview"
|
||||
:has-device="Boolean(selectedDevice)"
|
||||
:has-file="Boolean(selectedFile)"
|
||||
@copy="copyPreview"
|
||||
@refresh="refreshPreview"
|
||||
@download="downloadCurrent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="deviceDrawerVisible" title="选择终端设备" direction="ltr" size="360px">
|
||||
<device-panel
|
||||
:devices="devices"
|
||||
:loading="deviceLoading"
|
||||
:total="deviceTotal"
|
||||
:current="devicePage"
|
||||
:size="devicePageSize"
|
||||
:selected-hard-id="selectedDevice?.hardId"
|
||||
@select="selectDeviceFromDrawer"
|
||||
@refresh="loadDevices(true)"
|
||||
@page-change="changeDevicePage"
|
||||
/>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer v-model="fileDrawerVisible" title="日志文件树" direction="ltr" size="420px">
|
||||
<file-tree-panel
|
||||
:tree="fileTree"
|
||||
:loading="treeLoading"
|
||||
:hard-id="selectedDevice?.hardId"
|
||||
:device-name="selectedDevice?.hardName"
|
||||
:selected-key="selectedFile?.key"
|
||||
@select="selectFileFromDrawer"
|
||||
@refresh="loadFileTree"
|
||||
/>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog v-model="pullDialogVisible" title="拉取无历史日志设备的当前日志" width="520px" destroy-on-close>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="指令发送后,客户端会等待支付和识别结束再上传;日志无变化时列表不会更新。"
|
||||
/>
|
||||
<el-form label-width="88px" class="mt-4">
|
||||
<el-form-item label="终端设备">
|
||||
<select-hard v-model="pullHardId" placeholder="请选择要拉取日志的终端" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="pullDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!pullHardId" :loading="pulling" @click="pullOtherDevice">发送拉取指令</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="systemHardTerminalLogFile">
|
||||
import { useMediaQuery } from "@vueuse/core"
|
||||
import * as terminalLogApi from "/@/api/hard/hard-terminal-log-file"
|
||||
import type { TerminalLogDevice, TerminalLogPreview, TerminalLogQuery, TerminalLogTreeNode } from "/@/api/hard/hard-terminal-log-file"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { auth } from "/@/utils/authFunction"
|
||||
import commonFunction from "/@/utils/commonFunction"
|
||||
import DevicePanel from "./device-panel.vue"
|
||||
import FileTreePanel from "./file-tree-panel.vue"
|
||||
import PreviewPanel from "./preview-panel.vue"
|
||||
|
||||
const PREVIEW_PERMISSION = "hard_hardTerminalLogFile_preview"
|
||||
const DOWNLOAD_PERMISSION = "hard_hardTerminalLogFile_down"
|
||||
const PULL_PERMISSION = "hard_hardTerminalLogFile_pull"
|
||||
const POLL_INTERVAL_MS = 3000
|
||||
const POLL_MAX_COUNT = 20
|
||||
|
||||
const queryRef = ref()
|
||||
const queryForm = reactive({
|
||||
hardName: "",
|
||||
uniqueCode: "",
|
||||
merchantId: undefined as string | number | undefined,
|
||||
deptId: undefined as string | number | undefined,
|
||||
logDateRange: [] as string[],
|
||||
uploadTimeRange: [] as string[],
|
||||
})
|
||||
|
||||
const isNarrow = useMediaQuery("(max-width: 1280px)")
|
||||
const devices = ref<TerminalLogDevice[]>([])
|
||||
const deviceLoading = ref(false)
|
||||
const deviceTotal = ref(0)
|
||||
const devicePage = ref(1)
|
||||
const devicePageSize = 12
|
||||
const selectedDevice = ref<TerminalLogDevice>()
|
||||
const fileTree = ref<TerminalLogTreeNode[]>([])
|
||||
const treeLoading = ref(false)
|
||||
const selectedFile = ref<TerminalLogTreeNode>()
|
||||
const preview = ref<TerminalLogPreview>()
|
||||
const previewLoading = ref(false)
|
||||
const keyword = ref("")
|
||||
const pulling = ref(false)
|
||||
const pullHardId = ref<string | number>()
|
||||
const pullDialogVisible = ref(false)
|
||||
const deviceDrawerVisible = ref(false)
|
||||
const fileDrawerVisible = ref(false)
|
||||
let pollTimer: ReturnType<typeof setTimeout> | undefined
|
||||
const canPreview = computed(() => auth(PREVIEW_PERMISSION))
|
||||
|
||||
const { copyText } = commonFunction()
|
||||
|
||||
function buildQuery(): TerminalLogQuery {
|
||||
return {
|
||||
hardName: queryForm.hardName || undefined,
|
||||
uniqueCode: queryForm.uniqueCode || undefined,
|
||||
merchantId: queryForm.merchantId,
|
||||
deptId: queryForm.deptId,
|
||||
logDateStart: queryForm.logDateRange?.[0],
|
||||
logDateEnd: queryForm.logDateRange?.[1],
|
||||
uploadTimeStart: queryForm.uploadTimeRange?.[0],
|
||||
uploadTimeEnd: queryForm.uploadTimeRange?.[1],
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDevices(keepSelection = false) {
|
||||
deviceLoading.value = true
|
||||
try {
|
||||
const { data } = await terminalLogApi.fetchDevicePage({
|
||||
...buildQuery(),
|
||||
current: devicePage.value,
|
||||
size: devicePageSize,
|
||||
})
|
||||
devices.value = data?.records ?? []
|
||||
deviceTotal.value = Number(data?.total ?? 0)
|
||||
if (keepSelection && selectedDevice.value) {
|
||||
const refreshed = devices.value.find((item) => String(item.hardId) === String(selectedDevice.value?.hardId))
|
||||
if (refreshed) {
|
||||
selectedDevice.value = refreshed
|
||||
return
|
||||
}
|
||||
await selectDevice(devices.value[0])
|
||||
return
|
||||
}
|
||||
if (!devices.value.some((item) => String(item.hardId) === String(selectedDevice.value?.hardId))) {
|
||||
await selectDevice(devices.value[0])
|
||||
}
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "获取终端日志设备失败")
|
||||
} finally {
|
||||
deviceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectDevice(device?: TerminalLogDevice) {
|
||||
selectedDevice.value = device
|
||||
selectedFile.value = undefined
|
||||
preview.value = undefined
|
||||
keyword.value = ""
|
||||
fileTree.value = []
|
||||
if (device) await loadFileTree()
|
||||
}
|
||||
|
||||
async function loadFileTree() {
|
||||
if (!selectedDevice.value) return
|
||||
treeLoading.value = true
|
||||
try {
|
||||
const { data } = await terminalLogApi.fetchFileTree({
|
||||
...buildQuery(),
|
||||
hardId: selectedDevice.value.hardId,
|
||||
})
|
||||
fileTree.value = data ?? []
|
||||
const refreshedFile = selectedFile.value
|
||||
? findTreeFile(fileTree.value, selectedFile.value.key)
|
||||
: undefined
|
||||
if (refreshedFile) {
|
||||
selectedFile.value = refreshedFile
|
||||
} else {
|
||||
selectedFile.value = undefined
|
||||
preview.value = undefined
|
||||
keyword.value = ""
|
||||
}
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "获取日志文件树失败")
|
||||
} finally {
|
||||
treeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function findTreeFile(nodes: TerminalLogTreeNode[], key: string): TerminalLogTreeNode | undefined {
|
||||
for (const node of nodes) {
|
||||
if (node.key === key && !node.directory) return node
|
||||
const child = findTreeFile(node.children ?? [], key)
|
||||
if (child) return child
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function selectFile(file: TerminalLogTreeNode) {
|
||||
selectedFile.value = file
|
||||
preview.value = undefined
|
||||
keyword.value = ""
|
||||
if (!canPreview.value) {
|
||||
useMessage().warning(auth(DOWNLOAD_PERMISSION) ? "暂无日志预览权限,可使用完整下载" : "暂无日志预览权限")
|
||||
return
|
||||
}
|
||||
await refreshPreview()
|
||||
}
|
||||
|
||||
async function refreshPreview() {
|
||||
if (selectedFile.value?.fileId == null || !canPreview.value) return
|
||||
previewLoading.value = true
|
||||
try {
|
||||
const { data } = await terminalLogApi.fetchPreview(selectedFile.value.fileId)
|
||||
preview.value = data
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "读取日志预览失败")
|
||||
} finally {
|
||||
previewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function copyPreview() {
|
||||
if (!preview.value?.content) return
|
||||
const normalizedKeyword = keyword.value.trim().toLocaleLowerCase()
|
||||
const content = normalizedKeyword
|
||||
? preview.value.content
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.toLocaleLowerCase().includes(normalizedKeyword))
|
||||
.join("\n")
|
||||
: preview.value.content
|
||||
copyText(content)
|
||||
}
|
||||
|
||||
async function downloadCurrent() {
|
||||
if (selectedFile.value?.fileId == null || !auth(DOWNLOAD_PERMISSION)) return
|
||||
try {
|
||||
const blob = await terminalLogApi.downloadFile(selectedFile.value.fileId)
|
||||
saveLogBlob(blob, preview.value?.originalName || selectedFile.value.label)
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "日志下载失败")
|
||||
}
|
||||
}
|
||||
|
||||
function saveLogBlob(blob: Blob, fileName: string) {
|
||||
const link = document.createElement("a")
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = fileName.replace(/[<>:"/\\|?*]/g, "_").substring(0, 255)
|
||||
link.style.display = "none"
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(link.href)
|
||||
link.remove()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function handleQuery() {
|
||||
stopPolling()
|
||||
devicePage.value = 1
|
||||
selectDevice(undefined)
|
||||
loadDevices()
|
||||
}
|
||||
|
||||
function resetQuery() {
|
||||
queryRef.value?.resetFields()
|
||||
queryForm.logDateRange = []
|
||||
queryForm.uploadTimeRange = []
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
function changeDevicePage(page: number) {
|
||||
stopPolling()
|
||||
devicePage.value = page
|
||||
selectDevice(undefined)
|
||||
loadDevices()
|
||||
}
|
||||
|
||||
function selectDeviceFromDrawer(device: TerminalLogDevice) {
|
||||
deviceDrawerVisible.value = false
|
||||
selectDevice(device)
|
||||
}
|
||||
|
||||
function selectFileFromDrawer(file: TerminalLogTreeNode) {
|
||||
fileDrawerVisible.value = false
|
||||
selectFile(file)
|
||||
}
|
||||
|
||||
function openPullDialog() {
|
||||
if (!auth(PULL_PERMISSION)) return
|
||||
pullHardId.value = undefined
|
||||
pullDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function pullSelectedDevice() {
|
||||
if (selectedDevice.value) await sendPullCommand(selectedDevice.value.hardId, selectedDevice.value.latestUploadTime)
|
||||
}
|
||||
|
||||
async function pullOtherDevice() {
|
||||
if (!pullHardId.value) return
|
||||
pullDialogVisible.value = false
|
||||
await sendPullCommand(pullHardId.value)
|
||||
}
|
||||
|
||||
async function sendPullCommand(hardId: string | number, previousUploadTime?: string) {
|
||||
if (!auth(PULL_PERMISSION) || pulling.value) return
|
||||
stopPolling()
|
||||
pulling.value = true
|
||||
try {
|
||||
let baselineTime = previousUploadTime
|
||||
let hadHistory = previousUploadTime != null
|
||||
if (!hadHistory) {
|
||||
const { data } = await terminalLogApi.fetchDevicePage({ hardId, current: 1, size: 1 })
|
||||
const baseline = data?.records?.[0] as TerminalLogDevice | undefined
|
||||
hadHistory = Boolean(baseline)
|
||||
baselineTime = baseline?.latestUploadTime
|
||||
}
|
||||
await terminalLogApi.pullTerminalLog(hardId)
|
||||
useMessage().success("指令已发送,日志无变化时列表不会更新")
|
||||
startPolling(hardId, baselineTime, hadHistory)
|
||||
} catch (error: any) {
|
||||
pulling.value = false
|
||||
useMessage().error(error?.msg || "日志拉取指令发送失败")
|
||||
}
|
||||
}
|
||||
|
||||
// MQTT 只负责通知终端上传,管理端按固定窗口轮询,避免持续占用接口资源。
|
||||
function startPolling(hardId: string | number, previousUploadTime: string | undefined, hadHistory: boolean) {
|
||||
let count = 0
|
||||
const poll = async () => {
|
||||
count += 1
|
||||
try {
|
||||
const { data } = await terminalLogApi.fetchDevicePage({ hardId, current: 1, size: 1 })
|
||||
const current = data?.records?.[0] as TerminalLogDevice | undefined
|
||||
const changed = current && (!hadHistory || current.latestUploadTime !== previousUploadTime)
|
||||
if (changed) {
|
||||
stopPolling()
|
||||
devicePage.value = 1
|
||||
await loadDevices()
|
||||
const refreshed = devices.value.find((item) => String(item.hardId) === String(hardId)) ?? current
|
||||
await selectDevice(refreshed)
|
||||
useMessage().success("终端日志已更新")
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// 单次轮询失败不终止等待,网络恢复后仍可在剩余窗口内发现新日志。
|
||||
}
|
||||
if (count >= POLL_MAX_COUNT) {
|
||||
stopPolling()
|
||||
useMessage().info("等待结束,终端离线或日志内容无变化")
|
||||
return
|
||||
}
|
||||
pollTimer = setTimeout(poll, POLL_INTERVAL_MS)
|
||||
}
|
||||
pollTimer = setTimeout(poll, POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer) clearTimeout(pollTimer)
|
||||
pollTimer = undefined
|
||||
pulling.value = false
|
||||
}
|
||||
|
||||
onMounted(() => loadDevices())
|
||||
onBeforeUnmount(stopPolling)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.terminal-log-query:deep(.el-form-item) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 查询项由外层十二列栅格分配宽度,控件只负责填满网格,避免固定像素宽度造成随机换行。 */
|
||||
.terminal-log-query:deep(.el-input),
|
||||
.terminal-log-query:deep(.el-select),
|
||||
.terminal-log-query:deep(.el-tree-select),
|
||||
.terminal-log-query:deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.terminal-log-query:deep(.el-form-item__content) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.terminal-log-query__actions:deep(.el-form-item__content) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.terminal-log-split,
|
||||
.terminal-log-split:deep(.splitpanes__pane) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.terminal-log-split:deep(.splitpanes__splitter) {
|
||||
width: 5px;
|
||||
border-left: 1px solid var(--el-border-color-lighter);
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
:deep(.el-drawer__body) {
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div ref="hostRef" class="terminal-log-viewer" aria-label="终端日志只读预览"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CodeMirror from "codemirror"
|
||||
import "codemirror/lib/codemirror.css"
|
||||
import "codemirror/addon/selection/active-line"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: string
|
||||
keyword?: string
|
||||
}>(),
|
||||
{
|
||||
modelValue: "",
|
||||
keyword: "",
|
||||
},
|
||||
)
|
||||
|
||||
const hostRef = ref<HTMLElement>()
|
||||
// 当前工程未引入 CodeMirror 5 类型包,运行时 API 保持由锁定版本提供。
|
||||
let editor: any
|
||||
let keywordMarks: any[] = []
|
||||
|
||||
const displayValue = computed(() => {
|
||||
const keyword = props.keyword.trim().toLocaleLowerCase()
|
||||
if (!keyword) return props.modelValue
|
||||
return props.modelValue
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.toLocaleLowerCase().includes(keyword))
|
||||
.join("\n")
|
||||
})
|
||||
|
||||
const logLevelOverlay = {
|
||||
token(stream: any) {
|
||||
if (stream.match(/\b(ERROR|FATAL)\b/)) return "log-error"
|
||||
if (stream.match(/\bWARN(?:ING)?\b/)) return "log-warn"
|
||||
if (stream.match(/\bINFO\b/)) return "log-info"
|
||||
while (stream.next() != null && !stream.match(/\b(ERROR|FATAL|WARN(?:ING)?|INFO)\b/, false)) {
|
||||
// 连续消费普通字符,避免逐字符触发 overlay 解析。
|
||||
}
|
||||
return null
|
||||
},
|
||||
}
|
||||
|
||||
function refreshKeywordMarks() {
|
||||
keywordMarks.forEach((mark) => mark.clear())
|
||||
keywordMarks = []
|
||||
const keyword = props.keyword.trim()
|
||||
if (!editor || !keyword) return
|
||||
const normalizedKeyword = keyword.toLocaleLowerCase()
|
||||
for (let line = 0; line < editor.lineCount(); line += 1) {
|
||||
const source = editor.getLine(line)
|
||||
const normalizedSource = source.toLocaleLowerCase()
|
||||
let start = normalizedSource.indexOf(normalizedKeyword)
|
||||
while (start >= 0) {
|
||||
keywordMarks.push(
|
||||
editor.markText(
|
||||
{ line, ch: start },
|
||||
{ line, ch: start + keyword.length },
|
||||
{ className: "terminal-log-keyword" },
|
||||
),
|
||||
)
|
||||
start = normalizedSource.indexOf(normalizedKeyword, start + keyword.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch([displayValue, () => props.keyword], ([value]) => {
|
||||
if (!editor) return
|
||||
if (editor.getValue() !== value) editor.setValue(value)
|
||||
// 不同关键词可能筛出相同的行,仍需单独刷新高亮位置。
|
||||
refreshKeywordMarks()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!hostRef.value) return
|
||||
editor = CodeMirror(hostRef.value, {
|
||||
value: displayValue.value,
|
||||
readOnly: "nocursor",
|
||||
lineNumbers: true,
|
||||
lineWrapping: false,
|
||||
styleActiveLine: true,
|
||||
viewportMargin: 50,
|
||||
})
|
||||
editor.addOverlay(logLevelOverlay)
|
||||
refreshKeywordMarks()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
keywordMarks.forEach((mark) => mark.clear())
|
||||
keywordMarks = []
|
||||
editor = undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.terminal-log-viewer {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.terminal-log-viewer:deep(.CodeMirror) {
|
||||
height: 100%;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
background: var(--el-bg-color);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.terminal-log-viewer:deep(.CodeMirror-gutters) {
|
||||
border-right-color: var(--el-border-color-lighter);
|
||||
background: var(--el-fill-color-lighter);
|
||||
}
|
||||
|
||||
.terminal-log-viewer:deep(.cm-log-error) {
|
||||
color: var(--el-color-danger);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.terminal-log-viewer:deep(.cm-log-warn) {
|
||||
color: var(--el-color-warning);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.terminal-log-viewer:deep(.cm-log-info) {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.terminal-log-viewer:deep(.terminal-log-keyword) {
|
||||
border-radius: 2px;
|
||||
background: var(--el-color-warning-light-7);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<section class="flex h-full min-h-0 flex-col bg-[var(--el-bg-color)]" aria-label="终端日志预览">
|
||||
<header class="flex min-h-[53px] flex-wrap items-center justify-between gap-2 border-b border-br-light px-3 py-2">
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium text-[var(--el-text-color-primary)]">{{ preview?.originalName || "日志预览" }}</div>
|
||||
<div class="mt-0.5 truncate text-xs text-[var(--el-text-color-secondary)]">
|
||||
{{ preview?.relativePath || "从文件树选择一个日志文件" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-input v-model="keyword" clearable prefix-icon="Search" placeholder="筛选并高亮关键词" class="w-56" />
|
||||
<el-button icon="CopyDocument" :disabled="!preview?.content" @click="$emit('copy')">复制</el-button>
|
||||
<el-button
|
||||
v-auth="'hard_hardTerminalLogFile_preview'"
|
||||
icon="Refresh"
|
||||
:disabled="!hasFile"
|
||||
:loading="loading"
|
||||
@click="$emit('refresh')"
|
||||
>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button
|
||||
v-auth="'hard_hardTerminalLogFile_down'"
|
||||
type="primary"
|
||||
plain
|
||||
icon="Download"
|
||||
:disabled="!hasFile"
|
||||
@click="$emit('download')"
|
||||
>
|
||||
完整下载
|
||||
</el-button>
|
||||
</div>
|
||||
</header>
|
||||
<el-alert
|
||||
v-if="preview?.truncated"
|
||||
class="rounded-none"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="仅展示日志尾部最多 1MB / 5000 行,完整内容请下载查看。"
|
||||
/>
|
||||
<div v-loading="loading" class="min-h-0 flex-1">
|
||||
<log-viewer v-if="preview" :model-value="preview.content" :keyword="keyword" />
|
||||
<el-empty v-else :description="emptyDescription" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TerminalLogPreview } from "/@/api/hard/hard-terminal-log-file"
|
||||
import LogViewer from "./log-viewer.vue"
|
||||
|
||||
const props = defineProps<{
|
||||
preview?: TerminalLogPreview
|
||||
loading: boolean
|
||||
canPreview: boolean
|
||||
hasDevice: boolean
|
||||
hasFile: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(event: "copy"): void
|
||||
(event: "refresh"): void
|
||||
(event: "download"): void
|
||||
}>()
|
||||
|
||||
const keyword = defineModel<string>("keyword", { default: "" })
|
||||
|
||||
const emptyDescription = computed(() => {
|
||||
if (!props.hasDevice) return "请先选择终端设备"
|
||||
if (props.hasFile && !props.canPreview) return "暂无日志预览权限,可使用完整下载"
|
||||
return "请选择日志文件"
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="设备名称" prop="hardName">
|
||||
<el-input v-model="state.queryForm.hardName" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="设备唯一编号" prop="uniqueCode">
|
||||
<el-input v-model="state.queryForm.uniqueCode" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="创建时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
|
||||
</el-form-item>
|
||||
|
||||
<template #more>
|
||||
<el-form-item style="width: 300px" label="设备类型" prop="hardType">
|
||||
<dict-select v-model="state.queryForm.hardType" dict-code="hard_type" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="简介" prop="title">
|
||||
<el-input v-model="state.queryForm.title" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="false" @exportExcel="exportExcel" class="ml10 mr20"
|
||||
style="float: right;" @queryTable="getDataList"></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table-column type="selection" width="40" align="center" />
|
||||
<el-table-column type="index" label="#" width="40" />
|
||||
<el-table-column prop="hardName" label="设备名称" width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="uniqueCode" label="设备唯一编号" width="120" />
|
||||
<el-table-column prop="hardType" label="设备类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="hard_type" :value="row.hardType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="简介" width="250" show-overflow-tooltip />
|
||||
<el-table-column prop="headers" label="headers" width="180">
|
||||
<template #default="{ row }">
|
||||
<preview-json :json="row.headers" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="res" label="res" width="180">
|
||||
<template #default="{ row }">
|
||||
<preview-json :json="row.res" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="req" label="req" width="180">
|
||||
<template #default="{ row }">
|
||||
<preview-json :json="row.req" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="startTime" label="请求时间">
|
||||
<el-table-column prop="startTime" label="开始时间" width="180" />
|
||||
<el-table-column prop="endTime" label="结束时间" width="180" />
|
||||
<el-table-column prop="cost" label="耗时(秒)" width="100">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.cost }}毫秒</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="systemHardXfjLog">
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table";
|
||||
import * as hardXfjLogApi from "/@/api/hard/hard-xfj-log";
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message";
|
||||
import { useDict } from '/@/hooks/dict';
|
||||
|
||||
// 定义查询字典
|
||||
const { hard_type } = useDict("hard_type")
|
||||
// 定义变量内容
|
||||
|
||||
// 搜索变量
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
// 多选变量
|
||||
const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {},
|
||||
pageList: hardXfjLogApi.fetchList
|
||||
})
|
||||
|
||||
// table hook
|
||||
const {
|
||||
getDataList,
|
||||
currentChangeHandle,
|
||||
sizeChangeHandle,
|
||||
sortChangeHandle,
|
||||
downBlobFile,
|
||||
tableStyle
|
||||
} = useTable(state)
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
// 清空搜索条件
|
||||
queryRef.value?.resetFields()
|
||||
// 清空多选
|
||||
selectObjs.value = []
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 导出excel
|
||||
const exportExcel = () => {
|
||||
downBlobFile('/hard/hardXfjLog/export', Object.assign(state.queryForm, { ids: selectObjs }), 'hardXfjLog.xlsx')
|
||||
}
|
||||
|
||||
// 多选事件
|
||||
const selectionChangHandle = (objs: { id: string }[]) => {
|
||||
selectObjs.value = objs.map(({ id }) => id);
|
||||
multiple.value = !objs.length;
|
||||
};
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (ids: string[]) => {
|
||||
try {
|
||||
await useMessageBox().confirm('此操作将永久删除');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await hardXfjLogApi.delObjs(ids);
|
||||
getDataList();
|
||||
useMessage().success('删除成功');
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -42,6 +42,10 @@
|
||||
v-auth="'wallet_mchManage_add'">
|
||||
新 增
|
||||
</el-button>
|
||||
<el-button plain icon="RefreshRight" type="primary" :loading="refreshAllQrcodeLoading"
|
||||
v-auth="'wallet_mchManage_refresh_qrcode'" @click="refreshAllQrcode">
|
||||
刷新全部二维码
|
||||
</el-button>
|
||||
<el-button plain :disabled="multiple" icon="Delete" type="primary"
|
||||
v-auth="'wallet_mchManage_del'" @click="handleDelete(selectObjs)">
|
||||
删 除
|
||||
@@ -196,6 +200,7 @@ const showSearch = ref(true)
|
||||
// 多选变量
|
||||
const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
const refreshAllQrcodeLoading = ref(false)
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {},
|
||||
@@ -243,6 +248,26 @@ const refreshQrcode = async (row: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 重新生成全部商户二维码
|
||||
const refreshAllQrcode = async () => {
|
||||
try {
|
||||
await useMessageBox().confirm("此操作将重新生成全部商户二维码,是否继续")
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
refreshAllQrcodeLoading.value = true
|
||||
try {
|
||||
const { data } = await mchManangeApi.refreshAllQrcode()
|
||||
await getDataList()
|
||||
useMessage().success(`刷新完成,共处理 ${data ?? 0} 个商户`)
|
||||
} catch (e: any) {
|
||||
useMessage().error(e.msg || "刷新失败")
|
||||
} finally {
|
||||
refreshAllQrcodeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 修改是否在正元显示
|
||||
const isSyncZyChange = async (row: any) => {
|
||||
try {
|
||||
|
||||
+14
-13
@@ -1,19 +1,20 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 250px" label="第三方名" prop="name">
|
||||
<el-input v-model="state.queryForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 250px" label="第三方名" prop="name">
|
||||
<el-input v-model="state.queryForm.name" />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()" v-auth="'third_thirdKeyManage_add'">
|
||||
|
||||
+55
-26
@@ -1,7 +1,16 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 250px" label="用户" prop="paySourceType">
|
||||
<dict-select v-model="state.queryForm.apiType" dict-code="third_key_type" />
|
||||
</el-form-item>
|
||||
@@ -9,34 +18,43 @@
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="third_pay_api_type" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 400px" label="请求时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
|
||||
type="daterange" />
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD" type="daterange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()"
|
||||
v-auth="'third_thirdPayLog_add'">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()" v-auth="'third_thirdPayLog_add'">
|
||||
新 增
|
||||
</el-button>
|
||||
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'third_thirdPayLog_del'"
|
||||
@click="handleDelete(selectObjs)">
|
||||
<el-button
|
||||
plain
|
||||
:disabled="multiple"
|
||||
icon="Delete"
|
||||
type="primary"
|
||||
v-auth="'third_thirdPayLog_del'"
|
||||
@click="handleDelete(selectObjs)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="'third_thirdPayLog_export'"
|
||||
@exportExcel="exportExcel" class="ml10 mr20" style="float: right"
|
||||
@queryTable="getDataList"></right-toolbar>
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'third_thirdPayLog_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="80" />
|
||||
<el-table-column prop="paySourceType" label="发起方" width="120">
|
||||
<template #default="{ row }">
|
||||
@@ -64,19 +82,30 @@
|
||||
<el-table-column prop="createTime" label="调用时间" width="180" />
|
||||
<el-table-column label="操作" fixed="right" width="80" v-if="false">
|
||||
<template #default="scope">
|
||||
<el-button v-if="false" icon="edit-pen" text type="primary" v-auth="'third_thirdPayLog_edit'"
|
||||
@click="formDialogRef.openDialog(scope.row.id)">
|
||||
<el-button
|
||||
v-if="false"
|
||||
icon="edit-pen"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'third_thirdPayLog_edit'"
|
||||
@click="formDialogRef.openDialog(scope.row.id)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button v-if="false" icon="delete" text type="primary" v-auth="'third_thirdPayLog_del'"
|
||||
@click="handleDelete([scope.row.id])">
|
||||
<el-button
|
||||
v-if="false"
|
||||
icon="delete"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'third_thirdPayLog_del'"
|
||||
@click="handleDelete([scope.row.id])"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
|
||||
+196
-188
@@ -1,147 +1,162 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-form-item style="width: 250px" label="订单号" prop="payNo">
|
||||
<el-input v-model="state.queryForm.payNo" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="支付平台" prop="payPlatformType">
|
||||
<dict-select v-model="state.queryForm.payPlatformType" dict-code="mini_platform_type" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="业务类型" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="third_pay_order_biz_type" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="支付时间" prop="payTimeList">
|
||||
<el-date-picker v-model="state.queryForm.payTimeList" value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="datetimerange" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="退款时间" prop="refundTimeList">
|
||||
<el-date-picker v-model="state.queryForm.refundTimeList" value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="datetimerange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">
|
||||
{{ $t("common.resetBtn") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<!-- :export="'third_thirdPayLog_export'" -->
|
||||
<right-toolbar v-model:showSearch="showSearch" @exportExcel="exportExcel" class="ml10 mr20"
|
||||
style="float: right" @queryTable="getDataList"></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table-column type="index" label="#" width="40" />
|
||||
<el-table-column prop="payPlatformType" width="150" label="支付平台类型">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="mini_platform_type" :value="row.payPlatformType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="bizType" label="支付业务类型" width="150">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="third_pay_order_biz_type" :value="row.bizType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="bizId" label="支付业务id" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="payNo" label="支付订单号" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="username" label="用户" width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="userPhone" label="用户手机号" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="userCode" label="用户第三方平台唯一编码" width="280" />
|
||||
<el-table-column prop="price" width="100" label="支付金额">
|
||||
<template #default="{ row }">
|
||||
<div class="color-success">{{ displayPrice(row.price) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" width="100" label="支付状态">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="third_pay_order_status" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payTime" label="支付时间" width="180" />
|
||||
<el-table-column prop="payTime" label="支付" width="280">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[40%] text-left">支付参数:</div>
|
||||
<preview-json v-if="row.payRep" :json="row.payRep" />
|
||||
<div class="color-danger" v-else>暂无</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[40%] text-left">支付返回:</div>
|
||||
<preview-json v-if="row.payRes" :json="row.payRes" />
|
||||
<div class="color-danger" v-else>暂无</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[40%] text-left">支付回调返回:</div>
|
||||
<preview-json v-if="row.payCallbackRes" :json="row.payCallbackRes" />
|
||||
<div class="color-danger" v-else>暂无</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="refundNo" label="退款订单号" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="refundTime" label="退款时间" width="180" />
|
||||
<el-table-column prop="refundRes" label="退款" width="280">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[40%] text-left">退款参数:</div>
|
||||
<preview-json v-if="row.refundRep" :json="row.refundRep" />
|
||||
<div class="color-danger" v-else>暂无</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[40%] text-left">退款返回:</div>
|
||||
<preview-json v-if="row.refundRes" :json="row.refundRes" />
|
||||
<div class="color-danger" v-else>暂无</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[40%] text-left">退款回调返回:</div>
|
||||
<preview-json v-if="row.refundCallbackRes" :json="row.refundCallbackRes" />
|
||||
<div class="color-danger" v-else>暂无</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" align="center" label="操作" prop="createTime" width="100">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center justify-center">
|
||||
<el-button v-if="row.status == 20" icon="CloseBold" text type="primary" @click="refund(row)">
|
||||
退款
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 250px" label="订单号" prop="payNo">
|
||||
<el-input v-model="state.queryForm.payNo" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
<el-form-item style="width: 250px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="支付时间" prop="payTimeList">
|
||||
<el-date-picker v-model="state.queryForm.payTimeList" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
|
||||
</el-form-item>
|
||||
|
||||
</div>
|
||||
<template #more>
|
||||
<el-form-item style="width: 250px" label="支付平台" prop="payPlatformType">
|
||||
<dict-select v-model="state.queryForm.payPlatformType" dict-code="mini_platform_type" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="业务类型" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="third_pay_order_biz_type" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="退款时间" prop="refundTimeList">
|
||||
<el-date-picker v-model="state.queryForm.refundTimeList" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<!-- :export="'third_thirdPayLog_export'" -->
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="40" />
|
||||
<el-table-column prop="payPlatformType" width="150" label="支付平台类型">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="mini_platform_type" :value="row.payPlatformType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="bizType" label="支付业务类型" width="150">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="third_pay_order_biz_type" :value="row.bizType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="bizId" label="支付业务id" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="payNo" label="支付订单号" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="username" label="用户" width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="userPhone" label="用户手机号" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="userCode" label="用户第三方平台唯一编码" width="280" />
|
||||
<el-table-column prop="price" width="100" label="支付金额">
|
||||
<template #default="{ row }">
|
||||
<div class="color-success">{{ displayPrice(row.price) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" width="100" label="支付状态">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="third_pay_order_status" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payTime" label="支付时间" width="180" />
|
||||
<el-table-column prop="payTime" label="支付" width="280">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[40%] text-left">支付参数:</div>
|
||||
<preview-json v-if="row.payRep" :json="row.payRep" />
|
||||
<div class="color-danger" v-else>暂无</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[40%] text-left">支付返回:</div>
|
||||
<preview-json v-if="row.payRes" :json="row.payRes" />
|
||||
<div class="color-danger" v-else>暂无</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[40%] text-left">支付回调返回:</div>
|
||||
<preview-json v-if="row.payCallbackRes" :json="row.payCallbackRes" />
|
||||
<div class="color-danger" v-else>暂无</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="refundNo" label="退款订单号" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="refundTime" label="退款时间" width="180" />
|
||||
<el-table-column prop="refundRes" label="退款" width="280">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[40%] text-left">退款参数:</div>
|
||||
<preview-json v-if="row.refundRep" :json="row.refundRep" />
|
||||
<div class="color-danger" v-else>暂无</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[40%] text-left">退款返回:</div>
|
||||
<preview-json v-if="row.refundRes" :json="row.refundRes" />
|
||||
<div class="color-danger" v-else>暂无</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[40%] text-left">退款回调返回:</div>
|
||||
<preview-json v-if="row.refundCallbackRes" :json="row.refundCallbackRes" />
|
||||
<div class="color-danger" v-else>暂无</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" align="center" label="操作" prop="createTime" width="100">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center justify-center">
|
||||
<el-button v-if="row.status == 20" icon="CloseBold" text type="primary" @click="refund(row)"> 退款 </el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="systemThirdPayOrder">
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table";
|
||||
import * as thirdPayOrderApi from "/@/api/thirdparty/third-pay-order";
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message";
|
||||
import { useDict } from '/@/hooks/dict';
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import * as thirdPayOrderApi from "/@/api/thirdparty/third-pay-order"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { useDict } from "/@/hooks/dict"
|
||||
import { displayPrice } from "/@/utils/formatMoney"
|
||||
|
||||
// 引入组件
|
||||
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
|
||||
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
|
||||
// 定义查询字典
|
||||
const { third_pay_order_biz_type, mini_platform_type, third_pay_order_status } = useDict("third_pay_order_biz_type", "mini_platform_type", "third_pay_order_status")
|
||||
const { third_pay_order_biz_type, mini_platform_type, third_pay_order_status } = useDict(
|
||||
"third_pay_order_biz_type",
|
||||
"mini_platform_type",
|
||||
"third_pay_order_status",
|
||||
)
|
||||
// 定义变量内容
|
||||
const formDialogRef = ref()
|
||||
// 搜索变量
|
||||
@@ -152,81 +167,74 @@ const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
createdIsNeed: false,
|
||||
queryForm: {},
|
||||
pageList: thirdPayOrderApi.fetchList
|
||||
createdIsNeed: false,
|
||||
queryForm: {},
|
||||
pageList: thirdPayOrderApi.fetchList,
|
||||
})
|
||||
|
||||
// table hook
|
||||
const {
|
||||
getDataList,
|
||||
currentChangeHandle,
|
||||
sizeChangeHandle,
|
||||
sortChangeHandle,
|
||||
downBlobFile,
|
||||
tableStyle
|
||||
} = useTable(state)
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state)
|
||||
|
||||
onMounted(() => {
|
||||
const { query } = useRoute()
|
||||
if (query.payNo) {
|
||||
state.queryForm.payNo = query.payNo
|
||||
}
|
||||
getDataList()
|
||||
const { query } = useRoute()
|
||||
if (query.payNo) {
|
||||
state.queryForm.payNo = query.payNo
|
||||
}
|
||||
getDataList()
|
||||
})
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
// 清空搜索条件
|
||||
queryRef.value?.resetFields()
|
||||
// 清空多选
|
||||
selectObjs.value = []
|
||||
getDataList()
|
||||
// 清空搜索条件
|
||||
queryRef.value?.resetFields()
|
||||
// 清空多选
|
||||
selectObjs.value = []
|
||||
getDataList()
|
||||
}
|
||||
|
||||
const refund = async (row: any) => {
|
||||
try {
|
||||
await useMessageBox().confirm(`此操作将退款${row.price}元`)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await thirdPayOrderApi.miniRefund({
|
||||
payNo: row.payNo,
|
||||
refundPrice: ''
|
||||
})
|
||||
getDataList()
|
||||
useMessage().success("退款成功")
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg)
|
||||
}
|
||||
try {
|
||||
await useMessageBox().confirm(`此操作将退款${row.price}元`)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await thirdPayOrderApi.miniRefund({
|
||||
payNo: row.payNo,
|
||||
refundPrice: "",
|
||||
})
|
||||
getDataList()
|
||||
useMessage().success("退款成功")
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg)
|
||||
}
|
||||
}
|
||||
|
||||
// 导出excel
|
||||
const exportExcel = () => {
|
||||
downBlobFile('/third/third-pay-order/export', Object.assign(state.queryForm, { ids: selectObjs }), 'thirdPayOrder.xlsx')
|
||||
downBlobFile("/third/third-pay-order/export", Object.assign(state.queryForm, { ids: selectObjs }), "thirdPayOrder.xlsx")
|
||||
}
|
||||
|
||||
// 多选事件
|
||||
const selectionChangHandle = (objs: { id: string }[]) => {
|
||||
selectObjs.value = objs.map(({ id }) => id);
|
||||
multiple.value = !objs.length;
|
||||
};
|
||||
selectObjs.value = objs.map(({ id }) => id)
|
||||
multiple.value = !objs.length
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (ids: string[]) => {
|
||||
try {
|
||||
await useMessageBox().confirm('此操作将永久删除');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await useMessageBox().confirm("此操作将永久删除")
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await thirdPayOrderApi.delObjs(ids);
|
||||
getDataList();
|
||||
useMessage().success('删除成功');
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
try {
|
||||
await thirdPayOrderApi.delObjs(ids)
|
||||
getDataList()
|
||||
useMessage().success("删除成功")
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,18 +1,20 @@
|
||||
<template>
|
||||
<el-dialog title="使用情况" width="60%" v-model="visible" :close-on-click-modal="false" draggable>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="6">
|
||||
<el-form-item class="w100" label="是否使用" prop="status">
|
||||
<dict-select v-model="state.queryForm.status" dict-code="yes_no_type" placeholder="是否使用" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList"> {{ $t("common.queryBtn") }}</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
<SearchPanel
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-col :span="6">
|
||||
<el-form-item class="w100" label="是否使用" prop="status">
|
||||
<dict-select v-model="state.queryForm.status" dict-code="yes_no_type" placeholder="是否使用" clearable />
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</SearchPanel>
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="修改流水信息"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!loading"
|
||||
:show-close="!loading"
|
||||
draggable
|
||||
>
|
||||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="80px" v-loading="loading">
|
||||
<el-form-item label="部门" prop="deptId">
|
||||
<select-dept v-model="form.deptId" placeholder="请选择流水触发部门" />
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="form.createTime"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择流水创建时间"
|
||||
class="w100"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button :disabled="loading" @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="onSubmit">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="WalletTransactionInfoForm">
|
||||
import type { FormInstance, FormRules } from "element-plus"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { updateInfo, type WalletTransactionUpdateInfoParams } from "/@/api/wallet/wallet-transaction"
|
||||
|
||||
interface WalletTransactionInfoRow {
|
||||
id: string | number
|
||||
deptId?: string | number | null
|
||||
createTime?: string | null
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "refresh"): void
|
||||
}>()
|
||||
|
||||
const dataFormRef = ref<FormInstance>()
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const form = reactive<WalletTransactionUpdateInfoParams>({
|
||||
id: "",
|
||||
deptId: "",
|
||||
createTime: "",
|
||||
})
|
||||
/** 部门与创建时间必填:两者任一未选择都不允许提交,防止误清空原有归属。 */
|
||||
const dataRules: FormRules<WalletTransactionUpdateInfoParams> = {
|
||||
deptId: [{ required: true, message: "请选择流水触发部门", trigger: "change" }],
|
||||
createTime: [{ required: true, message: "请选择流水创建时间", trigger: "change" }],
|
||||
}
|
||||
|
||||
const openDialog = (row: WalletTransactionInfoRow) => {
|
||||
form.id = row.id
|
||||
form.deptId = row.deptId ?? ""
|
||||
form.createTime = row.createTime ?? ""
|
||||
visible.value = true
|
||||
nextTick(() => dataFormRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
const valid = await dataFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
try {
|
||||
await updateInfo({ id: form.id, deptId: form.deptId, createTime: form.createTime })
|
||||
useMessage().success("流水信息修改成功")
|
||||
visible.value = false
|
||||
emit("refresh")
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "流水信息修改失败")
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ openDialog })
|
||||
</script>
|
||||
@@ -11,25 +11,30 @@
|
||||
<template #default="{ row }"> {{ displayPrice(row.balance) }} </template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" width="80" label="版本号" prop="version" />
|
||||
<el-table-column align="center" label="备注" prop="remark" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column align="center" label="交易时间" prop="createTime" width="180" />
|
||||
<el-table-column align="center" label="操作" prop="createTime" width="100">
|
||||
<el-table-column align="center" label="操作" width="240">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.walletTypeUniqueCode == '3'" class="flex items-center justify-center">
|
||||
<el-button icon="CloseBold" text type="primary" @click="refund(row)">
|
||||
<div class="row-actions">
|
||||
<el-button v-if="row.walletTypeUniqueCode == '3'" icon="CloseBold" text type="primary"
|
||||
@click="refund(row)">
|
||||
退款
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
||||
<el-button icon="EditPen" link type="primary" v-auth="'wallet_walletTransaction_edit'"
|
||||
@click="remarkFormRef?.openDialog(row)">修改备注</el-button>
|
||||
<el-button icon="Edit" link type="primary" v-auth="'wallet_walletTransaction_update_info'"
|
||||
@click="infoFormRef?.openDialog(row)">修改信息</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
</template>
|
||||
</el-dialog>
|
||||
<transaction-remark-form ref="remarkFormRef" @refresh="getDataList(false)" />
|
||||
<transaction-info-form ref="infoFormRef" @refresh="getDataList(false)" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="WalletManageDialog">
|
||||
@@ -37,12 +42,22 @@ import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import * as walletTransactionApi from "/@/api/wallet/wallet-transaction"
|
||||
import { displayPrice } from "/@/utils/formatMoney"
|
||||
|
||||
// 所有钱包流水列表共用的备注编辑表单。
|
||||
const TransactionRemarkForm = defineAsyncComponent(() => import("./transaction-remark-form.vue"))
|
||||
// 所有钱包流水列表共用的流水信息编辑表单(部门、创建时间)。
|
||||
const TransactionInfoForm = defineAsyncComponent(() => import("./transaction-info-form.vue"))
|
||||
|
||||
const { push } = useRouter()
|
||||
|
||||
// 定义变量内容
|
||||
const visible = ref(false)
|
||||
// 定义字典
|
||||
|
||||
// 通用流水备注编辑弹窗引用。
|
||||
const remarkFormRef = ref()
|
||||
// 通用流水信息编辑弹窗引用。
|
||||
const infoFormRef = ref()
|
||||
|
||||
// 提交表单数据
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {},
|
||||
@@ -71,4 +86,19 @@ const openDialog = (id: string) => {
|
||||
defineExpose({
|
||||
openDialog,
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 操作列按钮改为紧凑 flex 布局,避免按钮换行。 */
|
||||
.row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
|
||||
:deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="修改流水备注"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!loading"
|
||||
:show-close="!loading"
|
||||
draggable
|
||||
>
|
||||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="80px" v-loading="loading">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:maxlength="WALLET_TRANSACTION_REMARK_MAX_LENGTH"
|
||||
show-word-limit
|
||||
placeholder="请输入流水备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button :disabled="loading" @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="onSubmit">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="WalletTransactionRemarkForm">
|
||||
import type { FormInstance, FormRules } from "element-plus"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { WALLET_TRANSACTION_REMARK_MAX_LENGTH, updateRemark, type WalletTransactionRemarkParams } from "/@/api/wallet/wallet-transaction"
|
||||
|
||||
/** 列表行打开备注弹窗所需的最小数据,避免组件依赖各页面完整行结构。 */
|
||||
interface WalletTransactionRemarkRow {
|
||||
/** 钱包流水编号。 */
|
||||
id: string | number
|
||||
/** 当前备注;历史数据未填写时允许为空。 */
|
||||
remark?: string | null
|
||||
}
|
||||
|
||||
/** 备注更新成功后通知所属列表刷新当前查询结果。 */
|
||||
const emit = defineEmits<{
|
||||
(event: "refresh"): void
|
||||
}>()
|
||||
|
||||
const dataFormRef = ref<FormInstance>()
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
/** 当前正在编辑的流水编号和备注副本,取消时不会污染列表原数据。 */
|
||||
const form = reactive<WalletTransactionRemarkParams>({
|
||||
id: "",
|
||||
remark: "",
|
||||
})
|
||||
/** 备注长度校验作为输入框截断之外的表单级保护。 */
|
||||
const dataRules: FormRules<WalletTransactionRemarkParams> = {
|
||||
remark: [
|
||||
{
|
||||
max: WALLET_TRANSACTION_REMARK_MAX_LENGTH,
|
||||
message: `流水备注不能超过${WALLET_TRANSACTION_REMARK_MAX_LENGTH}个字符`,
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const openDialog = (row: WalletTransactionRemarkRow) => {
|
||||
form.id = row.id
|
||||
form.remark = row.remark ?? ""
|
||||
visible.value = true
|
||||
nextTick(() => dataFormRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
const valid = await dataFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
try {
|
||||
await updateRemark({ id: form.id, remark: form.remark?.trim() })
|
||||
useMessage().success("流水备注修改成功")
|
||||
visible.value = false
|
||||
emit("refresh")
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "流水备注修改失败")
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ openDialog })
|
||||
</script>
|
||||
@@ -1,31 +1,35 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 250px" label="用户" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="是否激活" prop="isActiveHardware">
|
||||
<dict-select v-model="state.queryForm.isActiveHardware" dict-code="yes_no_type" placeholder="是否激活" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="卡号" prop="cardNumber">
|
||||
<el-input v-model="state.queryForm.cardNumber" placeholder="饭卡卡号" clearable />
|
||||
</el-form-item>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 250px" label="用户" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="是否激活" prop="isActiveHardware">
|
||||
<dict-select v-model="state.queryForm.isActiveHardware" dict-code="yes_no_type" placeholder="是否激活" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="卡号" prop="cardNumber">
|
||||
<el-input v-model="state.queryForm.cardNumber" placeholder="饭卡卡号" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<template #more>
|
||||
<el-form-item style="width: 250px" label="是否绑定用户" prop="isBindUser">
|
||||
<dict-select v-model="state.queryForm.isBindUser" dict-code="yes_no_type" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="是否硬件激活" prop="isMchActive">
|
||||
<dict-select v-model="state.queryForm.isMchActive" dict-code="yes_no_type" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
|
||||
@@ -1,106 +1,141 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="充值金额(元)" prop="priceList">
|
||||
<el-input v-if="false" v-model="state.queryForm.price" placeholder="请输入充值金额(元)" />
|
||||
<el-select v-model="state.queryForm.priceList" multiple filterable allow-create
|
||||
default-first-option :reserve-keyword="false" placeholder="请输入充值金额(元)"
|
||||
style="width: 240px">
|
||||
<el-option v-for="item in state.queryForm.priceList" :key="item" :label="item"
|
||||
:value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="充值方式" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_biz" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="false" style="width: 200px" label="充值类型" prop="type">
|
||||
<dict-select v-model="state.queryForm.type" dict-code="wallet_biz_recharge" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeIdList">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList" placeholder="饭卡类型"
|
||||
clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="充值时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
|
||||
type="daterange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">
|
||||
{{ $t("common.resetBtn") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<right-toolbar :search="false" v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletTransaction_export'" @exportExcel="exportExcel" class="ml10 mr20"
|
||||
style="float: right" @queryTable="getDataList">
|
||||
</right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle" :summary-method="getSummaries" :show-summary="true"
|
||||
row-class-name="success-row" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle" @expand-change="tableExpandChange">
|
||||
<el-table-column type="expand" width="60">
|
||||
<template #default="{ row }">
|
||||
<div class="ml-[60px] my-2">
|
||||
<el-table :data="getTableChildrenMap(row.mchId)" border
|
||||
:cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
|
||||
row-class-name="warning-row">
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column label="部门" align="center" width="250" prop="deptName" />
|
||||
<el-table-column label="充值金额" align="center" width="250" prop="price">
|
||||
<template #default="{ row }">
|
||||
<span>{{ displayPrice(row.price) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="充值人数" align="center" width="250" prop="personCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-primary">{{ row.personCount || 0 }}人</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="充值次数" align="center" prop="timeCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">{{ row.timeCount || 0 }}次</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column label="所属食堂" align="center" width="250" prop="mchName" />
|
||||
<el-table-column label="充值金额" align="center" width="250" prop="price">
|
||||
<template #default="{ row }">
|
||||
<span>{{ displayPrice(row.price) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="充值人数" align="center" width="250" prop="personCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-primary">{{ row.personCount || 0 }}人</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="充值次数" align="center" prop="timeCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">{{ row.timeCount || 0 }}次</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree
|
||||
ref="draggableDepTreeRef"
|
||||
placeholder="请输入部门名称"
|
||||
:show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode"
|
||||
switch-inactive-text="包含"
|
||||
switch-active-text="不包含"
|
||||
:defaultTreeWidth="20"
|
||||
:nodeClickable="false"
|
||||
>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="充值金额(元)" prop="priceList">
|
||||
<el-input v-if="false" v-model="state.queryForm.price" placeholder="请输入充值金额(元)" />
|
||||
<el-select
|
||||
v-model="state.queryForm.priceList"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
:reserve-keyword="false"
|
||||
placeholder="请输入充值金额(元)"
|
||||
style="width: 240px"
|
||||
>
|
||||
<el-option v-for="item in state.queryForm.priceList" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="充值方式" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_biz" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeIdList">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList" placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="充值时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD" type="daterange" />
|
||||
</el-form-item>
|
||||
|
||||
<template #more>
|
||||
<el-form-item v-if="false" style="width: 200px" label="充值类型" prop="type">
|
||||
<dict-select v-model="state.queryForm.type" dict-code="wallet_biz_recharge" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 260px" label="操作标签" prop="tagList">
|
||||
<dict-select v-model="state.queryForm.tagList" dict-code="wallet_transaction_tag" multiple />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<right-toolbar
|
||||
:search="false"
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletTransaction_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
>
|
||||
</right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
:summary-method="getSummaries"
|
||||
:show-summary="true"
|
||||
row-class-name="success-row"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
@expand-change="tableExpandChange"
|
||||
>
|
||||
<el-table-column type="expand" width="60">
|
||||
<template #default="{ row }">
|
||||
<div class="ml-[60px] my-2">
|
||||
<el-table
|
||||
:data="getTableChildrenMap(row.mchId)"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
row-class-name="warning-row"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column label="部门" align="center" width="250" prop="deptName" />
|
||||
<el-table-column label="充值金额" align="center" width="250" prop="price">
|
||||
<template #default="{ row }">
|
||||
<span>{{ displayPrice(row.price) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="充值人数" align="center" width="250" prop="personCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-primary">{{ row.personCount || 0 }}人</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="充值次数" align="center" prop="timeCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">{{ row.timeCount || 0 }}次</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column label="所属食堂" align="center" width="250" prop="mchName" />
|
||||
<el-table-column label="充值金额" align="center" width="250" prop="price">
|
||||
<template #default="{ row }">
|
||||
<span>{{ displayPrice(row.price) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="充值人数" align="center" width="250" prop="personCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-primary">{{ row.personCount || 0 }}人</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="充值次数" align="center" prop="timeCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">{{ row.timeCount || 0 }}次</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="systemWalletRecharge">
|
||||
@@ -111,15 +146,16 @@ import { useDict } from "/@/hooks/dict"
|
||||
import { displayPrice } from "/@/utils/formatMoney"
|
||||
|
||||
const defaultCreateTimeList = [
|
||||
dayjs().subtract(3, "month").startOf("day").format("YYYY-MM-DD"),
|
||||
dayjs().hour(23).minute(23).second(23).format("YYYY-MM-DD"),
|
||||
dayjs().subtract(3, "month").startOf("day").format("YYYY-MM-DD"),
|
||||
dayjs().hour(23).minute(23).second(23).format("YYYY-MM-DD"),
|
||||
]
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
isPage: false,
|
||||
queryForm: {
|
||||
createTimeList: defaultCreateTimeList
|
||||
},
|
||||
pageList: walletTransactionApi.pageCzTotal,
|
||||
isPage: false,
|
||||
queryForm: {
|
||||
createTimeList: defaultCreateTimeList,
|
||||
tagList: [],
|
||||
},
|
||||
pageList: walletTransactionApi.pageCzTotal,
|
||||
})
|
||||
|
||||
// 搜索变量
|
||||
@@ -137,84 +173,84 @@ const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, po
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
queryRef.value?.resetFields()
|
||||
// 清空多选
|
||||
selectObjs.value = []
|
||||
state.queryForm.deptId = ""
|
||||
state.queryForm.deptIds = []
|
||||
draggableDepTreeRef.value.resetData()
|
||||
getDataList()
|
||||
queryRef.value?.resetFields()
|
||||
// 清空多选
|
||||
selectObjs.value = []
|
||||
state.queryForm.deptId = ""
|
||||
state.queryForm.deptIds = []
|
||||
draggableDepTreeRef.value.resetData()
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 点击table展开行
|
||||
const tableExpandChange = async (e: any) => {
|
||||
try {
|
||||
const { data } = await walletTransactionApi.listDeptCzTotal({
|
||||
mchId: e.mchId,
|
||||
...state.queryForm
|
||||
})
|
||||
if (data && data.length) {
|
||||
tableChildrenMap.value.set(e.mchId, data)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
const { data } = await walletTransactionApi.listDeptCzTotal({
|
||||
mchId: e.mchId,
|
||||
...state.queryForm,
|
||||
})
|
||||
if (data && data.length) {
|
||||
tableChildrenMap.value.set(e.mchId, data)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const getTableChildrenMap = (key: string) => {
|
||||
return tableChildrenMap.value.get(key)
|
||||
return tableChildrenMap.value.get(key)
|
||||
}
|
||||
|
||||
// 树节点选择时
|
||||
const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepTree: any) => {
|
||||
state.queryForm.deptId = curClickedNode.id
|
||||
state.queryForm.deptIds = checkedTreeNodes.map((item: any) => item.id)
|
||||
state.queryForm.isInclude = !isDeepTree
|
||||
getDataList()
|
||||
state.queryForm.deptId = curClickedNode.id
|
||||
state.queryForm.deptIds = checkedTreeNodes.map((item: any) => item.id)
|
||||
state.queryForm.isInclude = !isDeepTree
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 导出excel
|
||||
const exportExcel = () => {
|
||||
postDownBlobFile(
|
||||
"/wallet/wallet-transaction/export-cz-total-person",
|
||||
Object.assign(state.queryForm, { current: state.pagination?.current, size: state.pagination?.size }),
|
||||
"钱包充值统计.xlsx",
|
||||
)
|
||||
postDownBlobFile(
|
||||
"/wallet/wallet-transaction/export-cz-total-person",
|
||||
Object.assign(state.queryForm, { current: state.pagination?.current, size: state.pagination?.size }),
|
||||
"钱包充值统计.xlsx",
|
||||
)
|
||||
}
|
||||
|
||||
// 多选事件
|
||||
const selectionChangHandle = (objs: { id: string }[]) => {
|
||||
selectObjs.value = objs.map(({ id }) => id)
|
||||
multiple.value = !objs.length
|
||||
selectObjs.value = objs.map(({ id }) => id)
|
||||
multiple.value = !objs.length
|
||||
}
|
||||
|
||||
const getSummaries = (param: any) => {
|
||||
const { columns, data } = param
|
||||
const totalColumns = ["price", "personCount", "timeCount"]
|
||||
const totalRow = Array.from({ length: columns.length })
|
||||
columns.forEach((i: any, index: number) => {
|
||||
if (totalColumns.includes(i.property)) {
|
||||
const total = data
|
||||
.filter((v: any) => v[i.property] != null)
|
||||
.map((v: any) => v[i.property])
|
||||
.reduce((pren: number, next: number) => Number(pren) + Number(next), 0)
|
||||
if (i.property === "price") {
|
||||
totalRow[index] = displayPrice(total)
|
||||
} else {
|
||||
totalRow[index] = total
|
||||
}
|
||||
}
|
||||
})
|
||||
return totalRow
|
||||
const { columns, data } = param
|
||||
const totalColumns = ["price", "personCount", "timeCount"]
|
||||
const totalRow = Array.from({ length: columns.length })
|
||||
columns.forEach((i: any, index: number) => {
|
||||
if (totalColumns.includes(i.property)) {
|
||||
const total = data
|
||||
.filter((v: any) => v[i.property] != null)
|
||||
.map((v: any) => v[i.property])
|
||||
.reduce((pren: number, next: number) => Number(pren) + Number(next), 0)
|
||||
if (i.property === "price") {
|
||||
totalRow[index] = displayPrice(total)
|
||||
} else {
|
||||
totalRow[index] = total
|
||||
}
|
||||
}
|
||||
})
|
||||
return totalRow
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.el-table .warning-row {
|
||||
--el-table-tr-bg-color: var(--el-color-warning-light-9);
|
||||
--el-table-tr-bg-color: var(--el-color-warning-light-9);
|
||||
}
|
||||
|
||||
.el-table .success-row {
|
||||
--el-table-tr-bg-color: var(--el-color-success-light-9);
|
||||
--el-table-tr-bg-color: var(--el-color-success-light-9);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,53 +1,78 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false">
|
||||
<DraggableDepTree
|
||||
ref="draggableDepTreeRef"
|
||||
placeholder="请输入部门名称"
|
||||
:show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode"
|
||||
switch-inactive-text="包含"
|
||||
switch-active-text="不包含"
|
||||
:defaultTreeWidth="20"
|
||||
:nodeClickable="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="充值方式" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_biz" />
|
||||
</el-form-item>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="充值方式" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_biz" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeIdList">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList" placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="充值时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD" type="daterange" />
|
||||
</el-form-item>
|
||||
|
||||
<template #more>
|
||||
<el-form-item v-if="false" style="width: 200px" label="充值类型" prop="type">
|
||||
<dict-select v-model="state.queryForm.type" dict-code="wallet_biz_recharge" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeIdList">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList"
|
||||
placeholder="饭卡类型" clearable />
|
||||
<el-form-item style="width: 260px" label="操作标签" prop="tagList">
|
||||
<dict-select v-model="state.queryForm.tagList" dict-code="wallet_transaction_tag" multiple />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="充值时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
|
||||
type="daterange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">
|
||||
{{ $t("common.resetBtn") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<right-toolbar :search="false" v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletTransaction_export'" @exportExcel="exportExcel" class="ml10 mr20"
|
||||
style="float: right" @queryTable="getDataList">
|
||||
<right-toolbar
|
||||
:search="false"
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletTransaction_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
>
|
||||
<el-tooltip class="item" effect="dark" content="统计" placement="top">
|
||||
<el-button icon="DataLine" circle @click="viewBalanceStatistics" />
|
||||
</el-tooltip>
|
||||
</right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :summary-method="getSummaries"
|
||||
show-summary :cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:summary-method="getSummaries"
|
||||
show-summary
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column width="120" prop="username" label="用户" />
|
||||
<el-table-column prop="userAvatar" label="头像">
|
||||
@@ -81,21 +106,50 @@
|
||||
<div class="color-primary">{{ displayPrice(row.balance) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="tags" width="200" align="center" label="操作标签">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="wallet_transaction_tag" :value="row.tags?.split(',')" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" width="200" label="充值时间" />
|
||||
<el-table-column v-if="false" label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button icon="delete" text type="primary" v-auth="'wallet_walletRecharge_del'"
|
||||
@click="handleDelete([scope.row.id])">删除</el-button>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="175"
|
||||
fixed="right"
|
||||
class-name="wallet-tx-actions-col"
|
||||
v-auths="['wallet_walletTransaction_edit', 'wallet_walletTransaction_update_info']"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="row-actions">
|
||||
<el-button
|
||||
icon="EditPen"
|
||||
link
|
||||
type="primary"
|
||||
v-auth="'wallet_walletTransaction_edit'"
|
||||
@click="remarkFormRef?.openDialog(row)"
|
||||
>修改备注</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="Edit"
|
||||
link
|
||||
type="primary"
|
||||
v-auth="'wallet_walletTransaction_update_info'"
|
||||
@click="infoFormRef?.openDialog(row)"
|
||||
>修改信息</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</template>
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
<transaction-remark-form ref="remarkFormRef" @refresh="getDataList(false)" />
|
||||
<transaction-info-form ref="infoFormRef" @refresh="getDataList(false)" />
|
||||
<el-drawer v-model="showDrawer" @closed="drawerClose">
|
||||
<el-table :data="[balanceStatistics]" style="width: 100%">
|
||||
<el-table-column align="center" label="充值统计">
|
||||
@@ -128,24 +182,30 @@
|
||||
<script setup lang="ts" name="systemWalletRecharge">
|
||||
import dayjs from "dayjs"
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import { pageCz, czTotal, delObjs } from "/@/api/wallet/wallet-transaction"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { pageCz, czTotal } from "/@/api/wallet/wallet-transaction"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { useDict } from "/@/hooks/dict"
|
||||
import { displayPrice } from "/@/utils/formatMoney"
|
||||
import { deptTree } from "/@/api/admin/dept"
|
||||
|
||||
const showDrawer = ref(false)
|
||||
const balanceStatisticsLoading = ref(false)
|
||||
const balanceStatistics = reactive<any>({
|
||||
|
||||
})
|
||||
const balanceStatistics = reactive<any>({})
|
||||
|
||||
// 引入组件
|
||||
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
|
||||
// 所有钱包流水列表共用的备注编辑表单。
|
||||
const TransactionRemarkForm = defineAsyncComponent(() => import("../components/transaction-remark-form.vue"))
|
||||
// 所有钱包流水列表共用的流水信息编辑表单(部门、创建时间)。
|
||||
const TransactionInfoForm = defineAsyncComponent(() => import("../components/transaction-info-form.vue"))
|
||||
// 定义查询字典
|
||||
const { wallet_type, wallet_biz } = useDict("wallet_type", "wallet_biz")
|
||||
const { wallet_type, wallet_biz, wallet_transaction_tag } = useDict("wallet_type", "wallet_biz", "wallet_transaction_tag")
|
||||
// 定义变量内容
|
||||
const formDialogRef = ref()
|
||||
// 通用流水备注编辑弹窗引用。
|
||||
const remarkFormRef = ref()
|
||||
// 通用流水信息编辑弹窗引用。
|
||||
const infoFormRef = ref()
|
||||
// 搜索变量
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
@@ -160,7 +220,8 @@ const defaultCreateTimeList = [
|
||||
]
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {
|
||||
createTimeList: defaultCreateTimeList
|
||||
createTimeList: defaultCreateTimeList,
|
||||
tagList: [],
|
||||
},
|
||||
pageList: pageCz,
|
||||
})
|
||||
@@ -236,23 +297,6 @@ const selectionChangHandle = (objs: { id: string }[]) => {
|
||||
multiple.value = !objs.length
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (ids: string[]) => {
|
||||
try {
|
||||
await useMessageBox().confirm("此操作将永久删除")
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await delObjs(ids)
|
||||
getDataList()
|
||||
useMessage().success("删除成功")
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg)
|
||||
}
|
||||
}
|
||||
|
||||
// 树节点选择时
|
||||
const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepTree: any) => {
|
||||
state.queryForm.deptId = curClickedNode.id
|
||||
@@ -261,3 +305,22 @@ const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepT
|
||||
getDataList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 操作列按钮改为紧凑 flex 布局并收窄单元格内边距,避免按钮换行和两侧留白。 */
|
||||
.row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
|
||||
:deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.wallet-tx-actions-col .cell) {
|
||||
padding: 0 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,20 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">{{ $t("common.queryBtn") }}</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()" v-auth="'wallet_walletForeign_add'">
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="申请人" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="饭卡卡号" prop="cardNumber">
|
||||
<el-input v-model="state.queryForm.cardNumber" placeholder="饭卡卡号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">{{ $t("common.queryBtn") }}</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="申请人" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="饭卡卡号" prop="cardNumber">
|
||||
<el-input v-model="state.queryForm.cardNumber" placeholder="饭卡卡号" clearable />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
|
||||
@@ -1,30 +1,44 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false">
|
||||
<DraggableDepTree
|
||||
ref="draggableDepTreeRef"
|
||||
placeholder="请输入部门名称"
|
||||
:show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode"
|
||||
switch-inactive-text="包含"
|
||||
switch-active-text="不包含"
|
||||
:defaultTreeWidth="20"
|
||||
:nodeClickable="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="不活跃月数" prop="unactiveMonthsNumber">
|
||||
<el-input v-model="state.queryForm.unactiveMonthsNumber" type="number"
|
||||
placeholder="不活跃月数" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">{{ $t("common.queryBtn")
|
||||
}}</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="不活跃月数" prop="unactiveMonthsNumber">
|
||||
<el-input v-model="state.queryForm.unactiveMonthsNumber" type="number" placeholder="不活跃月数" />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button plain :disabled="multiple" icon="Delete" type="primary"
|
||||
v-auth="'wallet_walletManage_del'" @click="handleDelete(selectObjs)">
|
||||
<el-button
|
||||
plain
|
||||
:disabled="multiple"
|
||||
icon="Delete"
|
||||
type="primary"
|
||||
v-auth="'wallet_walletManage_del'"
|
||||
@click="handleDelete(selectObjs)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<!-- <right-toolbar
|
||||
@@ -37,11 +51,22 @@
|
||||
></right-toolbar> -->
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="tableList" v-loading="state.loading" :row-key="(row: any) => row.id" lazy
|
||||
:load="load" :indent="30" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:summary-method="getSummaries" show-summary :header-cell-style="tableStyle.headerCellStyle"
|
||||
:cell-style="tableStyle.cellStyle" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle" :row-class-name="tableRowClassName">
|
||||
<el-table
|
||||
:data="tableList"
|
||||
v-loading="state.loading"
|
||||
:row-key="(row: any) => row.id"
|
||||
lazy
|
||||
:load="load"
|
||||
:indent="30"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:summary-method="getSummaries"
|
||||
show-summary
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column prop="username" label="用户" />
|
||||
<el-table-column prop="userAvatar" label="头像">
|
||||
@@ -57,8 +82,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="updateTime" label="最后充值时间" />
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</template>
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
|
||||
@@ -1,48 +1,66 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false">
|
||||
<DraggableDepTree
|
||||
ref="draggableDepTreeRef"
|
||||
placeholder="请输入部门名称"
|
||||
:show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode"
|
||||
switch-inactive-text="包含"
|
||||
switch-active-text="不包含"
|
||||
:defaultTreeWidth="20"
|
||||
:nodeClickable="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="充值方式" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_biz" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeIdList">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList"
|
||||
placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="消费时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
|
||||
type="daterange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">
|
||||
{{ $t("common.resetBtn") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="充值方式" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_biz" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeIdList">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList" placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="消费时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD" type="daterange" />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<right-toolbar :search="false" v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletTransaction_export'" @exportExcel="exportExcel" class="ml10 mr20"
|
||||
style="float: right" @queryTable="getDataList">
|
||||
<right-toolbar
|
||||
:search="false"
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletTransaction_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
>
|
||||
<el-button icon="DataLine" circle @click="viewBalanceStatistics" />
|
||||
</right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :summary-method="getSummaries"
|
||||
show-summary :cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:summary-method="getSummaries"
|
||||
show-summary
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="40" />
|
||||
<el-table-column width="120" prop="username" label="用户" show-overflow-tooltip />
|
||||
<el-table-column prop="userAvatar" label="头像" show-overflow-tooltip>
|
||||
@@ -71,10 +89,38 @@
|
||||
<div class="color-danger">{{ displayPrice(row.price) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" width="200" label="记账扣款时间" />
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="175"
|
||||
fixed="right"
|
||||
class-name="wallet-tx-actions-col"
|
||||
v-auths="['wallet_walletTransaction_edit', 'wallet_walletTransaction_update_info']"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="row-actions">
|
||||
<el-button
|
||||
icon="EditPen"
|
||||
link
|
||||
type="primary"
|
||||
v-auth="'wallet_walletTransaction_edit'"
|
||||
@click="remarkFormRef?.openDialog(row)"
|
||||
>修改备注</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="Edit"
|
||||
link
|
||||
type="primary"
|
||||
v-auth="'wallet_walletTransaction_update_info'"
|
||||
@click="infoFormRef?.openDialog(row)"
|
||||
>修改信息</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</template>
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
@@ -105,6 +151,8 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-drawer>
|
||||
<transaction-remark-form ref="remarkFormRef" @refresh="getDataList(false)" />
|
||||
<transaction-info-form ref="infoFormRef" @refresh="getDataList(false)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -120,12 +168,18 @@ const { push } = useRouter()
|
||||
|
||||
const showDrawer = ref(false)
|
||||
const balanceStatisticsLoading = ref(false)
|
||||
const balanceStatistics = reactive<any>({
|
||||
|
||||
})
|
||||
const balanceStatistics = reactive<any>({})
|
||||
|
||||
// 所有钱包流水列表共用的备注编辑表单。
|
||||
const TransactionRemarkForm = defineAsyncComponent(() => import("../components/transaction-remark-form.vue"))
|
||||
// 所有钱包流水列表共用的流水信息编辑表单(部门、创建时间)。
|
||||
const TransactionInfoForm = defineAsyncComponent(() => import("../components/transaction-info-form.vue"))
|
||||
// 定义查询字典
|
||||
const { wallet_type, wallet_biz } = useDict("wallet_type", "wallet_biz")
|
||||
// 通用流水备注编辑弹窗引用。
|
||||
const remarkFormRef = ref()
|
||||
// 通用流水信息编辑弹窗引用。
|
||||
const infoFormRef = ref()
|
||||
// 搜索变量
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
@@ -139,7 +193,7 @@ const defaultCreateTimeList = [
|
||||
]
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {
|
||||
createTimeList: defaultCreateTimeList
|
||||
createTimeList: defaultCreateTimeList,
|
||||
},
|
||||
pageList: pageJz,
|
||||
})
|
||||
@@ -184,7 +238,6 @@ const viewBalanceStatistics = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 导出excel
|
||||
const exportExcel = () => {
|
||||
postDownBlobFile(
|
||||
@@ -224,3 +277,22 @@ const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepT
|
||||
getDataList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 操作列按钮改为紧凑 flex 布局并收窄单元格内边距,避免按钮换行和两侧留白。 */
|
||||
.row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
|
||||
:deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.wallet-tx-actions-col .cell) {
|
||||
padding: 0 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -37,6 +37,18 @@
|
||||
<el-input v-model="balanceResult" placeholder="" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="操作标签" prop="tagList">
|
||||
<dict-select v-model="form.tagList" dict-code="wallet_transaction_tag" multiple />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3"
|
||||
:maxlength="walletTransactionApi.WALLET_TRANSACTION_REMARK_MAX_LENGTH" show-word-limit
|
||||
placeholder="请输入本次余额变动备注" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -73,6 +85,10 @@ const form = reactive<any>({
|
||||
balance: "0",
|
||||
changeBalance: 0,
|
||||
changeType: 1,
|
||||
// 页面使用数组承载多选值,提交时再转换为后端约定的英文逗号分隔格式。
|
||||
tagList: [],
|
||||
// 备注会写入本次余额变动生成的 wallet_transaction 流水,取消弹窗不会影响历史数据。
|
||||
remark: "",
|
||||
})
|
||||
|
||||
/** 变动后的余额 */
|
||||
@@ -81,6 +97,13 @@ const balanceResult = computed(() => displayPrice(convertToInteger(form.balance)
|
||||
// 定义校验规则
|
||||
const dataRules = ref({
|
||||
changeBalance: [{ required: true, message: "变动余额不能为空", trigger: "blur" }],
|
||||
remark: [
|
||||
{
|
||||
max: walletTransactionApi.WALLET_TRANSACTION_REMARK_MAX_LENGTH,
|
||||
message: `备注不能超过${walletTransactionApi.WALLET_TRANSACTION_REMARK_MAX_LENGTH}个字符`,
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 打开弹窗
|
||||
@@ -117,6 +140,8 @@ const onSubmit = async () => {
|
||||
userId: form.id,
|
||||
walletId: form.walletId,
|
||||
balance: form.changeBalance * 100,
|
||||
tags: form.tagList?.join(",") || undefined,
|
||||
remark: form.remark?.trim() || undefined,
|
||||
}
|
||||
if (pa.changeType == -1) {
|
||||
pa.balance = -1 * pa.balance
|
||||
@@ -145,6 +170,8 @@ const getwalletManageData = async (userId: string, id: string) => {
|
||||
form.balance = displayPrice(wallet.balance, "")
|
||||
form.changeType = 1
|
||||
form.changeBalance = 0
|
||||
form.tagList = []
|
||||
form.remark = ""
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false">
|
||||
<DraggableDepTree
|
||||
ref="draggableDepTreeRef"
|
||||
placeholder="请输入部门名称"
|
||||
:show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode"
|
||||
switch-inactive-text="包含"
|
||||
switch-active-text="不包含"
|
||||
:defaultTreeWidth="20"
|
||||
:nodeClickable="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
@@ -20,29 +36,40 @@
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">
|
||||
{{ $t("common.resetBtn") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button plain :disabled="multiple" icon="Delete" type="primary"
|
||||
v-auth="'wallet_walletManage_del'" @click="handleDelete(selectObjs)">
|
||||
<el-button
|
||||
plain
|
||||
:disabled="multiple"
|
||||
icon="Delete"
|
||||
type="primary"
|
||||
v-auth="'wallet_walletManage_del'"
|
||||
@click="handleDelete(selectObjs)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="'wallet_walletManage_export'"
|
||||
@exportExcel="exportExcel" class="ml10 mr20" style="float: right"
|
||||
@queryTable="getDataList"></right-toolbar>
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletManage_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :summary-method="getSummaries"
|
||||
show-summary :cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:summary-method="getSummaries"
|
||||
show-summary
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column prop="username" width="100" label="用户" />
|
||||
<el-table-column prop="userAvatar" width="80" label="头像">
|
||||
@@ -85,33 +112,51 @@
|
||||
</el-table-column>
|
||||
<el-table-column v-if="false" prop="activeBindStatus" width="120" label="饭卡绑定状态" />
|
||||
<el-table-column prop="createTime" width="200" label="创建时间" />
|
||||
<el-table-column label="操作" fixed="right" width="250" v-auths="[
|
||||
'wallet_walletManage_edit',
|
||||
'wallet_walletTransaction_update_balance',
|
||||
'wallet_walletRuleTransaction_transfer_accounts',
|
||||
]">
|
||||
<el-table-column
|
||||
label="操作"
|
||||
fixed="right"
|
||||
width="250"
|
||||
v-auths="[
|
||||
'wallet_walletManage_edit',
|
||||
'wallet_walletTransaction_update_balance',
|
||||
'wallet_walletRuleTransaction_transfer_accounts',
|
||||
]"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-start">
|
||||
<el-button icon="InfoFilled" text type="primary" v-auth="'wallet_walletManage_edit'"
|
||||
@click="formDialogRef.openDialog(row.id)">
|
||||
<el-button
|
||||
icon="InfoFilled"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'wallet_walletManage_edit'"
|
||||
@click="formDialogRef.openDialog(row.id)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button icon="Money" text type="primary"
|
||||
<el-button
|
||||
icon="Money"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'wallet_walletTransaction_update_balance'"
|
||||
@click="changeBalanceRef.openDialog(row.userId, row.id)">
|
||||
@click="changeBalanceRef.openDialog(row.userId, row.id)"
|
||||
>
|
||||
充值/扣款
|
||||
</el-button>
|
||||
<el-button v-if="row.isRule == 1" icon="SwitchFilled" text type="primary"
|
||||
<el-button
|
||||
v-if="row.isRule == 1"
|
||||
icon="SwitchFilled"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'wallet_walletRuleTransaction_transfer'"
|
||||
@click="doTransferAccounts(row)">
|
||||
@click="doTransferAccounts(row)"
|
||||
>
|
||||
转账
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</template>
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
|
||||
@@ -1,40 +1,64 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false">
|
||||
<DraggableDepTree
|
||||
ref="draggableDepTreeRef"
|
||||
placeholder="请输入部门名称"
|
||||
:show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode"
|
||||
switch-inactive-text="包含"
|
||||
switch-active-text="不包含"
|
||||
:defaultTreeWidth="20"
|
||||
:nodeClickable="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeIdList">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList"
|
||||
placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="月份" prop="walletRecordMonth">
|
||||
<el-date-picker v-model="state.queryForm.walletRecordMonth" value-format="YYYY-MM"
|
||||
type="month" :clearable="false" placeholder="选择月份" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">{{ $t("common.queryBtn")
|
||||
}}</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeIdList">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList" placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="月份" prop="walletRecordMonth">
|
||||
<el-date-picker
|
||||
v-model="state.queryForm.walletRecordMonth"
|
||||
value-format="YYYY-MM"
|
||||
type="month"
|
||||
:clearable="false"
|
||||
placeholder="选择月份"
|
||||
/>
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8 flex justify-between" style="width: 100%">
|
||||
<el-button plain :disabled="multiple" icon="Delete" type="primary"
|
||||
v-auth="'wallet_walletManage_del'" @click="handleDelete(selectObjs)">
|
||||
<el-button
|
||||
plain
|
||||
:disabled="multiple"
|
||||
icon="Delete"
|
||||
type="primary"
|
||||
v-auth="'wallet_walletManage_del'"
|
||||
@click="handleDelete(selectObjs)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<div class="flex gap-5">
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="'wallet_walletRecord_export'"
|
||||
@exportExcel="exportExcel" class="ml10 mr20" style="float: right"
|
||||
@queryTable="getDataList">
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletRecord_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
>
|
||||
<el-tooltip class="item" effect="dark" content="统计" placement="top">
|
||||
<el-button icon="DataLine" circle @click="viewBalanceStatistics" />
|
||||
</el-tooltip>
|
||||
@@ -42,9 +66,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :summary-method="getSummaries"
|
||||
show-summary :cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:summary-method="getSummaries"
|
||||
show-summary
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column prop="username" width="120" label="用户" />
|
||||
<el-table-column prop="userAvatar" width="80" label="头像">
|
||||
@@ -83,8 +115,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</template>
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
@@ -137,9 +168,7 @@ const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
const showDrawer = ref(false) // 余额统计抽屉开关
|
||||
const balanceStatisticsLoading = ref(false)
|
||||
const balanceStatistics = reactive<any>({
|
||||
|
||||
})
|
||||
const balanceStatistics = reactive<any>({})
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {
|
||||
|
||||
@@ -1,64 +1,89 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-form v-show="showSearch" ref="queryRef" :inline="true" :model="state.queryForm"
|
||||
@keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" placeholder="用户" />
|
||||
</el-form-item>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" placeholder="用户" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="width: 200px" label="充值类型" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_biz_recharge" placeholder="充值类型" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="width: 450px" label="创建时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
|
||||
</el-form-item>
|
||||
|
||||
<template #more>
|
||||
<el-form-item style="width: 200px" label="饭卡类型" prop="walletTypeId">
|
||||
<select-wallet-type v-model="state.queryForm.walletTypeId" placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="充值类型" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_biz_recharge"
|
||||
placeholder="充值类型" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="发票号" prop="invoiceCode">
|
||||
<el-input v-model="state.queryForm.invoiceCode" placeholder="发票号" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="创建时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="datetimerange" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="审核时间" prop="processTimeList">
|
||||
<el-date-picker v-model="state.queryForm.processTimeList" value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="datetimerange" />
|
||||
<el-date-picker v-model="state.queryForm.processTimeList" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">{{ $t("common.queryBtn")
|
||||
}}</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="uploadExcelRef.show()"
|
||||
v-auth="'wallet_walletRechargeApply_add'">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="uploadExcelRef.show()" v-auth="'wallet_walletRechargeApply_add'">
|
||||
导 入
|
||||
</el-button>
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="uploadExcelUnbindRef.show()"
|
||||
v-auth="'wallet_walletRechargeApply_unbind'">
|
||||
<el-button
|
||||
icon="folder-add"
|
||||
type="primary"
|
||||
class="ml10"
|
||||
@click="uploadExcelUnbindRef.show()"
|
||||
v-auth="'wallet_walletRechargeApply_unbind'"
|
||||
>
|
||||
导 入(不绑定发票)
|
||||
</el-button>
|
||||
<el-button v-if="false" plain :disabled="multiple" icon="Delete" type="primary"
|
||||
v-auth="'wallet_walletRechargeApply_del'" @click="handleDelete(selectObjs)">
|
||||
<el-button
|
||||
v-if="false"
|
||||
plain
|
||||
:disabled="multiple"
|
||||
icon="Delete"
|
||||
type="primary"
|
||||
v-auth="'wallet_walletRechargeApply_del'"
|
||||
@click="handleDelete(selectObjs)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<div class="flex justify-between items-center">
|
||||
<el-tabs class="flex-1" v-model="activeName" @tab-click="handleClickTabItem">
|
||||
<el-tab-pane v-for="v in tabItems" :key="v.id" :label="v.label" :name="v.value" />
|
||||
</el-tabs>
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="'wallet_walletRechargeApply_export'"
|
||||
@exportExcel="exportExcel" class="ml10 mr20" style="float: right"
|
||||
@queryTable="getDataList" />
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletRechargeApply_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="selection" width="40" align="center" />
|
||||
<el-table-column type="index" label="#" width="80" />
|
||||
<el-table-column prop="username" label="申请人" />
|
||||
@@ -116,59 +141,111 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" v-auths="[
|
||||
'wallet_walletRechargeApply_view',
|
||||
'wallet_walletRechargeApply_apply',
|
||||
'wallet_walletRechargeApply_edit',
|
||||
'wallet_walletRechargeApply_del',
|
||||
'wallet_walletRechargeApply_check',
|
||||
]" width="300" fixed="right">
|
||||
<el-table-column
|
||||
label="操作"
|
||||
v-auths="[
|
||||
'wallet_walletRechargeApply_view',
|
||||
'wallet_walletRechargeApply_apply',
|
||||
'wallet_walletRechargeApply_edit',
|
||||
'wallet_walletRechargeApply_del',
|
||||
'wallet_walletRechargeApply_check',
|
||||
]"
|
||||
width="300"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.importTime" icon="Document" text type="primary"
|
||||
v-auth="'wallet_walletRechargeApply_view'" @click="doInfo(row)">
|
||||
<el-button
|
||||
v-if="row.importTime"
|
||||
icon="Document"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'wallet_walletRechargeApply_view'"
|
||||
@click="doInfo(row)"
|
||||
>
|
||||
数据
|
||||
</el-button>
|
||||
<el-button v-if="row.importTime && !row.processId" icon="Connection" text type="primary"
|
||||
v-auth="'wallet_walletRechargeApply_apply'" @click="doApply(row)">
|
||||
<el-button
|
||||
v-if="row.importTime && !row.processId"
|
||||
icon="Connection"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'wallet_walletRechargeApply_apply'"
|
||||
@click="doApply(row)"
|
||||
>
|
||||
申请
|
||||
</el-button>
|
||||
<el-button v-if="!row.processId" icon="edit-pen" text type="primary"
|
||||
v-auth="'wallet_walletRechargeApply_edit'" @click="formDialogRef.openDialog(row.id)">
|
||||
<el-button
|
||||
v-if="!row.processId"
|
||||
icon="edit-pen"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'wallet_walletRechargeApply_edit'"
|
||||
@click="formDialogRef.openDialog(row.id)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button v-if="row.isCheckUser && row.processStatus == '1'" type="primary" size="small" link
|
||||
icon="VideoPlay" @click="deal(row)" v-auth="'wallet_walletRechargeApply_check'">
|
||||
<el-button
|
||||
v-if="row.isCheckUser && row.processStatus == '1'"
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
icon="VideoPlay"
|
||||
@click="deal(row)"
|
||||
v-auth="'wallet_walletRechargeApply_check'"
|
||||
>
|
||||
开始处理
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.userId == userInfos?.user?.userId && [null, '1'].includes(row.processStatus)"
|
||||
icon="delete" text type="primary" v-auth="'wallet_walletRechargeApply_del'"
|
||||
@click="handleDelete([row.id])">
|
||||
icon="delete"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'wallet_walletRechargeApply_del'"
|
||||
@click="handleDelete([row.id])"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
|
||||
<upload-excel ref="uploadExcelRef" title="充值导入" url="/wallet/wallet-recharge-apply/read-excel"
|
||||
temp-url="/admin/sys-file/local/file/wallet_recharge.xlsx" @refreshDataList="getDataList(false)" />
|
||||
<upload-excel-unbind ref="uploadExcelUnbindRef" title="充值导入" url="/wallet/wallet-recharge-apply/read-excel"
|
||||
temp-url="/admin/sys-file/local/file/wallet_recharge.xlsx" @refreshDataList="getDataList(false)" />
|
||||
<upload-excel
|
||||
ref="uploadExcelRef"
|
||||
title="充值导入"
|
||||
url="/wallet/wallet-recharge-apply/read-excel"
|
||||
temp-url="/admin/sys-file/local/file/wallet_recharge.xlsx"
|
||||
@refreshDataList="getDataList(false)"
|
||||
/>
|
||||
<upload-excel-unbind
|
||||
ref="uploadExcelUnbindRef"
|
||||
title="充值导入"
|
||||
url="/wallet/wallet-recharge-apply/read-excel"
|
||||
temp-url="/admin/sys-file/local/file/wallet_recharge.xlsx"
|
||||
@refreshDataList="getDataList(false)"
|
||||
/>
|
||||
|
||||
<!-- 审批抽屉 -->
|
||||
<process-drawer ref="processDrawerRef" :width="errorState.errorList?.length ? '55%' : '400px'"
|
||||
:errorMessage="errorState.errorList?.length ? '当前还有错误信息,是否继续?' : ''" @task-submit-event="taskSubmitEvent">
|
||||
<process-drawer
|
||||
ref="processDrawerRef"
|
||||
:width="errorState.errorList?.length ? '55%' : '400px'"
|
||||
:errorMessage="errorState.errorList?.length ? '当前还有错误信息,是否继续?' : ''"
|
||||
@task-submit-event="taskSubmitEvent"
|
||||
>
|
||||
<template #top>
|
||||
<div v-if="errorState.errorList?.length" class="w-[100%]">
|
||||
<div class="title">错误信息</div>
|
||||
<el-table :data="errorState.errorList" v-loading="errorState.loading" border
|
||||
:cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle">
|
||||
<el-table
|
||||
:data="errorState.errorList"
|
||||
v-loading="errorState.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
>
|
||||
<el-table-column prop="username" width="150" label="用户" align="center" />
|
||||
<el-table-column prop="userMobile" width="120" label="手机号" align="center" />
|
||||
<el-table-column prop="bizType" width="120" label="充值类型" align="center">
|
||||
@@ -363,7 +440,7 @@ const deal = async (row: any) => {
|
||||
errorState.errorList = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("查询是否有错误error:", error);
|
||||
console.log("查询是否有错误error:", error)
|
||||
errorState.errorList = []
|
||||
} finally {
|
||||
errorState.loading = false
|
||||
|
||||
@@ -1,58 +1,70 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-form v-show="showSearch" ref="queryRef" :inline="true" :model="state.queryForm"
|
||||
@keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 350px" label="申请用户" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" placeholder="申请用户" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 350px" label="交款单位或个人" prop="deptOrUser">
|
||||
<el-input v-model="state.queryForm.deptOrUser" clearable placeholder="交款单位或个人" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 350px" label="发票状态" prop="userIds">
|
||||
<dict-select v-model="state.queryForm.status" dict-code="recharge_invoice_status" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 350px" label="发票号" prop="code">
|
||||
<el-input v-model="state.queryForm.code" />
|
||||
</el-form-item>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 350px" label="申请用户" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" placeholder="申请用户" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 350px" label="交款单位或个人" prop="deptOrUser">
|
||||
<el-input v-model="state.queryForm.deptOrUser" clearable placeholder="交款单位或个人" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 350px" label="发票状态" prop="userIds">
|
||||
<dict-select v-model="state.queryForm.status" dict-code="recharge_invoice_status" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 350px" label="发票号" prop="code">
|
||||
<el-input v-model="state.queryForm.code" />
|
||||
</el-form-item>
|
||||
|
||||
<template #more>
|
||||
<el-form-item style="width: 350px" label="备注" prop="remark">
|
||||
<el-input v-model="state.queryForm.remark" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="创建时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="datetimerange" />
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="审核时间" prop="processTimeList">
|
||||
<el-date-picker v-model="state.queryForm.processTimeList" value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="datetimerange" />
|
||||
<el-date-picker v-model="state.queryForm.processTimeList" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">{{ $t("common.queryBtn")
|
||||
}}</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="uploadExcelRef.show()"
|
||||
v-auth="'wallet_walletRechargeApply_add'">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="uploadExcelRef.show()" v-auth="'wallet_walletRechargeApply_add'">
|
||||
导 入
|
||||
</el-button>
|
||||
<div class="flex justify-between items-center">
|
||||
<el-tabs class="flex-1" v-model="activeName" @tab-click="handleClickTabItem">
|
||||
<el-tab-pane v-for="v in tabItems" :key="v.id" :label="v.label" :name="v.value" />
|
||||
</el-tabs>
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="'wallet_walletRechargeInvoice_export'"
|
||||
@exportExcel="exportExcel" class="ml10 mr20" style="float: right"
|
||||
@queryTable="getDataList" />
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletRechargeInvoice_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="selection" width="40" align="center" />
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table-column prop="code" min-width="120" label="发票编号" sortable />
|
||||
@@ -82,8 +94,11 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="stampPicUrl" min-width="120" label="发票图片">
|
||||
<template #default="{ row }">
|
||||
<preview-image :src="row.stampPicUrl" download
|
||||
:downloadName="`${row.code}-${row.deptOrUser}-${row.payeeDept}-发票-${row.updateTime}`" />
|
||||
<preview-image
|
||||
:src="row.stampPicUrl"
|
||||
download
|
||||
:downloadName="`${row.code}-${row.deptOrUser}-${row.payeeDept}-发票-${row.updateTime}`"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" show-overflow-tooltip />
|
||||
@@ -106,32 +121,53 @@
|
||||
<el-table-column label="操作" min-width="320" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="flex">
|
||||
<el-button v-if="!['6'].includes(row.status)" icon="Document" text type="primary"
|
||||
v-auth="'wallet_walletRechargeApply_view'" @click="doInfo(row)">
|
||||
<el-button
|
||||
v-if="!['6'].includes(row.status)"
|
||||
icon="Document"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'wallet_walletRechargeApply_view'"
|
||||
@click="doInfo(row)"
|
||||
>
|
||||
数据
|
||||
</el-button>
|
||||
<el-button v-auth="'wallet_walletRechargeApply_print'" icon="printer" text type="primary"
|
||||
:disabled="loading" @click="previewInvoice(row)">
|
||||
<el-button
|
||||
v-auth="'wallet_walletRechargeApply_print'"
|
||||
icon="printer"
|
||||
text
|
||||
type="primary"
|
||||
:disabled="loading"
|
||||
@click="previewInvoice(row)"
|
||||
>
|
||||
查看发票
|
||||
</el-button>
|
||||
<el-button v-if="row.isCheckUser && row.processStatus == '1'" type="primary" link
|
||||
icon="VideoPlay" :disabled="loading" @click="deal(row)">
|
||||
<el-button
|
||||
v-if="row.isCheckUser && row.processStatus == '1'"
|
||||
type="primary"
|
||||
link
|
||||
icon="VideoPlay"
|
||||
:disabled="loading"
|
||||
@click="deal(row)"
|
||||
>
|
||||
开始处理
|
||||
</el-button>
|
||||
<el-button icon="delete" text type="primary" v-auth="'wallet_walletRechargeInvoice_del'"
|
||||
@click="handleDelete([row.id])">
|
||||
<el-button icon="delete" text type="primary" v-auth="'wallet_walletRechargeInvoice_del'" @click="handleDelete([row.id])">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
|
||||
<upload-excel ref="uploadExcelRef" title="导入" url="/wallet/wallet-recharge-apply/read-topup-excel"
|
||||
temp-url="/admin/sys-file/local/file/wallet_recharge.xlsx" @refreshDataList="getDataList(false)" />
|
||||
<upload-excel
|
||||
ref="uploadExcelRef"
|
||||
title="导入"
|
||||
url="/wallet/wallet-recharge-apply/read-topup-excel"
|
||||
temp-url="/admin/sys-file/local/file/wallet_recharge.xlsx"
|
||||
@refreshDataList="getDataList(false)"
|
||||
/>
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
@@ -197,20 +233,25 @@ const resetQuery = () => {
|
||||
|
||||
const doInfo = (row: any) => {
|
||||
push({
|
||||
path: "/wallet/wallet-recharge-apply/preview-excel", query: {
|
||||
path: "/wallet/wallet-recharge-apply/preview-excel",
|
||||
query: {
|
||||
id: null,
|
||||
invoiceId: row.id
|
||||
}
|
||||
invoiceId: row.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 导出excel
|
||||
const exportExcel = () => {
|
||||
postDownBlobFile("/wallet/walletRechargeInvoice/export", {
|
||||
...state.queryForm,
|
||||
ascs: state.ascs,
|
||||
descs: state.descs,
|
||||
}, "充值扣款发票申请.xlsx")
|
||||
postDownBlobFile(
|
||||
"/wallet/walletRechargeInvoice/export",
|
||||
{
|
||||
...state.queryForm,
|
||||
ascs: state.ascs,
|
||||
descs: state.descs,
|
||||
},
|
||||
"充值扣款发票申请.xlsx",
|
||||
)
|
||||
}
|
||||
|
||||
// 多选事件
|
||||
|
||||
@@ -1,54 +1,77 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false">
|
||||
<DraggableDepTree
|
||||
ref="draggableDepTreeRef"
|
||||
placeholder="请输入部门名称"
|
||||
:show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode"
|
||||
switch-inactive-text="包含"
|
||||
switch-active-text="不包含"
|
||||
:defaultTreeWidth="20"
|
||||
:nodeClickable="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" placeholder="用户" />
|
||||
</el-form-item>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" placeholder="用户" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="width: 200px" label="审核状态" prop="processStatus">
|
||||
<dict-select v-model="state.queryForm.processStatus" dict-code="check_status" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="width: 450px" label="审核时间" prop="processTimeList">
|
||||
<el-date-picker v-model="state.queryForm.processTimeList" value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
|
||||
</el-form-item>
|
||||
|
||||
<template #more>
|
||||
<el-form-item style="width: 200px" label="充值类型" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_biz_recharge" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="审核状态" prop="processStatus">
|
||||
<dict-select v-model="state.queryForm.processStatus" dict-code="check_status" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="钱包类型" prop="walletTypeId">
|
||||
<select-wallet-type v-model="state.queryForm.walletTypeId" placeholder="饭卡类型"
|
||||
clearable />
|
||||
<select-wallet-type v-model="state.queryForm.walletTypeId" placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 450px" label="审核时间" prop="processTimeList">
|
||||
<el-date-picker v-model="state.queryForm.processTimeList"
|
||||
value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<right-toolbar v-model:showSearch="showSearch" :export="'wallet_walletRechargeApply_export'"
|
||||
@exportExcel="exportExcel" class="ml10 mr20" style="float: right"
|
||||
@queryTable="getDataList"></right-toolbar>
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletRechargeApply_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle" :summary-method="getSummaries" show-summary
|
||||
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
:summary-method="getSummaries"
|
||||
show-summary
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="80" />
|
||||
<el-table-column width="200" label="用户">
|
||||
<template #default="{ row }">
|
||||
<div class="flex flex-row items-center justify-center">
|
||||
<preview-image width="50px" height="50px" :src="row.userAvatar" />
|
||||
<div
|
||||
class="flex flex-col ml-2 items-start cursor-pointer hover:underline hover:text-blue-500">
|
||||
<div class="flex flex-col ml-2 items-start cursor-pointer hover:underline hover:text-blue-500">
|
||||
<span> {{ row.username }} </span>
|
||||
<span> {{ row.userMobile }} </span>
|
||||
<span> {{ row.userDeptName }} </span>
|
||||
@@ -90,15 +113,19 @@
|
||||
</el-table-column>
|
||||
<el-table-column v-auths="['wallet_walletRechargeImport_recover']" label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button v-auth="'wallet_walletRechargeImport_recover'" icon="RefreshRight" text
|
||||
type="primary" @click="handleRecover(row.id)">
|
||||
<el-button
|
||||
v-auth="'wallet_walletRechargeImport_recover'"
|
||||
icon="RefreshRight"
|
||||
text
|
||||
type="primary"
|
||||
@click="handleRecover(row.id)"
|
||||
>
|
||||
恢复
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</template>
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
|
||||
@@ -1,48 +1,69 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<DraggableDepTree
|
||||
ref="draggableDepTreeRef"
|
||||
placeholder="请输入部门名称"
|
||||
:show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode"
|
||||
switch-inactive-text="包含"
|
||||
switch-active-text="不包含"
|
||||
:defaultTreeWidth="20"
|
||||
:nodeClickable="false"
|
||||
>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 250px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="是否退回" prop="isRefund">
|
||||
<dict-select v-model="state.queryForm.isRefund" dict-code="yes_no_type" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="width: 250px" label="转账类型" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_rule_transation_type" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 350px" label="转账时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
|
||||
type="daterange" />
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD" type="daterange" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 350px" label="退回时间" prop="refundTimeList">
|
||||
<el-date-picker v-model="state.queryForm.refundTimeList" value-format="YYYY-MM-DD"
|
||||
type="daterange" />
|
||||
</el-form-item>
|
||||
<el-row>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">
|
||||
{{ $t("common.resetBtn") }}
|
||||
</el-button>
|
||||
|
||||
<el-row> </el-row>
|
||||
|
||||
<template #more>
|
||||
<el-form-item style="width: 250px" label="是否退回" prop="isRefund">
|
||||
<dict-select v-model="state.queryForm.isRefund" dict-code="yes_no_type" />
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<el-form-item style="width: 350px" label="退回时间" prop="refundTimeList">
|
||||
<el-date-picker v-model="state.queryForm.refundTimeList" value-format="YYYY-MM-DD" type="daterange" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<!-- :export="'wallet_walletRuleTransaction_export'" -->
|
||||
<right-toolbar v-model:showSearch="showSearch" @exportExcel="exportExcel" class="ml10 mr20"
|
||||
style="float: right" @queryTable="getDataList"></right-toolbar>
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column label="用户">
|
||||
<el-table-column width="120" prop="username" label="姓名" />
|
||||
@@ -109,18 +130,22 @@
|
||||
<el-table-column prop="refundTime" width="170" label="退回时间" />
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" width="170" label="创建时间" />
|
||||
<el-table-column label="操作" fixed="right" width="80"
|
||||
v-auths="['wallet_walletRuleTransaction_refund']">
|
||||
<el-table-column label="操作" fixed="right" width="80" v-auths="['wallet_walletRuleTransaction_refund']">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.isRefund == '0'" icon="RefreshRight" text type="primary"
|
||||
v-auth="'wallet_walletRuleTransaction_refund'" @click="doRefund(row)">
|
||||
<el-button
|
||||
v-if="row.isRefund == '0'"
|
||||
icon="RefreshRight"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'wallet_walletRuleTransaction_refund'"
|
||||
@click="doRefund(row)"
|
||||
>
|
||||
退回
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
|
||||
@@ -157,7 +182,7 @@ const defaultCreateTimeList = [
|
||||
]
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {
|
||||
createTimeList: defaultCreateTimeList
|
||||
createTimeList: defaultCreateTimeList,
|
||||
},
|
||||
pageList: walletRuleTransactionApi.pageJoin,
|
||||
})
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">{{ $t("common.queryBtn") }}</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t("common.resetBtn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userIds">
|
||||
<select-user v-model="state.queryForm.userIds" />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()" v-auth="'admin_walletStride_add'">
|
||||
|
||||
@@ -1,45 +1,63 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false">
|
||||
<DraggableDepTree
|
||||
ref="draggableDepTreeRef"
|
||||
placeholder="请输入部门名称"
|
||||
:show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode"
|
||||
switch-inactive-text="包含"
|
||||
switch-active-text="不包含"
|
||||
:defaultTreeWidth="20"
|
||||
:nodeClickable="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeId">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList"
|
||||
placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="退款时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
|
||||
type="daterange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">
|
||||
{{ $t("common.resetBtn") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeId">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList" placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="退款时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD" type="daterange" />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<right-toolbar :search="false" v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletTransaction_export'" @exportExcel="exportExcel" class="ml10 mr20"
|
||||
style="float: right" @queryTable="getDataList">
|
||||
<right-toolbar
|
||||
:search="false"
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletTransaction_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
>
|
||||
<el-button icon="DataLine" circle @click="viewBalanceStatistics" />
|
||||
</right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :summary-method="getSummaries"
|
||||
show-summary :cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:summary-method="getSummaries"
|
||||
show-summary
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column prop="username" width="120" label="用户" show-overflow-tooltip />
|
||||
<el-table-column prop="userAvatar" label="头像" show-overflow-tooltip>
|
||||
@@ -68,21 +86,38 @@
|
||||
<div class="color-danger">{{ displayPrice(row.price) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" width="200" label="退款时间" />
|
||||
<el-table-column label="操作" width="150">
|
||||
<el-table-column label="操作" width="240" fixed="right" class-name="wallet-tx-actions-col">
|
||||
<template #default="{ row }">
|
||||
<el-button icon="InfoFilled" link type="primary" @click="doRefund(row)"> 详情 </el-button>
|
||||
<el-button v-if="false" icon="edit-pen" text type="primary"
|
||||
v-auth="'wallet_walletRecharge_edit'"
|
||||
@click="formDialogRef.openDialog(row.id)">编辑</el-button>
|
||||
<div class="row-actions">
|
||||
<el-button icon="InfoFilled" link type="primary" @click="doRefund(row)"> 详情 </el-button>
|
||||
<el-button
|
||||
icon="EditPen"
|
||||
link
|
||||
type="primary"
|
||||
v-auth="'wallet_walletTransaction_edit'"
|
||||
@click="remarkFormRef?.openDialog(row)"
|
||||
>修改备注</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="Edit"
|
||||
link
|
||||
type="primary"
|
||||
v-auth="'wallet_walletTransaction_update_info'"
|
||||
@click="infoFormRef?.openDialog(row)"
|
||||
>修改信息</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</template>
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
<transaction-remark-form ref="remarkFormRef" @refresh="getDataList(false)" />
|
||||
<transaction-info-form ref="infoFormRef" @refresh="getDataList(false)" />
|
||||
<el-drawer v-model="showDrawer" @closed="drawerClose">
|
||||
<el-table :data="[balanceStatistics]" style="width: 100%">
|
||||
<el-table-column align="center" label="退还各自食堂统计">
|
||||
@@ -123,14 +158,20 @@ import { displayPrice } from "/@/utils/formatMoney"
|
||||
|
||||
const showDrawer = ref(false)
|
||||
const balanceStatisticsLoading = ref(false)
|
||||
const balanceStatistics = reactive<any>({
|
||||
|
||||
})
|
||||
const balanceStatistics = reactive<any>({})
|
||||
|
||||
// 所有钱包流水列表共用的备注编辑表单。
|
||||
const TransactionRemarkForm = defineAsyncComponent(() => import("../components/transaction-remark-form.vue"))
|
||||
// 所有钱包流水列表共用的流水信息编辑表单(部门、创建时间)。
|
||||
const TransactionInfoForm = defineAsyncComponent(() => import("../components/transaction-info-form.vue"))
|
||||
// 定义查询字典
|
||||
const { wallet_type, wallet_biz } = useDict("wallet_type", "wallet_biz")
|
||||
// 定义变量内容
|
||||
const formDialogRef = ref()
|
||||
// 通用流水备注编辑弹窗引用。
|
||||
const remarkFormRef = ref()
|
||||
// 通用流水信息编辑弹窗引用。
|
||||
const infoFormRef = ref()
|
||||
// 搜索变量
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
@@ -147,7 +188,7 @@ const defaultCreateTimeList = [
|
||||
]
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {
|
||||
createTimeList: defaultCreateTimeList
|
||||
createTimeList: defaultCreateTimeList,
|
||||
},
|
||||
pageList: pageTh,
|
||||
})
|
||||
@@ -239,3 +280,22 @@ const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepT
|
||||
getDataList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 操作列按钮改为紧凑 flex 布局并收窄单元格内边距,避免按钮换行和两侧留白。 */
|
||||
.row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
|
||||
:deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.wallet-tx-actions-col .cell) {
|
||||
padding: 0 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,45 +1,63 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false">
|
||||
<DraggableDepTree
|
||||
ref="draggableDepTreeRef"
|
||||
placeholder="请输入部门名称"
|
||||
:show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode"
|
||||
switch-inactive-text="包含"
|
||||
switch-active-text="不包含"
|
||||
:defaultTreeWidth="20"
|
||||
:nodeClickable="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeIdList">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList"
|
||||
placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="退款时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
|
||||
type="daterange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">
|
||||
{{ $t("common.resetBtn") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeIdList">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList" placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="退款时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD" type="daterange" />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<right-toolbar :search="false" v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletTransaction_export'" @exportExcel="exportExcel" class="ml10 mr20"
|
||||
style="float: right" @queryTable="getDataList">
|
||||
<right-toolbar
|
||||
:search="false"
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletTransaction_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
>
|
||||
<el-button icon="DataLine" circle @click="viewBalanceStatistics" />
|
||||
</right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :summary-method="getSummaries"
|
||||
show-summary :cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:summary-method="getSummaries"
|
||||
show-summary
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column width="120" prop="username" label="用户" show-overflow-tooltip />
|
||||
<el-table-column prop="userAvatar" label="头像" show-overflow-tooltip>
|
||||
@@ -68,23 +86,40 @@
|
||||
<div class="color-success">{{ displayPrice(row.price) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" width="200" label="退款时间" />
|
||||
<el-table-column label="操作" width="150">
|
||||
<el-table-column label="操作" width="240" fixed="right" class-name="wallet-tx-actions-col">
|
||||
<template #default="{ row }">
|
||||
<el-button icon="InfoFilled" link type="primary" @click="doRefund(row)"> 详情 </el-button>
|
||||
<el-button v-if="false" icon="edit-pen" text type="primary"
|
||||
v-auth="'wallet_walletRecharge_edit'"
|
||||
@click="formDialogRef.openDialog(row.id)">编辑</el-button>
|
||||
<div class="row-actions">
|
||||
<el-button icon="InfoFilled" link type="primary" @click="doRefund(row)"> 详情 </el-button>
|
||||
<el-button
|
||||
icon="EditPen"
|
||||
link
|
||||
type="primary"
|
||||
v-auth="'wallet_walletTransaction_edit'"
|
||||
@click="remarkFormRef?.openDialog(row)"
|
||||
>修改备注</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="Edit"
|
||||
link
|
||||
type="primary"
|
||||
v-auth="'wallet_walletTransaction_update_info'"
|
||||
@click="infoFormRef?.openDialog(row)"
|
||||
>修改信息</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</template>
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
<transaction-remark-form ref="remarkFormRef" @refresh="getDataList(false)" />
|
||||
<transaction-info-form ref="infoFormRef" @refresh="getDataList(false)" />
|
||||
<el-drawer v-model="showDrawer" @closed="drawerClose">
|
||||
<el-table :data="[balanceStatistics]" style="width: 100%">
|
||||
<el-table-column align="center" label="充值统计">
|
||||
@@ -125,16 +160,22 @@ import { displayPrice } from "/@/utils/formatMoney"
|
||||
|
||||
const showDrawer = ref(false)
|
||||
const balanceStatisticsLoading = ref(false)
|
||||
const balanceStatistics = reactive<any>({
|
||||
|
||||
})
|
||||
const balanceStatistics = reactive<any>({})
|
||||
|
||||
// 引入组件
|
||||
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
|
||||
// 所有钱包流水列表共用的备注编辑表单。
|
||||
const TransactionRemarkForm = defineAsyncComponent(() => import("../components/transaction-remark-form.vue"))
|
||||
// 所有钱包流水列表共用的流水信息编辑表单(部门、创建时间)。
|
||||
const TransactionInfoForm = defineAsyncComponent(() => import("../components/transaction-info-form.vue"))
|
||||
// 定义查询字典
|
||||
const { wallet_type, wallet_biz } = useDict("wallet_type", "wallet_biz")
|
||||
// 定义变量内容
|
||||
const formDialogRef = ref()
|
||||
// 通用流水备注编辑弹窗引用。
|
||||
const remarkFormRef = ref()
|
||||
// 通用流水信息编辑弹窗引用。
|
||||
const infoFormRef = ref()
|
||||
// 搜索变量
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
@@ -151,7 +192,7 @@ const defaultCreateTimeList = [
|
||||
]
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {
|
||||
createTimeList: defaultCreateTimeList
|
||||
createTimeList: defaultCreateTimeList,
|
||||
},
|
||||
pageList: pageTk,
|
||||
})
|
||||
@@ -243,3 +284,22 @@ const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepT
|
||||
getDataList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 操作列按钮改为紧凑 flex 布局并收窄单元格内边距,避免按钮换行和两侧留白。 */
|
||||
.row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
|
||||
:deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.wallet-tx-actions-col .cell) {
|
||||
padding: 0 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,32 +1,35 @@
|
||||
<template>
|
||||
<el-dialog title="钱包规则" width="85%" v-model="visible" append-to-body :close-on-click-modal="false" draggable>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="部门" prop="targetDeptId">
|
||||
<select-dept v-model="state.queryForm.targetDeptId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="是否启用" prop="status">
|
||||
<dict-select v-model="state.queryForm.status" dict-code="yes_no_type" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">
|
||||
{{ $t("common.resetBtn") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<SearchPanel
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="部门" prop="targetDeptId">
|
||||
<select-dept v-model="state.queryForm.targetDeptId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="是否启用" prop="status">
|
||||
<dict-select v-model="state.queryForm.status" dict-code="yes_no_type" />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="doAdd"
|
||||
v-auth="'dzc_walletRuleMchConfig_add'"> 新 增 </el-button>
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="doAdd" v-auth="'dzc_walletRuleMchConfig_add'"> 新 增 </el-button>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="selection" width="40" align="center" />
|
||||
<el-table-column type="index" label="#" width="80" />
|
||||
<el-table-column min-width="120" label="源钱包类型">
|
||||
@@ -62,8 +65,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="isReservePrice" min-width="100" label="是否保留100元用于跨单位">
|
||||
<template #default="{ row }">
|
||||
<el-switch active-value="1" inactive-value="0" v-model="row.isReservePrice"
|
||||
@change="isReservePriceChange(row)" />
|
||||
<el-switch active-value="1" inactive-value="0" v-model="row.isReservePrice" @change="isReservePriceChange(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column min-width="300" label="描述">
|
||||
@@ -73,10 +75,17 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button icon="edit-pen" text type="primary" v-auth="'dzc_walletRuleMchConfig_edit'"
|
||||
@click="WalletRuleFormDialogRef.openDialog(row.id, row.sourceWalletTypeId)">编辑</el-button>
|
||||
<el-button icon="delete" text type="primary" v-auth="'dzc_walletRuleMchConfig_del'"
|
||||
@click="handleDelete([row.id])">删除</el-button>
|
||||
<el-button
|
||||
icon="edit-pen"
|
||||
text
|
||||
type="primary"
|
||||
v-auth="'dzc_walletRuleMchConfig_edit'"
|
||||
@click="WalletRuleFormDialogRef.openDialog(row.id, row.sourceWalletTypeId)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button icon="delete" text type="primary" v-auth="'dzc_walletRuleMchConfig_del'" @click="handleDelete([row.id])"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
@@ -1,48 +1,66 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
|
||||
:defaultTreeWidth="20" :nodeClickable="false">
|
||||
<DraggableDepTree
|
||||
ref="draggableDepTreeRef"
|
||||
placeholder="请输入部门名称"
|
||||
:show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode"
|
||||
switch-inactive-text="包含"
|
||||
switch-active-text="不包含"
|
||||
:defaultTreeWidth="20"
|
||||
:nodeClickable="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-row>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="充值方式" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_biz" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeIdList">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList"
|
||||
placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="消费时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD"
|
||||
type="daterange" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">
|
||||
{{ $t("common.resetBtn") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<SearchPanel
|
||||
v-show="showSearch"
|
||||
ref="queryRef"
|
||||
:model="state.queryForm"
|
||||
:loading="Boolean(state.loading)"
|
||||
:search-text="$t('common.queryBtn')"
|
||||
:reset-text="$t('common.resetBtn')"
|
||||
@search="getDataList"
|
||||
@reset="resetQuery"
|
||||
>
|
||||
<el-form-item style="width: 300px" label="用户" prop="userId">
|
||||
<select-user v-model="state.queryForm.userId" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 200px" label="充值方式" prop="bizType">
|
||||
<dict-select v-model="state.queryForm.bizType" dict-code="wallet_biz" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="钱包类型" prop="walletTypeIdList">
|
||||
<select-wallet-type multiple v-model="state.queryForm.walletTypeIdList" placeholder="饭卡类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 300px" label="消费时间" prop="createTimeList">
|
||||
<el-date-picker v-model="state.queryForm.createTimeList" value-format="YYYY-MM-DD" type="daterange" />
|
||||
</el-form-item>
|
||||
</SearchPanel>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<right-toolbar :search="false" v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletTransaction_export'" @exportExcel="exportExcel" class="ml10 mr20"
|
||||
style="float: right" @queryTable="getDataList">
|
||||
<right-toolbar
|
||||
:search="false"
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'wallet_walletTransaction_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
>
|
||||
<el-button icon="DataLine" circle @click="viewBalanceStatistics" />
|
||||
</right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="state.dataList" v-loading="state.loading" border :summary-method="getSummaries"
|
||||
show-summary :cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:summary-method="getSummaries"
|
||||
show-summary
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column width="120" prop="username" label="用户" show-overflow-tooltip />
|
||||
<el-table-column prop="userAvatar" label="头像" show-overflow-tooltip>
|
||||
@@ -71,26 +89,41 @@
|
||||
<div class="color-danger">{{ displayPrice(row.price) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" width="200" label="消费时间" />
|
||||
<el-table-column label="操作" width="150">
|
||||
<el-table-column label="操作" width="240" fixed="right" class-name="wallet-tx-actions-col">
|
||||
<template #default="{ row }">
|
||||
<el-button icon="InfoFilled" link type="primary" @click="doRefund(row)"> 详情 </el-button>
|
||||
<el-button v-if="false" icon="edit-pen" text type="primary"
|
||||
v-auth="'wallet_walletRecharge_edit'"
|
||||
@click="formDialogRef.openDialog(row.id)">编辑</el-button>
|
||||
<el-button v-if="false" icon="delete" text type="primary"
|
||||
v-auth="'wallet_walletRecharge_del'" @click="handleDelete([row.id])">删除</el-button>
|
||||
<div class="row-actions">
|
||||
<el-button icon="InfoFilled" link type="primary" @click="doRefund(row)"> 详情 </el-button>
|
||||
<el-button
|
||||
icon="EditPen"
|
||||
link
|
||||
type="primary"
|
||||
v-auth="'wallet_walletTransaction_edit'"
|
||||
@click="remarkFormRef?.openDialog(row)"
|
||||
>修改备注</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="Edit"
|
||||
link
|
||||
type="primary"
|
||||
v-auth="'wallet_walletTransaction_update_info'"
|
||||
@click="infoFormRef?.openDialog(row)"
|
||||
>修改信息</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
|
||||
v-bind="state.pagination" />
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</template>
|
||||
</DraggableDepTree>
|
||||
</div>
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
<transaction-remark-form ref="remarkFormRef" @refresh="getDataList(false)" />
|
||||
<transaction-info-form ref="infoFormRef" @refresh="getDataList(false)" />
|
||||
<el-drawer v-model="showDrawer" @closed="drawerClose">
|
||||
<el-table :data="[balanceStatistics]" style="width: 100%">
|
||||
<el-table-column align="center" label="充值统计">
|
||||
@@ -124,7 +157,7 @@
|
||||
import dayjs from "dayjs"
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import { pageXf, xfTotal } from "/@/api/wallet/wallet-transaction"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { useDict } from "/@/hooks/dict"
|
||||
import { displayPrice } from "/@/utils/formatMoney"
|
||||
import { deptTree } from "/@/api/admin/dept"
|
||||
@@ -133,17 +166,22 @@ const { push } = useRouter()
|
||||
|
||||
const showDrawer = ref(false)
|
||||
const balanceStatisticsLoading = ref(false)
|
||||
const balanceStatistics = reactive<any>({
|
||||
|
||||
})
|
||||
|
||||
const balanceStatistics = reactive<any>({})
|
||||
|
||||
// 引入组件
|
||||
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
|
||||
// 所有钱包流水列表共用的备注编辑表单。
|
||||
const TransactionRemarkForm = defineAsyncComponent(() => import("../components/transaction-remark-form.vue"))
|
||||
// 所有钱包流水列表共用的流水信息编辑表单(部门、创建时间)。
|
||||
const TransactionInfoForm = defineAsyncComponent(() => import("../components/transaction-info-form.vue"))
|
||||
// 定义查询字典
|
||||
const { wallet_type, wallet_biz } = useDict("wallet_type", "wallet_biz")
|
||||
// 定义变量内容
|
||||
const formDialogRef = ref()
|
||||
// 通用流水备注编辑弹窗引用。
|
||||
const remarkFormRef = ref()
|
||||
// 通用流水信息编辑弹窗引用。
|
||||
const infoFormRef = ref()
|
||||
// 搜索变量
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
@@ -158,7 +196,7 @@ const defaultCreateTimeList = [
|
||||
]
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {
|
||||
createTimeList: defaultCreateTimeList
|
||||
createTimeList: defaultCreateTimeList,
|
||||
},
|
||||
pageList: pageXf,
|
||||
})
|
||||
@@ -237,23 +275,6 @@ const getSummaries = (param: any) => {
|
||||
return totalRow
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (ids: string[]) => {
|
||||
try {
|
||||
await useMessageBox().confirm("此操作将永久删除")
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await delObjs(ids)
|
||||
getDataList()
|
||||
useMessage().success("删除成功")
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg)
|
||||
}
|
||||
}
|
||||
|
||||
// 树节点选择时
|
||||
const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepTree: any) => {
|
||||
state.queryForm.deptId = curClickedNode.id
|
||||
@@ -262,3 +283,22 @@ const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepT
|
||||
getDataList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 操作列按钮改为紧凑 flex 布局并收窄单元格内边距,避免按钮换行和两侧留白。 */
|
||||
.row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
|
||||
:deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.wallet-tx-actions-col .cell) {
|
||||
padding: 0 6px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user