Merge remote-tracking branch 'origin/master'
# Conflicts: # src/views/canteen/canteenSpu/form.vue
This commit is contained in:
commit
0e7050daeb
25 files changed
+1608
-122
No files matched your search
@@ -49,6 +49,14 @@ export const addObj = (obj: Object) => {
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取后端生成且当前未被占用的用户登录账号 */
|
||||
export const generateUsername = () => {
|
||||
return request({
|
||||
url: "/admin/user/generate-username",
|
||||
method: "get",
|
||||
})
|
||||
}
|
||||
|
||||
export const getObj = (id: String) => {
|
||||
return request({
|
||||
url: "/admin/user/details/" + id,
|
||||
|
||||
Executable
+98
@@ -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",
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<!-- excel 导入组件 -->
|
||||
<template>
|
||||
<el-dialog :title="prop.title" v-model="state.upload.open" :close-on-click-modal="false" draggable>
|
||||
<!-- 可选前置区域供业务组件放置导入类型等条件,不影响既有上传调用。 -->
|
||||
<slot name="before-upload" :uploading="state.upload.isUploading" />
|
||||
<el-upload v-loading="state.upload.isUploading" ref="uploadRef" :limit="1" accept=".xlsx, .xls"
|
||||
:headers="headers" :action="baseURL + other.adaptationUrl(url)" :disabled="state.upload.isUploading"
|
||||
:on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :on-error="handleFileError"
|
||||
|
||||
Vendored
+1
@@ -1,5 +1,6 @@
|
||||
// 申明外部 npm 插件模块
|
||||
declare module 'splitpanes';
|
||||
declare module 'codemirror';
|
||||
declare module 'js-cookie';
|
||||
declare module '@wangeditor/editor-for-vue';
|
||||
declare module 'qs';
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<!-- 用户导入组件:按所选用户类型同步切换导入接口和模板。 -->
|
||||
<template>
|
||||
<upload-excel ref="uploadExcelRef" title="用户导入" :temp-url="currentImportOption.tempUrl"
|
||||
:url="currentImportOption.url" @refreshDataList="handleRefreshDataList">
|
||||
<template #before-upload="{ uploading }">
|
||||
<el-form class="mb20" :model="form" label-width="90px">
|
||||
<el-form-item label="导入类型" prop="importType" required class="mb20">
|
||||
<el-select v-model="form.importType" placeholder="请选择导入类型" class="w100" :disabled="uploading">
|
||||
<el-option v-for="item in importOptions" :key="item.value" :label="item.label"
|
||||
:value="item.value" :disabled="!auth(item.permission)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</upload-excel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="user-import-excel">
|
||||
import UploadExcel from "/@/components/Upload/Excel.vue"
|
||||
import { auth } from "/@/utils/authFunction"
|
||||
|
||||
type UserImportType = "backend" | "student"
|
||||
|
||||
interface UserImportOption {
|
||||
value: UserImportType
|
||||
label: string
|
||||
url: string
|
||||
tempUrl: string
|
||||
permission: string
|
||||
}
|
||||
|
||||
const emit = defineEmits(["refreshDataList"])
|
||||
|
||||
// 接口和模板保持在同一份配置中,避免切换导入类型时二者不一致。
|
||||
const importOptions: UserImportOption[] = [
|
||||
{
|
||||
value: "backend",
|
||||
label: "后台用户",
|
||||
url: "/admin/user/import-face",
|
||||
tempUrl: "/admin/sys-file/local/file/user.xlsx",
|
||||
permission: "sys_user_export",
|
||||
},
|
||||
{
|
||||
value: "student",
|
||||
label: "学生用户",
|
||||
url: "/admin/user/import-student",
|
||||
tempUrl: "/admin/sys-file/local/file/student_user_import.xlsx",
|
||||
permission: "sys_student_user_export",
|
||||
},
|
||||
]
|
||||
|
||||
const uploadExcelRef = ref()
|
||||
const form = reactive({
|
||||
importType: "backend" as UserImportType,
|
||||
})
|
||||
|
||||
const currentImportOption = computed(() => importOptions.find((item) => item.value === form.importType) ?? importOptions[0])
|
||||
|
||||
const handleRefreshDataList = () => {
|
||||
emit("refreshDataList")
|
||||
}
|
||||
|
||||
/** 每次打开优先恢复为后台用户导入;仅有学生导入权限时自动选中学生用户。 */
|
||||
const show = () => {
|
||||
form.importType = auth(importOptions[0].permission) ? "backend" : "student"
|
||||
uploadExcelRef.value?.show()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
</script>
|
||||
@@ -4,7 +4,16 @@
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item :label="$t('sysuser.username')" prop="username">
|
||||
<el-input v-model.trim="model.username" :disabled="isEdit && !auth('user_sysUser_editUsername')"
|
||||
placeholder="请输入用户名" />
|
||||
placeholder="请输入用户名">
|
||||
<template #suffix>
|
||||
<el-tooltip v-if="!isEdit && auth('sys_user_add')" content="自动生成用户名" placement="top">
|
||||
<el-icon class="cursor-pointer text-primary" @click.stop="handleGenerateUsername">
|
||||
<ele-Loading v-if="generateUsernameLoading" class="animate-spin" />
|
||||
<ele-MagicStick v-else />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" class="mb20">
|
||||
@@ -125,8 +134,8 @@
|
||||
</el-col>
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="学校名称" prop="studentInfo.schoolName">
|
||||
<el-input v-model.trim="model.studentInfo.schoolName" clearable
|
||||
placeholder="请输入学校名称" />
|
||||
<dict-select v-model="model.studentInfo.schoolName"
|
||||
dict-code="user_student_school" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" class="mb20">
|
||||
@@ -145,7 +154,7 @@
|
||||
|
||||
<script setup lang="ts" name="UserInfoForm">
|
||||
import type { FormInstance, FormRules } from "element-plus"
|
||||
import { validatePhone, validateUsername } from "/@/api/admin/user"
|
||||
import { generateUsername, validatePhone, validateUsername } from "/@/api/admin/user"
|
||||
import { deptTree } from "/@/api/admin/dept"
|
||||
import { list as roleList } from "/@/api/admin/role"
|
||||
import { list as postList } from "/@/api/admin/post"
|
||||
@@ -176,6 +185,7 @@ const { lock_flag } = useDict("lock_flag")
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const optionsLoading = ref(false)
|
||||
const generateUsernameLoading = ref(false)
|
||||
const deptData = ref<any[]>([])
|
||||
const roleData = ref<any[]>([])
|
||||
const postData = ref<any[]>([])
|
||||
@@ -205,6 +215,22 @@ const validatePhoneUnique = (currentRule: any, value: string, callback: (error?:
|
||||
validatePhone(currentRule, value, callback, isEdit.value)
|
||||
}
|
||||
|
||||
// 账号由后端统一生成并确认未被占用,避免前端随机值与现有用户发生冲突。
|
||||
const handleGenerateUsername = async () => {
|
||||
if (isEdit.value || generateUsernameLoading.value) return
|
||||
generateUsernameLoading.value = true
|
||||
try {
|
||||
const { data } = await generateUsername()
|
||||
model.value.username = data
|
||||
await nextTick()
|
||||
formRef.value?.clearValidate("username")
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "自动生成用户名失败")
|
||||
} finally {
|
||||
generateUsernameLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formRules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: "用户名不能为空", trigger: "blur" },
|
||||
@@ -239,6 +265,7 @@ const formRules: FormRules = {
|
||||
{ required: true, message: "学号不能为空", trigger: "blur" },
|
||||
{ validator: rule.overLength, trigger: "blur" },
|
||||
],
|
||||
"studentInfo.schoolName": [{ required: true, message: "学校名称不能为空", trigger: "blur" }],
|
||||
}
|
||||
|
||||
watch(
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
<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 plain v-auths="['sys_user_export', 'sys_student_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"
|
||||
@@ -181,8 +181,7 @@
|
||||
|
||||
<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" />
|
||||
<user-import-excel ref="excelUploadRef" @refreshDataList="getDataList" />
|
||||
<!-- 审批抽屉 -->
|
||||
<process-drawer ref="processDrawerRef" @task-submit-event="taskSubmitEvent" />
|
||||
<!-- 批量审批对话框 -->
|
||||
@@ -207,6 +206,7 @@ const FaceManage = defineAsyncComponent(() => import("./face-manage.vue"))
|
||||
const PopoverInput = defineAsyncComponent(() => import("/@/components/PopoverInput/index.vue"))
|
||||
const BatchDeal = defineAsyncComponent(() => import("./batchDeal.vue"))
|
||||
const TagTable = defineAsyncComponent(() => import("./tag-table.vue"))
|
||||
const UserImportExcel = defineAsyncComponent(() => import("./components/user-import-excel.vue"))
|
||||
|
||||
const { t } = useI18n()
|
||||
const { push } = useRouter()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-dialog :close-on-click-modal="false" :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" draggable v-model="visible">
|
||||
<el-dialog :close-on-click-modal="false" :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" draggable
|
||||
v-model="visible">
|
||||
<el-form :model="form" :rules="dataRules" label-width="90px" ref="dataFormRef" v-loading="loading">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12" class="mb20">
|
||||
@@ -11,7 +12,8 @@
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item :label="t('channel.channelId')" prop="state">
|
||||
<el-select v-model="form.channelId">
|
||||
<el-option :key="item.value" :label="item.label" :value="item.value" v-for="item in channel_type"> </el-option>
|
||||
<el-option :key="item.value" :label="item.label" :value="item.value"
|
||||
v-for="item in channel_type"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -31,7 +33,8 @@
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item :label="t('channel.state')" prop="state">
|
||||
<el-select v-model="form.state">
|
||||
<el-option :key="item.value" :label="item.label" :value="item.value" v-for="item in status_type"> </el-option>
|
||||
<el-option :key="item.value" :label="item.label" :value="item.value"
|
||||
v-for="item in status_type"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -50,13 +53,15 @@
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item :label="t('channel.remark')" prop="remark">
|
||||
<el-input :placeholder="t('channel.inputRemarkTip')" :rows="2" type="textarea" v-model="form.remark" />
|
||||
<el-input :placeholder="t('channel.inputRemarkTip')" :rows="2" type="textarea"
|
||||
v-model="form.remark" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item :label="t('channel.param')" prop="param">
|
||||
<el-input :placeholder="t('channel.inputParamTip')" :rows="4" type="textarea" v-model="form.param" />
|
||||
<el-input :placeholder="t('channel.inputParamTip')" :rows="4" type="textarea"
|
||||
v-model="form.param" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -64,7 +69,8 @@
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
|
||||
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
|
||||
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText')
|
||||
}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -105,24 +111,24 @@ const form = reactive({
|
||||
// 定义校验规则
|
||||
const dataRules = ref({
|
||||
appId: [
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
{ required: true, message: 'appId不能为空', trigger: 'blur' }
|
||||
],
|
||||
channelName: [{ required: true, message: '渠道名称不能为空', trigger: 'blur' },{ validator: rule.overLength, trigger: 'blur' }],
|
||||
channelName: [{ required: true, message: '渠道名称不能为空', trigger: 'blur' }, { validator: rule.overLength, trigger: 'blur' }],
|
||||
|
||||
channelMchId: [
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
{ required: true, message: '商户ID不能为空', trigger: 'blur' }
|
||||
],
|
||||
state: [{ required: true, message: '状态不能为空', trigger: 'blur' },{ validator: rule.overLength, trigger: 'blur' }],
|
||||
state: [{ required: true, message: '状态不能为空', trigger: 'blur' }, { validator: rule.overLength, trigger: 'blur' }],
|
||||
|
||||
remark:[{ validator: rule.overLength, trigger: 'blur' }],
|
||||
remark: [{ validator: rule.overLength, trigger: 'blur' }],
|
||||
returnUrl: [
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
{ validater: rule.url, trigger: 'blur' },
|
||||
],
|
||||
|
||||
notifyUrl: [{ required: true, message: '后端回调不能为空', trigger: 'blur' },{ validator: rule.overLength, trigger: 'blur' }],
|
||||
notifyUrl: [{ required: true, message: '后端回调不能为空', trigger: 'blur' }, { validator: rule.overLength, trigger: 'blur' }],
|
||||
param: [{ required: true, message: '参数配置不能为空', trigger: 'blur' }],
|
||||
});
|
||||
|
||||
@@ -145,20 +151,20 @@ 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;
|
||||
// 清除占位符,避免提交错误的数据
|
||||
const { appId, channelMchId } = form;
|
||||
// 清除占位符,避免提交错误的数据
|
||||
const { appId, channelMchId } = form;
|
||||
|
||||
if (appId?.includes('**')) {
|
||||
form.appId = undefined;
|
||||
}
|
||||
if (channelMchId?.includes('**')) {
|
||||
form.channelMchId = undefined;
|
||||
}
|
||||
if (appId?.includes('**')) {
|
||||
form.appId = undefined;
|
||||
}
|
||||
if (channelMchId?.includes('**')) {
|
||||
form.channelMchId = undefined;
|
||||
}
|
||||
form.id ? await putObj(form) : await addObj(form);
|
||||
useMessage().success(t(form.id ? 'common.editSuccessText' : 'common.addSuccessText'));
|
||||
visible.value = false;
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px"
|
||||
v-loading="loading">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="14" class="mb20">
|
||||
<el-form-item label="商户" prop="merchantId" class="w-60 my-6">
|
||||
<select-mch v-model="form.merchantId" autoFirst :apiParams="{ types: '1', }" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="14" class="mb20">
|
||||
<el-form-item label="分类名称" prop="name">
|
||||
<el-input v-model.trim="form.name" placeholder="请输入分类名称" />
|
||||
@@ -71,11 +76,13 @@ const form = reactive({
|
||||
updater: "",
|
||||
deleted: "",
|
||||
deptId: "",
|
||||
merchantId: "",
|
||||
categoryType: [],
|
||||
})
|
||||
|
||||
// 定义校验规则
|
||||
const dataRules = ref({
|
||||
merchantId: [{ required: true, message: "请选择商户", trigger: "change" }],
|
||||
name: [{ required: true, message: "分类名称不能为空", trigger: "blur" }],
|
||||
categoryType: [{ required: true, message: "餐段类型不能为空", trigger: "blur" }],
|
||||
sort: [{ required: true, message: "分类排序不能为空", trigger: "blur" }],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :close-on-click-modal="false" draggable width="820px">
|
||||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="140px" v-loading="loading">
|
||||
<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="140px"
|
||||
v-loading="loading">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="14" class="mb20">
|
||||
<el-form-item label="商品类型" prop="spuType">
|
||||
@@ -10,6 +11,11 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="14" class="mb20">
|
||||
<el-form-item label="商户" prop="merchantId">
|
||||
<select-mch v-model="form.merchantId" autoFirst :apiParams="{ types: '1', }" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="14" class="mb20">
|
||||
<el-form-item label="菜品名称" prop="name">
|
||||
<el-input v-model.trim="form.name" placeholder="请输入菜品名称" />
|
||||
@@ -52,9 +58,10 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="是否在移动端显示" prop="isShow">
|
||||
<el-form-item label="是否在移动端显示" prop="status">
|
||||
<el-radio-group v-model="form.isShow">
|
||||
<el-radio :label="item.value" v-for="(item, index) in is_show" border :key="index">{{ item.label }} </el-radio>
|
||||
<el-radio :label="item.value" v-for="(item, index) in is_show" border :key="index">{{
|
||||
item.label }} </el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -299,12 +306,15 @@ const openDialog = async (id: string) => {
|
||||
form.spuType = 1
|
||||
form.comboItems = []
|
||||
dataFormRef.value?.resetFields()
|
||||
|
||||
// 获取canteenSpu信息
|
||||
if (id) {
|
||||
form.id = id
|
||||
getcanteenSpuData(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交
|
||||
const onSubmit = async () => {
|
||||
const valid = await dataFormRef?.value.validate().catch(() => {})
|
||||
if (!valid) return false
|
||||
@@ -336,7 +346,9 @@ const onSubmit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化表单数据
|
||||
const getcanteenSpuData = (id: string) => {
|
||||
// 获取数据
|
||||
loading.value = true
|
||||
getObj(id)
|
||||
.then((res: any) => {
|
||||
@@ -360,13 +372,12 @@ const getcanteenSpuData = (id: string) => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const categoryList = ref<any>([])
|
||||
const categoryList = ref<any>([]) // 菜品分类树
|
||||
onMounted(async () => {
|
||||
const data = await getAllSimple()
|
||||
categoryList.value = data.data
|
||||
})
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openDialog,
|
||||
})
|
||||
|
||||
@@ -109,8 +109,11 @@ const selectionChangHandle = (objs: { id: string }[]) => {
|
||||
multiple.value = !objs.length
|
||||
}
|
||||
|
||||
const openDialog = (createTimeStr: any, hourType: any) => {
|
||||
const openDialog = (createTimeStr: any, hourType: any, queryParams: any) => {
|
||||
visible.value = true
|
||||
state.queryForm = {
|
||||
...queryParams,
|
||||
}
|
||||
state.queryForm.orderApptUseDateStr = createTimeStr
|
||||
state.queryForm.hourType = hourType
|
||||
getDataList()
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<el-table-column fixed="right" label="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button icon="Tickets" text type="primary"
|
||||
@click="productTotalTableRef.openDialog(parseTime(row.createTime, '{y}-{m}-{d}'), row.hourType)">
|
||||
@click="productTotalTableRef.openDialog(parseTime(row.createTime, '{y}-{m}-{d}'), row.hourType, state.queryForm)">
|
||||
菜品详情
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
@@ -109,8 +109,11 @@ const selectionChangHandle = (objs: { id: string }[]) => {
|
||||
multiple.value = !objs.length
|
||||
}
|
||||
|
||||
const openDialog = (createTimeStr: any, hourType: any) => {
|
||||
const openDialog = (createTimeStr: any, hourType: any, queryParams: any) => {
|
||||
visible.value = true
|
||||
state.queryForm = {
|
||||
...queryParams,
|
||||
}
|
||||
state.queryForm.createTimeStr = createTimeStr
|
||||
state.queryForm.hourType = hourType
|
||||
getDataList()
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<el-table-column fixed="right" label="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button icon="Tickets" text type="primary"
|
||||
@click="productTotalTableRef.openDialog(parseTime(row.createTime, '{y}-{m}-{d}'), row.hourType)">
|
||||
@click="productTotalTableRef.openDialog(parseTime(row.createTime, '{y}-{m}-{d}'), row.hourType, state.queryForm)">
|
||||
菜品详情
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<el-form-item label="起始日" prop="targetStartOffsetDays">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="whitespace-nowrap text-sm text-gray-500">截止后第</span>
|
||||
<el-input-number v-model="form.targetStartOffsetDays" :min="1" :max="31"
|
||||
<el-input-number v-model="form.targetStartOffsetDays" :min="0" :max="31"
|
||||
controls-position="right" />
|
||||
<span class="whitespace-nowrap text-sm text-gray-500">天</span>
|
||||
</div>
|
||||
@@ -263,8 +263,9 @@ const previewBatch = computed(() => {
|
||||
return windowForMonday(monday.add(1, "week"))
|
||||
})
|
||||
|
||||
// 第 0 天表示截止日期当天,不能用真值回退,否则 0 会被误算成第 1 天。
|
||||
const targetPreviewStart = computed(() =>
|
||||
previewBatch.value?.close.startOf("day").add(Number(form.targetStartOffsetDays) || 1, "day"),
|
||||
previewBatch.value?.close.startOf("day").add(Number(form.targetStartOffsetDays ?? 0), "day"),
|
||||
)
|
||||
|
||||
const batchPreview = computed(() => {
|
||||
|
||||
@@ -49,6 +49,43 @@
|
||||
<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 width="100" align="center" label="在线状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="isTerminalStatusSupported(row)" :type="row.online ? 'success' : 'danger'"
|
||||
size="small" effect="light">
|
||||
{{ row.online ? "在线" : "离线" }}
|
||||
</el-tag>
|
||||
<span v-else class="text-xs text-[var(--el-text-color-secondary)]">暂不支持</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="150" label="APK 版本">
|
||||
<template #default="{ row }">
|
||||
<div v-if="isTerminalStatusSupported(row) && (row.apkVersionName || row.apkVersionCode)"
|
||||
class="flex flex-col leading-5">
|
||||
<span>{{ row.apkVersionName || "--" }}</span>
|
||||
<span v-if="row.apkVersionCode" class="text-xs text-[var(--el-text-color-secondary)]">
|
||||
Build {{ row.apkVersionCode }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="text-[var(--el-text-color-secondary)]">
|
||||
{{ isTerminalStatusSupported(row) ? "尚未上报" : "暂不支持" }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="180" label="最后在线时间">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-[var(--el-text-color-secondary)]': !row.lastOnlineTime }">
|
||||
{{ getLastOnlineTimeText(row) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="230" label="可能断线原因" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-[var(--el-text-color-secondary)]': !row.disconnectReason }">
|
||||
{{ getDisconnectReasonText(row) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="mainTopic" width="350" label="topic" />
|
||||
<el-table-column prop="isTakeFood" width="200" label="是否为当前商户的取餐机器">
|
||||
<template #default="{ row }">
|
||||
@@ -119,6 +156,40 @@ const showSearch = ref(true)
|
||||
const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
|
||||
/** 支持终端心跳与 APK 状态上报的设备类型编码,仅包含超市和食堂 Android 终端。 */
|
||||
const terminalStatusSupportedTypes = ["1", "2"]
|
||||
|
||||
/**
|
||||
* 判断当前设备是否支持在线与版本状态展示。
|
||||
*
|
||||
* @param row 设备分页行,类型编码来源于 hard_manager.type
|
||||
* @returns 超市或食堂终端返回 true,其他硬件返回 false
|
||||
*/
|
||||
const isTerminalStatusSupported = (row: any) => terminalStatusSupportedTypes.includes(String(row.type))
|
||||
|
||||
/**
|
||||
* 获取最后在线时间列文案,明确区分未上报和不支持的设备。
|
||||
*
|
||||
* @param row 设备分页行
|
||||
* @returns 服务端心跳时间或对应的空状态文案
|
||||
*/
|
||||
const getLastOnlineTimeText = (row: any) => {
|
||||
if (!isTerminalStatusSupported(row)) return "暂不支持"
|
||||
return row.lastOnlineTime || "尚未上报"
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可能断线原因。在线设备没有计划内原因时使用短横线,避免误导为故障。
|
||||
*
|
||||
* @param row 设备分页行
|
||||
* @returns 服务端推断原因或对应的空状态文案
|
||||
*/
|
||||
const getDisconnectReasonText = (row: any) => {
|
||||
if (!isTerminalStatusSupported(row)) return "暂不支持"
|
||||
if (row.disconnectReason) return row.disconnectReason
|
||||
return row.online ? "--" : "尚未收到终端心跳"
|
||||
}
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {},
|
||||
pageList: hardManageApi.pageJoin,
|
||||
|
||||
@@ -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>
|
||||
+229
@@ -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>
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<el-dialog title="编辑定餐配置" v-model="visible" :close-on-click-modal="false" draggable>
|
||||
<el-form ref="dataFormRef" :model="form" label-width="180px" v-loading="loading">
|
||||
<el-form ref="dataFormRef" :model="form" label-width="250px" v-loading="loading">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="预约模式" prop="mode">
|
||||
<dict-select v-model="form.mode" dict-code="mch_table_reservation_mode" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="预约时间模式" prop="reserveMode">
|
||||
<dict-select v-model="form.reserveMode" dict-code="hard_appt_reserve_mode" />
|
||||
@@ -17,9 +22,16 @@
|
||||
inactive-text="否" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="同一人同一天同一餐段仅允许预约一次" prop="singleReservationOnlyPerMeal">
|
||||
<el-switch v-model="form.singleReservationOnlyPerMeal" inline-prompt active-text="是"
|
||||
inactive-text="否" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="代订餐用户关系" prop="userRelationTypeIdList">
|
||||
<dict-select v-model="form.userRelationTypeIdList" dict-code="sys_user_relation_type" multiple />
|
||||
<dict-select v-model="form.userRelationTypeIdList" dict-code="sys_user_relation_type"
|
||||
multiple />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -45,15 +57,19 @@ const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const createDefaultTableReservationConf = () => ({
|
||||
mode: "10",
|
||||
reserveMode: RESERVE_MODE.TIME_WINDOW,
|
||||
singleProductOnlyInCart: false,
|
||||
singleReservationOnlyPerMeal: false,
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
id: "",
|
||||
mode: "10",
|
||||
tableReservationConf: createDefaultTableReservationConf(),
|
||||
reserveMode: RESERVE_MODE.TIME_WINDOW,
|
||||
singleProductOnlyInCart: false,
|
||||
singleReservationOnlyPerMeal: false,
|
||||
userRelationTypeIdList: [],
|
||||
})
|
||||
|
||||
@@ -62,8 +78,10 @@ const openDialog = (mchId: string) => {
|
||||
visible.value = true
|
||||
form.id = mchId
|
||||
form.tableReservationConf = createDefaultTableReservationConf()
|
||||
form.mode = "10"
|
||||
form.reserveMode = RESERVE_MODE.TIME_WINDOW
|
||||
form.singleProductOnlyInCart = false
|
||||
form.singleReservationOnlyPerMeal = false
|
||||
form.userRelationTypeIdList = []
|
||||
nextTick(() => {
|
||||
dataFormRef.value?.resetFields?.()
|
||||
@@ -90,8 +108,10 @@ const onSubmit = async () => {
|
||||
tableReservationConf: {
|
||||
...createDefaultTableReservationConf(),
|
||||
...(data?.tableReservationConf || {}),
|
||||
mode: form.mode,
|
||||
reserveMode: normalizeReserveMode(form.reserveMode),
|
||||
singleProductOnlyInCart: form.singleProductOnlyInCart,
|
||||
singleReservationOnlyPerMeal: form.singleReservationOnlyPerMeal,
|
||||
userRelationTypeIdList: form.userRelationTypeIdList,
|
||||
}
|
||||
}
|
||||
@@ -125,7 +145,9 @@ const getTableReservationConfData = async (mchId: string) => {
|
||||
form.tableReservationConf = tableReservationConf
|
||||
form.reserveMode = tableReservationConf.reserveMode
|
||||
form.singleProductOnlyInCart = tableReservationConf.singleProductOnlyInCart
|
||||
form.singleReservationOnlyPerMeal = tableReservationConf.singleReservationOnlyPerMeal
|
||||
form.userRelationTypeIdList = tableReservationConf.userRelationTypeIdList || []
|
||||
form.mode = tableReservationConf.mode || "10"
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || error?.message || "获取定餐配置失败")
|
||||
} finally {
|
||||
|
||||
@@ -98,6 +98,11 @@
|
||||
</div>
|
||||
<div class="take-card__placeholder">
|
||||
<div class="take-info" v-loading="state.tableReservationConfLoading">
|
||||
<div class="take-info-item">
|
||||
<span class="take-info-item__label">预约模式</span>
|
||||
<dict-tag class="take-info-item__value" :options="mch_table_reservation_mode"
|
||||
:value="state.tableReservationConf.mode" />
|
||||
</div>
|
||||
<div class="take-info-item">
|
||||
<span class="take-info-item__label">预约时间模式</span>
|
||||
<dict-tag class="take-info-item__value" :options="hard_appt_reserve_mode"
|
||||
@@ -109,6 +114,12 @@
|
||||
{{ state.tableReservationConf.singleProductOnlyInCart ? "是" : "否" }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="take-info-item">
|
||||
<span class="take-info-item__label">同一人同一天同一餐段仅允许预约一次</span>
|
||||
<span class="take-info-item__value">
|
||||
{{ state.tableReservationConf.singleReservationOnlyPerMeal ? "是" : "否" }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="take-info-item">
|
||||
<span class="take-info-item__label">代订餐用户关系</span>
|
||||
<span class="take-info-item__value">
|
||||
@@ -123,84 +134,84 @@
|
||||
<div class="take-card__placeholder" style="margin-top: 16px;">
|
||||
<template v-if="currentReserveMode === RESERVE_MODE.TIME_WINDOW">
|
||||
<div class="mb-4 text-base font-medium text-gray-700">动态时间窗餐段规则</div>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10"
|
||||
@click="formDialogRef.openDialog(null, state.mchDetail.deptId, state.mchId)"
|
||||
v-auth="'hard_hardAppt_add'">
|
||||
新 增
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10"
|
||||
@click="formDialogRef.openDialog(null, state.mchDetail.deptId, state.mchId)"
|
||||
v-auth="'hard_hardAppt_add'">
|
||||
新 增
|
||||
</el-button>
|
||||
<el-button plain icon="Delete" type="primary" v-auth="'hard_hardAppt_del'"
|
||||
@click="handleDelete(selectObjs)">
|
||||
删除
|
||||
</el-button>
|
||||
<right-toolbar class="ml10 mr20" style="float: right" @queryTable="getDataList" />
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="apptState.dataList" v-loading="apptState.loading" border
|
||||
:cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table-column type="index" label="#" width="40" />
|
||||
<el-table-column prop="title" label="标题" />
|
||||
<el-table-column prop="title" label="类型">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="daily_menu" :value="row.hourType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预约模式" width="130">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="hard_appt_reserve_mode"
|
||||
:value="normalizeReserveMode(row.reserveMode)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可预约时间" width="300">
|
||||
<template #default="{ row }">
|
||||
<span v-if="normalizeReserveMode(row.reserveMode) === RESERVE_MODE.WEEKLY_CYCLE">
|
||||
由周期预约规则配置
|
||||
</span>
|
||||
<div v-else class="flex flex-wrap">
|
||||
<dict-tag style="margin-right: 5px" v-for="value in row.reserveTimeList"
|
||||
:key="value" :options="hard_appt_reserve_date_type" :value="value" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时间段" width="300">
|
||||
<template #default="{ row }">
|
||||
<span v-if="normalizeReserveMode(row.reserveMode) === RESERVE_MODE.WEEKLY_CYCLE">
|
||||
由周期预约规则配置
|
||||
</span>
|
||||
<div v-else class="take-time-range">
|
||||
<div class="take-time-range__item">
|
||||
<dict-tag :options="hard_appt_time_type" :value="row.startTimeType" />
|
||||
<span class="take-time-range__time">{{ row.startTime }}</span>
|
||||
</div>
|
||||
<span class="take-time-range__sep">—</span>
|
||||
<div class="take-time-range__item">
|
||||
<dict-tag :options="hard_appt_time_type" :value="row.endTimeType" />
|
||||
<span class="take-time-range__time">{{ row.endTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="yes_no_type" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" />
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button icon="edit-pen" text type="primary" v-auth="'hard_hardAppt_edit'"
|
||||
@click="formDialogRef.openDialog(scope.row.id, state.mchDetail.deptId, state.mchId)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button plain icon="Delete" type="primary" v-auth="'hard_hardAppt_del'"
|
||||
@click="handleDelete(selectObjs)">
|
||||
<el-button icon="delete" text type="primary" v-auth="'hard_hardAppt_del'"
|
||||
@click="handleDelete([scope.row.id])">
|
||||
删除
|
||||
</el-button>
|
||||
<right-toolbar class="ml10 mr20" style="float: right" @queryTable="getDataList" />
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table :data="apptState.dataList" v-loading="apptState.loading" border
|
||||
:cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
|
||||
@sort-change="sortChangeHandle">
|
||||
<el-table-column type="index" label="#" width="40" />
|
||||
<el-table-column prop="title" label="标题" />
|
||||
<el-table-column prop="title" label="类型">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="daily_menu" :value="row.hourType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预约模式" width="130">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="hard_appt_reserve_mode"
|
||||
:value="normalizeReserveMode(row.reserveMode)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可预约时间" width="300">
|
||||
<template #default="{ row }">
|
||||
<span v-if="normalizeReserveMode(row.reserveMode) === RESERVE_MODE.WEEKLY_CYCLE">
|
||||
由周期预约规则配置
|
||||
</span>
|
||||
<div v-else class="flex flex-wrap">
|
||||
<dict-tag style="margin-right: 5px" v-for="value in row.reserveTimeList"
|
||||
:key="value" :options="hard_appt_reserve_date_type" :value="value" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时间段" width="300">
|
||||
<template #default="{ row }">
|
||||
<span v-if="normalizeReserveMode(row.reserveMode) === RESERVE_MODE.WEEKLY_CYCLE">
|
||||
由周期预约规则配置
|
||||
</span>
|
||||
<div v-else class="take-time-range">
|
||||
<div class="take-time-range__item">
|
||||
<dict-tag :options="hard_appt_time_type" :value="row.startTimeType" />
|
||||
<span class="take-time-range__time">{{ row.startTime }}</span>
|
||||
</div>
|
||||
<span class="take-time-range__sep">—</span>
|
||||
<div class="take-time-range__item">
|
||||
<dict-tag :options="hard_appt_time_type" :value="row.endTimeType" />
|
||||
<span class="take-time-range__time">{{ row.endTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="yes_no_type" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" />
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button icon="edit-pen" text type="primary" v-auth="'hard_hardAppt_edit'"
|
||||
@click="formDialogRef.openDialog(scope.row.id, state.mchDetail.deptId, state.mchId)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button icon="delete" text type="primary" v-auth="'hard_hardAppt_del'"
|
||||
@click="handleDelete([scope.row.id])">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<template v-else>
|
||||
<cycle-rule-list :mch-id="props.mchId" :dept-id="state.mchDetail.deptId" />
|
||||
@@ -279,6 +290,7 @@ const createDefaultFunctionScope = () => ({
|
||||
const createDefaultTableReservationConf = () => ({
|
||||
reserveMode: RESERVE_MODE.TIME_WINDOW,
|
||||
singleProductOnlyInCart: false,
|
||||
singleReservationOnlyPerMeal: false,
|
||||
})
|
||||
|
||||
const takeOutFormRef = ref()
|
||||
@@ -311,6 +323,7 @@ const {
|
||||
hard_appt_reserve_date_type,
|
||||
hard_appt_reserve_mode,
|
||||
sys_user_relation_type,
|
||||
mch_table_reservation_mode,
|
||||
} = useDict(
|
||||
'yes_no_type',
|
||||
'daily_menu',
|
||||
@@ -318,6 +331,7 @@ const {
|
||||
'hard_appt_reserve_date_type',
|
||||
'hard_appt_reserve_mode',
|
||||
'sys_user_relation_type',
|
||||
'mch_table_reservation_mode',
|
||||
)
|
||||
const currentReserveMode = computed(() => normalizeReserveMode(state.tableReservationConf.reserveMode))
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ const dataRules = ref({
|
||||
validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||
const length = (value ?? "").trim().length
|
||||
if (length === 0) return callback()
|
||||
if (length === 10 || length === 12) return callback()
|
||||
callback(new Error("卡号长度必须为10位或12位"))
|
||||
if (length === 8 || length === 10 || length === 12) return callback()
|
||||
callback(new Error("卡号长度必须为8、10、12位"))
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user