diff --git a/.gitignore b/.gitignore index 3efc9c52..0b05e6a6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,3 @@ dist-ssr /dist* *-lock.* pnpm-debug - -.idea -.history \ No newline at end of file diff --git a/src/views/bpm/form/formEditor.vue b/src/views/bpm/form/formEditor.vue index 989ea56e..418a75e8 100644 --- a/src/views/bpm/form/formEditor.vue +++ b/src/views/bpm/form/formEditor.vue @@ -133,15 +133,24 @@ const openModel = (title: string) => { } /** 复制 **/ const copy = async (text: string) => { - const { copy, copied, isSupported } = useClipboard({ source: text }) - if (!isSupported) { - message.error(t('common.copyError')) - } else { - await copy() - if (unref(copied)) { - message.success(t('common.copySuccess')) - } - } + // const { copy, copied, isSupported } = useClipboard({ source: JSON.stringify(text) }) + // if (!isSupported.value) { + // message.error(t('common.copyError')) + // } else { + // await copy() + // if (unref(copied.value)) { + // message.success(t('common.copySuccess')) + // } + // } + let url = JSON.stringify(text) + let oInput = document.createElement('textarea') + oInput.value = url + document.body.appendChild(oInput) + oInput.select() // 选择对象; + // console.log(oInput.value) + document.execCommand('Copy') // 执行浏览器复制命令 + message.success(t('common.copySuccess')) + oInput.remove() } // ========== 初始化 ========== onMounted(() => { diff --git a/src/views/infra/build/index.vue b/src/views/infra/build/index.vue index 6f577e95..b3af638b 100644 --- a/src/views/infra/build/index.vue +++ b/src/views/infra/build/index.vue @@ -77,15 +77,24 @@ const showTemplate = () => { /** 复制 **/ const copy = async (text: string) => { - const { copy, copied, isSupported } = useClipboard({ source: text }) - if (!isSupported) { - message.error(t('common.copyError')) - } else { - await copy() - if (unref(copied)) { - message.success(t('common.copySuccess')) - } - } + // const { copy, copied, isSupported } = useClipboard({ source: JSON.stringify(text) }) + // if (!isSupported.value) { + // message.error(t('common.copyError')) + // } else { + // await copy() + // if (unref(copied.value)) { + // message.success(t('common.copySuccess')) + // } + // } + let url = JSON.stringify(text) + let oInput = document.createElement('textarea') + oInput.value = url + document.body.appendChild(oInput) + oInput.select() // 选择对象; + // console.log(oInput.value) + document.execCommand('Copy') // 执行浏览器复制命令 + message.success(t('common.copySuccess')) + oInput.remove() } const makeTemplate = () => { diff --git a/src/views/infra/codegen/components/Preview.vue b/src/views/infra/codegen/components/Preview.vue index 2d9482ff..6a6246a8 100644 --- a/src/views/infra/codegen/components/Preview.vue +++ b/src/views/infra/codegen/components/Preview.vue @@ -130,11 +130,11 @@ const handleFiles = (datas: CodegenPreviewVO[]) => { /** 复制 **/ const copy = async (text: string) => { const { copy, copied, isSupported } = useClipboard({ source: text }) - if (!isSupported) { + if (!isSupported.value) { message.error(t('common.copyError')) } else { await copy() - if (unref(copied)) { + if (unref(copied.value)) { message.success(t('common.copySuccess')) } } diff --git a/src/views/infra/config/config.data.ts b/src/views/infra/config/config.data.ts index 41acfa15..635745a5 100644 --- a/src/views/infra/config/config.data.ts +++ b/src/views/infra/config/config.data.ts @@ -6,7 +6,8 @@ export const rules = reactive({ category: [required], name: [required], key: [required], - value: [required] + value: [required], + visible: [{ required: true, message: '请选择是否可见', trigger: 'change' }] }) // CrudSchema diff --git a/src/views/infra/config/index.vue b/src/views/infra/config/index.vue index b2bc8a8b..ddf9eeba 100644 --- a/src/views/infra/config/index.vue +++ b/src/views/infra/config/index.vue @@ -93,8 +93,8 @@ const message = useMessage() // 消息弹窗 // 列表相关的变量 const [registerTable, { reload, deleteData, exportList }] = useXTable({ allSchemas: allSchemas, - getListApi: ConfigApi.getConfigPageApi, - deleteApi: ConfigApi.deleteConfigApi, + getListApi: ConfigApi.getConfigPage, + deleteApi: ConfigApi.deleteConfig, exportListApi: ConfigApi.exportConfigApi }) @@ -127,14 +127,6 @@ const handleCreate = async () => { }, 2 ) - unref(formRef)?.addSchema( - { - field: 'value', - label: '参数键值', - component: 'Input' - }, - 3 - ) } } @@ -142,17 +134,15 @@ const handleCreate = async () => { const handleUpdate = async (rowId: number) => { setDialogTile('update') // 设置数据 - const res = await ConfigApi.getConfigApi(rowId) + const res = await ConfigApi.getConfig(rowId) unref(formRef)?.delSchema('key') - unref(formRef)?.delSchema('value') - unref(formRef)?.setValues(res) } // 详情操作 const handleDetail = async (rowId: number) => { setDialogTile('detail') - const res = await ConfigApi.getConfigApi(rowId) + const res = await ConfigApi.getConfig(rowId) detailData.value = res } @@ -167,10 +157,10 @@ const submitForm = async () => { try { const data = unref(formRef)?.formModel as ConfigApi.ConfigVO if (actionType.value === 'create') { - await ConfigApi.createConfigApi(data) + await ConfigApi.createConfig(data) message.success(t('common.createSuccess')) } else { - await ConfigApi.updateConfigApi(data) + await ConfigApi.updateConfig(data) message.success(t('common.updateSuccess')) } dialogVisible.value = false diff --git a/src/views/infra/fileList/index.vue b/src/views/infra/fileList/index.vue index b9bfb815..2309d05c 100644 --- a/src/views/infra/fileList/index.vue +++ b/src/views/infra/fileList/index.vue @@ -59,6 +59,7 @@ :on-exceed="handleExceed" :on-success="handleFileSuccess" :on-error="excelUploadError" + :on-change="handleFileChange" :before-remove="beforeRemove" :auto-upload="false" accept=".jpg, .png, .gif" @@ -83,7 +84,7 @@ </XModal> </template> <script setup lang="ts" name="FileList"> -import type { UploadInstance, UploadRawFile, UploadProps } from 'element-plus' +import type { UploadInstance, UploadRawFile, UploadProps, UploadFile } from 'element-plus' // 业务相关的 import import { allSchemas } from './fileList.data' import * as FileApi from '@/api/infra/fileList' @@ -119,9 +120,11 @@ const beforeUpload = (file: UploadRawFile) => { return isImg && isLt5M } // 处理上传的文件发生变化 -// const handleFileChange = (uploadFile: UploadFile): void => { -// uploadRef.value.data.path = uploadFile.name -// } +const handleFileChange = (uploadFile: UploadFile): void => { + // uploadRef.value.data.path = uploadFile.name + console.log(uploadFile, 'uploadFile') + uploadDisabled.value = false +} // 文件上传 const submitFileForm = () => { uploadHeaders.value = { @@ -148,10 +151,12 @@ const beforeRemove: UploadProps['beforeRemove'] = () => { // 文件数超出提示 const handleExceed = (): void => { message.error('最多只能上传一个文件!') + uploadDisabled.value = false } // 上传错误提示 const excelUploadError = (): void => { message.error('导入数据失败,请您重新上传!') + uploadDisabled.value = false } // 详情操作 @@ -164,14 +169,26 @@ const handleDetail = (row: FileApi.FileVO) => { // ========== 复制相关 ========== const handleCopy = async (text: string) => { - const { copy, copied, isSupported } = useClipboard({ source: text }) - if (!isSupported) { - message.error(t('common.copyError')) - } else { - await copy() - if (unref(copied)) { - message.success(t('common.copySuccess')) - } - } + let url = text + let oInput = document.createElement('textarea') + oInput.value = url + document.body.appendChild(oInput) + oInput.select() // 选择对象; + // console.log(oInput.value) + document.execCommand('Copy') // 执行浏览器复制命令 + message.success(t('common.copySuccess')) + oInput.remove() + // const { copy, copied, isSupported } = useClipboard({ source: text, read: true }) + // console.log(copy, 'copycopycopy') + // console.log(copied, 'copiedcopiedcopied') + // console.log(isSupported, 'isSupportedisSupportedisSupported') + // if (!isSupported.value) { + // message.error(t('common.copyError')) + // } else { + // await copy() + // if (unref(copied.value)) { + // message.success(t('common.copySuccess')) + // } + // } } </script> diff --git a/src/views/infra/job/JobLog.vue b/src/views/infra/job/JobLog.vue index daa20046..1771bbd7 100644 --- a/src/views/infra/job/JobLog.vue +++ b/src/views/infra/job/JobLog.vue @@ -1,179 +1,77 @@ <template> - <content-wrap> - <!-- 搜索栏 --> - <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="120px"> - <el-form-item label="处理器的名字" prop="handlerName"> - <el-input - v-model="queryParams.handlerName" - placeholder="请输入处理器的名字" - clearable - @keyup.enter="handleQuery" - /> - </el-form-item> - <el-form-item label="开始执行时间" prop="beginTime"> - <el-date-picker - clearable - v-model="queryParams.beginTime" - type="date" - value-format="YYYY-MM-DD" - placeholder="选择开始执行时间" - /> - </el-form-item> - <el-form-item label="结束执行时间" prop="endTime"> - <el-date-picker - clearable - v-model="queryParams.endTime" - type="date" - value-format="YYYY-MM-DD" - placeholder="选择结束执行时间" - /> - </el-form-item> - <el-form-item label="任务状态" prop="status"> - <el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable> - <el-option - v-for="dict in getDictOptions(DICT_TYPE.INFRA_JOB_LOG_STATUS)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> - <el-button - type="success" - plain - @click="handleExport" - :loading="exportLoading" + <ContentWrap> + <!-- 列表 --> + <XTable @register="registerTable"> + <template #toolbar_buttons> + <XButton + type="warning" + preIcon="ep:download" + :title="t('action.export')" v-hasPermi="['infra:job:export']" - > - <Icon icon="ep:download" class="mr-5px" /> 导出 - </el-button> - </el-form-item> - </el-form> - - <el-table v-loading="loading" :data="list"> - <el-table-column label="日志编号" align="center" prop="id" /> - <el-table-column label="任务编号" align="center" prop="jobId" /> - <el-table-column label="处理器的名字" align="center" prop="handlerName" /> - <el-table-column label="处理器的参数" align="center" prop="handlerParam" /> - <el-table-column label="第几次执行" align="center" prop="executeIndex" /> - <el-table-column label="执行时间" align="center" width="180"> - <template #default="scope"> - <span>{{ parseTime(scope.row.beginTime) + ' ~ ' + parseTime(scope.row.endTime) }}</span> - </template> - </el-table-column> - <el-table-column label="执行时长" align="center" prop="duration"> - <template #default="scope"> - <span>{{ scope.row.duration + ' 毫秒' }}</span> - </template> - </el-table-column> - <el-table-column label="任务状态" align="center" prop="status"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.INFRA_JOB_LOG_STATUS" :value="scope.row.status" /> - </template> - </el-table-column> - <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> - <template #default="scope"> - <el-button - link - icon="el-icon-view" - @click="handleView(scope.row.id)" - :loading="exportLoading" - v-hasPermi="['infra:job:query']" - >详细 - </el-button> - </template> - </el-table-column> - </el-table> - - <pagination - v-show="total > 0" - :total="total" - v-model:page="queryParams.pageNo" - v-model:limit="queryParams.pageSize" - @pagination="getList" - /> - </content-wrap> - <!-- 表单弹窗:查看 --> - <log-view ref="viewModalRef" @success="getList" /> + @click="exportList('定时任务详情.xls')" + /> + </template> + <template #beginTime_default="{ row }"> + <span>{{ + dayjs(row.beginTime).format('YYYY-MM-DD HH:mm:ss') + + ' ~ ' + + dayjs(row.endTime).format('YYYY-MM-DD HH:mm:ss') + }}</span> + </template> + <template #duration_default="{ row }"> + <span>{{ row.duration + ' 毫秒' }}</span> + </template> + <template #actionbtns_default="{ row }"> + <XTextButton + preIcon="ep:view" + :title="t('action.detail')" + v-hasPermi="['infra:job:query']" + @click="handleDetail(row)" + /> + </template> + </XTable> + </ContentWrap> + <XModal v-model="dialogVisible" :title="dialogTitle"> + <!-- 对话框(详情) --> + <Descriptions :schema="allSchemas.detailSchema" :data="detailData"> + <template #retryInterval="{ row }"> + <span>{{ row.retryInterval + '毫秒' }} </span> + </template> + <template #monitorTimeout="{ row }"> + <span>{{ row.monitorTimeout > 0 ? row.monitorTimeout + ' 毫秒' : '未开启' }}</span> + </template> + </Descriptions> + <!-- 操作按钮 --> + <template #footer> + <XButton :title="t('dialog.close')" @click="dialogVisible = false" /> + </template> + </XModal> </template> - <script setup lang="ts" name="JobLog"> -import { DICT_TYPE, getDictOptions } from '@/utils/dict' -import download from '@/utils/download' -import LogView from './JobLogView.vue' +import dayjs from 'dayjs' import * as JobLogApi from '@/api/infra/jobLog' -import { parseTime } from './utils' +import { allSchemas } from './jobLog.data' -const message = useMessage() // 消息弹窗 - -const loading = ref(true) // 列表的加载中 -const total = ref(0) // 列表的总页数 -const list = ref([]) // 列表的数据 -const queryParams = reactive({ - pageNo: 1, - pageSize: 10, - handlerName: undefined, - beginTime: undefined, - endTime: undefined, - status: undefined +const { t } = useI18n() // 国际化 +// 列表相关的变量 +const [registerTable, { exportList }] = useXTable({ + allSchemas: allSchemas, + getListApi: JobLogApi.getJobLogPageApi, + exportListApi: JobLogApi.exportJobLogApi }) -const queryFormRef = ref() // 搜索的表单 -const exportLoading = ref(false) // 导出的加载中 +// ========== CRUD 相关 ========== +const dialogVisible = ref(false) // 是否显示弹出层 +const dialogTitle = ref('') // 弹出层标题 -/** 查询参数列表 */ -const getList = async () => { - loading.value = true - try { - const data = await JobLogApi.getJobLogPageApi({ - ...queryParams, - beginTime: queryParams.beginTime ? queryParams.beginTime + ' 00:00:00' : undefined, - endTime: queryParams.endTime ? queryParams.endTime + ' 23:59:59' : undefined - }) - list.value = data.list - total.value = data.total - } finally { - loading.value = false - } +// ========== 详情相关 ========== +const detailData = ref() // 详情 Ref + +// 详情操作 +const handleDetail = async (row: JobLogApi.JobLogVO) => { + // 设置数据 + const res = await JobLogApi.getJobLogApi(row.id) + detailData.value = res + dialogTitle.value = t('action.detail') + dialogVisible.value = true } - -/** 搜索按钮操作 */ -const handleQuery = () => { - queryParams.pageNo = 1 - getList() -} - -/** 重置按钮操作 */ -const resetQuery = () => { - queryFormRef.value.resetFields() - handleQuery() -} - -/** 查看操作 */ -const viewModalRef = ref() -const handleView = (rowId?: number) => { - viewModalRef.value.openModal(rowId) -} - -/** 导出按钮操作 */ -const handleExport = async () => { - try { - // 导出的二次确认 - await message.exportConfirm() - // 发起导出 - exportLoading.value = true - const data = await JobLogApi.exportJobLogApi(queryParams) - download.excel(data, '定时任务执行日志.xls') - } catch { - } finally { - exportLoading.value = false - } -} - -/** 初始化 **/ -onMounted(() => { - getList() -}) </script> diff --git a/src/views/infra/job/JobLogView.vue b/src/views/infra/job/JobLogView.vue deleted file mode 100644 index c66e0d80..00000000 --- a/src/views/infra/job/JobLogView.vue +++ /dev/null @@ -1,74 +0,0 @@ -<template> - <!-- 调度日志详细 --> - <Dialog title="调度日志详细" v-model="modelVisible" width="700px" append-to-body> - <el-form ref="form" :model="formData" label-width="120px" size="mini"> - <el-row> - <el-col :span="12"> - <el-form-item label="日志编号:">{{ formData.id }}</el-form-item> - <el-form-item label="任务编号:">{{ formData.jobId }}</el-form-item> - <el-form-item label="处理器的名字:">{{ formData.handlerName }}</el-form-item> - <el-form-item label="处理器的参数:">{{ formData.handlerParam }}</el-form-item> - <el-form-item label="第几次执行:">{{ formData.executeIndex }}</el-form-item> - <el-form-item label="执行时间:">{{ - parseTime(formData.beginTime) + ' ~ ' + parseTime(formData.endTime) - }}</el-form-item> - <el-form-item label="执行时长:">{{ formData.duration + ' 毫秒' }}</el-form-item> - <el-form-item label="任务状态:"> - <dict-tag :type="DICT_TYPE.INFRA_JOB_LOG_STATUS" :value="formData.status" /> - </el-form-item> - <el-form-item label="执行结果:">{{ formData.result }}</el-form-item> - </el-col> - </el-row> - </el-form> - <template #footer> - <div class="dialog-footer"> - <el-button @click="close">关 闭</el-button> - </div> - </template> - </Dialog> -</template> -<script setup lang="ts" name="JobView"> -import * as JobLogApi from '@/api/infra/jobLog' -import { DICT_TYPE } from '@/utils/dict' -import { parseTime } from './utils' - -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 - -const { t } = useI18n() // 国际化 - -const modelVisible = ref(false) // 弹窗的是否展示 -const modelTitle = ref('') // 弹窗的标题 -const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const formData = ref({ - id: undefined, - jobId: undefined, - handlerParam: '', - handlerName: '', - executeIndex: '', - beginTime: undefined, - endTime: undefined, - duration: true, - result: '', - status: undefined -}) - -/** 打开弹窗 */ -const openModal = async (id?: number) => { - modelVisible.value = true - modelTitle.value = t('action.detail') - // 查看,设置数据 - if (id) { - formLoading.value = true - try { - formData.value = await JobLogApi.getJobLogApi(id) - } finally { - formLoading.value = false - } - } -} -defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗 - -const close = () => { - emit('success') -} -</script> diff --git a/src/views/infra/job/form.vue b/src/views/infra/job/form.vue deleted file mode 100644 index 24488fd7..00000000 --- a/src/views/infra/job/form.vue +++ /dev/null @@ -1,172 +0,0 @@ -<template> - <!-- 添加或修改定时任务对话框 --> - <Dialog :title="modelTitle" v-model="modelVisible"> - <el-form - ref="formRef" - :model="formData" - :rules="formRules" - label-width="120px" - v-loading="formLoading" - > - <el-form-item label="任务名称" prop="name"> - <el-input v-model="formData.name" placeholder="请输入任务名称" /> - </el-form-item> - <el-form-item label="处理器的名字" prop="handlerName"> - <el-input - :readonly="formData.id !== undefined" - v-model="formData.handlerName" - placeholder="请输入处理器的名字" - /> - </el-form-item> - <el-form-item label="处理器的参数" prop="handlerParam"> - <el-input v-model="formData.handlerParam" placeholder="请输入处理器的参数" /> - </el-form-item> - <el-form-item label="CRON 表达式" prop="cronExpression"> - <el-input v-model="formData.cronExpression" placeholder="请输入CRON 表达式"> - <template #append> - <el-button type="primary" @click="handleShowCron"> - 生成表达式 - <i class="el-icon-time el-icon--right"></i> - </el-button> - </template> - </el-input> - </el-form-item> - <el-form-item label="重试次数" prop="retryCount"> - <el-input - v-model="formData.retryCount" - placeholder="请输入重试次数。设置为 0 时,不进行重试" - /> - </el-form-item> - <el-form-item label="重试间隔" prop="retryInterval"> - <el-input - v-model="formData.retryInterval" - placeholder="请输入重试间隔,单位:毫秒。设置为 0 时,无需间隔" - /> - </el-form-item> - <el-form-item label="监控超时时间" prop="monitorTimeout"> - <el-input v-model="formData.monitorTimeout" placeholder="请输入监控超时时间,单位:毫秒" /> - </el-form-item> - </el-form> - <!-- 操作按钮 --> - <template #footer> - <!-- 按钮:保存 --> - <div class="dialog-footer"> - <el-button type="primary" @click="submitForm" :loading="formLoading">确 定</el-button> - <el-button @click="modelVisible = false">取 消</el-button> - </div> - </template> - </Dialog> - <el-dialog - title="Cron表达式生成器" - v-model="openCron" - append-to-body - class="scrollbar" - destroy-on-close - > - <crontab @hide="openCron = false" @fill="crontabFill" :expression="expression" /> - </el-dialog> -</template> -<script setup lang="ts" name="JobForm"> -import * as JobApi from '@/api/infra/job' - -const emit = defineEmits(['success', 'crontabFill']) // 定义 success 事件,用于操作成功后的回调 - -const { t } = useI18n() // 国际化 -const message = useMessage() // 消息弹窗 - -const modelVisible = ref(false) // 弹窗的是否展示 -const modelTitle = ref('') // 弹窗的标题 -const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const formType = ref('') // 表单的类型:create - 新增;update - 修改 -const defaultFormData = { - id: undefined, - name: '', - status: 0, - handlerName: '', - handlerParam: '', - cronExpression: '', - retryCount: 0, - retryInterval: 0, - monitorTimeout: 0, - createTime: new Date() -} -const formData = ref({ ...defaultFormData }) - -// 是否显示Cron表达式弹出层 -const openCron = ref(false) -// 传入的表达式 -const expression = ref('') -// 表单校验 -const formRules = reactive({ - name: [{ required: true, message: '任务名称不能为空', trigger: 'blur' }], - handlerName: [{ required: true, message: '处理器的名字不能为空', trigger: 'blur' }], - cronExpression: [{ required: true, message: 'CRON 表达式不能为空', trigger: 'blur' }], - retryCount: [{ required: true, message: '重试次数不能为空', trigger: 'blur' }], - retryInterval: [{ required: true, message: '重试间隔不能为空', trigger: 'blur' }] -}) -const formRef = ref() // 表单 Ref - -/** 打开弹窗 */ -const openModal = async (type: string, id?: number) => { - modelVisible.value = true - modelTitle.value = t('action.' + type) - formType.value = type - resetForm() - // 修改时,设置数据 - if (id) { - formLoading.value = true - try { - formData.value = await JobApi.getJobApi(id) - } finally { - formLoading.value = false - } - } -} -defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗 - -/** cron表达式按钮操作 */ -const handleShowCron = () => { - console.info(123333333333) - expression.value = formData.value.cronExpression - openCron.value = true -} - -// cron表达式填充 -const crontabFill = (expression: string) => { - formData.value.cronExpression = expression - emit('crontabFill', expression) -} - -// 提交按钮 -const submitForm = async () => { - // 校验表单 - if (!formRef) return - const valid = await formRef.value.validate() - if (!valid) return - // 提交请求 - formLoading.value = true - try { - const data = formData.value as unknown as JobApi.JobVO - if (formType.value === 'create') { - await JobApi.createJobApi(data) - message.success(t('common.createSuccess')) - } else { - await JobApi.updateJobApi(data) - message.success(t('common.updateSuccess')) - } - modelVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - ...defaultFormData - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/infra/job/index.vue b/src/views/infra/job/index.vue index 702b31fe..613f84c4 100644 --- a/src/views/infra/job/index.vue +++ b/src/views/infra/job/index.vue @@ -1,175 +1,243 @@ <template> - <content-wrap> - <!-- 搜索栏 --> - <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="100px"> - <el-form-item label="任务名称" prop="name"> - <el-input - v-model="queryParams.name" - placeholder="请输入任务名称" - clearable - @keyup.enter="handleQuery" - /> - </el-form-item> - <el-form-item label="任务状态" prop="status"> - <el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable> - <el-option - v-for="dict in getDictOptions(DICT_TYPE.INFRA_JOB_STATUS)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item label="处理器的名字" prop="handlerName"> - <el-input - v-model="queryParams.handlerName" - placeholder="请输入处理器的名字" - clearable - @keyup.enter="handleQuery" - /> - </el-form-item> - <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> - <el-button + <ContentWrap> + <!-- 列表 --> + <XTable @register="registerTable"> + <template #toolbar_buttons> + <!-- 操作:新增 --> + <XButton type="primary" - plain - @click="openModal('create')" + preIcon="ep:zoom-in" + :title="t('action.add')" v-hasPermi="['infra:job:create']" - > - <Icon icon="ep:plus" class="mr-5px" /> 新增 - </el-button> - <el-button - type="success" - plain - @click="handleExport" - :loading="exportLoading" + @click="handleCreate()" + /> + <!-- 操作:导出 --> + <XButton + type="warning" + preIcon="ep:download" + :title="t('action.export')" v-hasPermi="['infra:job:export']" - > - <Icon icon="ep:download" class="mr-5px" /> 导出 - </el-button> - - <el-button type="info" plain @click="handleJobLog" v-hasPermi="['infra:job:query']"> - <Icon icon="ep:zoom-in" class="mr-5px" /> 执行日志 - </el-button> - </el-form-item> - </el-form> - - <el-table v-loading="loading" :data="list"> - <el-table-column label="任务编号" align="center" prop="id" /> - <el-table-column label="任务名称" align="center" prop="name" /> - <el-table-column label="任务状态" align="center" prop="status"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.INFRA_JOB_STATUS" :value="scope.row.status" /> - </template> </el-table-column - >> - <el-table-column label="处理器的名字" align="center" prop="handlerName" /> - <el-table-column label="处理器的参数" align="center" prop="handlerParam" /> - <el-table-column label="CRON 表达式" align="center" prop="cronExpression" /> - <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> - <template #default="scope"> - <el-button - link - icon="el-icon-edit" - @click="openModal('update', scope.row.id)" - v-hasPermi="['infra:job:update']" - >修改</el-button - > - <el-button - link - icon="el-icon-check" - @click="handleChangeStatus(scope.row)" - v-hasPermi="['infra:job:update']" - >{{ scope.row.status === InfraJobStatusEnum.STOP ? '开启' : '暂停' }}</el-button - > - <el-button - link - icon="el-icon-delete" - @click="handleDelete(scope.row)" - v-hasPermi="['infra:job:delete']" - >删除</el-button - > - <el-dropdown - class="mt-1" - :teleported="true" - @command="(command) => handleCommand(command, scope.row)" - v-hasPermi="['infra:job:trigger', 'infra:job:query']" - > - <el-button link icon="el-icon-d-arrow-right">更多</el-button> - <template #dropdown> - <el-dropdown-menu> - <el-dropdown-item command="handleRun" v-if="hasPermi(['infra:job:trigger'])"> - 执行一次 - </el-dropdown-item> - <el-dropdown-item command="handleView" v-if="hasPermi(['infra:job:query'])"> - 任务详细 - </el-dropdown-item> - <el-dropdown-item command="handleJobLog" v-if="hasPermi(['infra:job:query'])"> - 调度日志 - </el-dropdown-item> - </el-dropdown-menu> - </template> - </el-dropdown> - </template> - </el-table-column> - </el-table> - <!-- 分页组件 --> - <pagination - v-show="total > 0" - :total="total" - v-model:page="queryParams.pageNo" - v-model:limit="queryParams.pageSize" - @pagination="getList" - /> - </content-wrap> - - <!-- 表单弹窗:添加/修改 --> - <job-form ref="modalRef" @success="getList" /> - <!-- 表单弹窗:查看 --> - <job-view ref="viewModalRef" @success="getList" /> + @click="exportList('定时任务.xls')" + /> + <XButton + type="info" + preIcon="ep:zoom-in" + title="执行日志" + v-hasPermi="['infra:job:query']" + @click="handleJobLog()" + /> + </template> + <template #actionbtns_default="{ row }"> + <!-- 操作:修改 --> + <XTextButton + preIcon="ep:edit" + :title="t('action.edit')" + v-hasPermi="['infra:job:update']" + @click="handleUpdate(row.id)" + /> + <XTextButton + preIcon="ep:edit" + :title="row.status === InfraJobStatusEnum.STOP ? '开启' : '暂停'" + v-hasPermi="['infra:job:update']" + @click="handleChangeStatus(row)" + /> + <!-- 操作:删除 --> + <XTextButton + preIcon="ep:delete" + :title="t('action.del')" + v-hasPermi="['infra:job:delete']" + @click="deleteData(row.id)" + /> + <el-dropdown class="p-0.5" v-hasPermi="['infra:job:trigger', 'infra:job:query']"> + <XTextButton :title="t('action.more')" postIcon="ep:arrow-down" /> + <template #dropdown> + <el-dropdown-menu> + <el-dropdown-item> + <!-- 操作:执行 --> + <XTextButton + preIcon="ep:view" + title="执行一次" + v-hasPermi="['infra:job:trigger']" + @click="handleRun(row)" + /> + </el-dropdown-item> + <el-dropdown-item> + <!-- 操作:详情 --> + <XTextButton + preIcon="ep:view" + :title="t('action.detail')" + v-hasPermi="['infra:job:query']" + @click="handleDetail(row.id)" + /> + </el-dropdown-item> + <el-dropdown-item> + <!-- 操作:日志 --> + <XTextButton + preIcon="ep:view" + title="调度日志" + v-hasPermi="['infra:job:query']" + @click="handleJobLog(row.id)" + /> + </el-dropdown-item> + </el-dropdown-menu> + </template> + </el-dropdown> + </template> + </XTable> + </ContentWrap> + <XModal v-model="dialogVisible" :title="dialogTitle"> + <!-- 对话框(添加 / 修改) --> + <Form + v-if="['create', 'update'].includes(actionType)" + :schema="allSchemas.formSchema" + :rules="rules" + ref="formRef" + > + <template #cronExpression="form"> + <Crontab v-model="form['cronExpression']" :shortcuts="shortcuts" /> + </template> + </Form> + <!-- 对话框(详情) --> + <Descriptions + v-if="actionType === 'detail'" + :schema="allSchemas.detailSchema" + :data="detailData" + > + <template #retryInterval="{ row }"> + <span>{{ row.retryInterval + '毫秒' }} </span> + </template> + <template #monitorTimeout="{ row }"> + <span>{{ row.monitorTimeout > 0 ? row.monitorTimeout + ' 毫秒' : '未开启' }}</span> + </template> + <template #nextTimes> + <span>{{ Array.from(nextTimes, (x) => parseTime(x)).join('; ') }}</span> + </template> + </Descriptions> + <!-- 操作按钮 --> + <template #footer> + <!-- 按钮:保存 --> + <XButton + v-if="['create', 'update'].includes(actionType)" + type="primary" + :title="t('action.save')" + :loading="actionLoading" + @click="submitForm()" + /> + <!-- 按钮:关闭 --> + <XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" /> + </template> + </XModal> </template> - <script setup lang="ts" name="Job"> -import { DICT_TYPE, getDictOptions } from '@/utils/dict' -import JobForm from './form.vue' -import JobView from './view.vue' -import download from '@/utils/download' +import type { FormExpose } from '@/components/Form' import * as JobApi from '@/api/infra/job' +import { rules, allSchemas } from './job.data' import { InfraJobStatusEnum } from '@/utils/constants' -import { CACHE_KEY, useCache } from '@/hooks/web/useCache' const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 const { push } = useRouter() -const loading = ref(true) // 列表的加载中 -const total = ref(0) // 列表的总页数 -const list = ref([]) // 列表的数据 -const queryParams = reactive({ - pageNo: 1, - pageSize: 10, - name: undefined, - status: undefined, - handlerName: undefined +// 列表相关的变量 +const [registerTable, { reload, deleteData, exportList }] = useXTable({ + allSchemas: allSchemas, + getListApi: JobApi.getJobPageApi, + deleteApi: JobApi.deleteJobApi, + exportListApi: JobApi.exportJobApi }) -const queryFormRef = ref() // 搜索的表单 -const exportLoading = ref(false) // 导出的加载中 -/** 查询参数列表 */ -const getList = async () => { - loading.value = true - try { - const data = await JobApi.getJobPageApi(queryParams) - list.value = data.list - total.value = data.total - } finally { - loading.value = false +// ========== CRUD 相关 ========== +const actionLoading = ref(false) // 遮罩层 +const actionType = ref('') // 操作按钮的类型 +const dialogVisible = ref(false) // 是否显示弹出层 +const dialogTitle = ref('edit') // 弹出层标题 +const formRef = ref<FormExpose>() // 表单 Ref +const detailData = ref() // 详情 Ref +const nextTimes = ref([]) +const shortcuts = ref([ + { + text: '每天8点和12点 (自定义追加)', + value: '0 0 8,12 * * ?' } +]) +// 设置标题 +const setDialogTile = (type: string) => { + dialogTitle.value = t('action.' + type) + actionType.value = type + dialogVisible.value = true +} + +// 新增操作 +const handleCreate = () => { + setDialogTile('create') +} + +// 修改操作 +const handleUpdate = async (rowId: number) => { + setDialogTile('update') + // 设置数据 + const res = await JobApi.getJobApi(rowId) + unref(formRef)?.setValues(res) +} + +// 详情操作 +const handleDetail = async (rowId: number) => { + // 设置数据 + const res = await JobApi.getJobApi(rowId) + detailData.value = res + // 后续执行时长 + const jobNextTime = await JobApi.getJobNextTimesApi(rowId) + nextTimes.value = jobNextTime + setDialogTile('detail') +} + +const parseTime = (time) => { + if (!time) { + return null + } + const format = '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if (typeof time === 'string' && /^[0-9]+$/.test(time)) { + time = parseInt(time) + } else if (typeof time === 'string') { + time = time + .replace(new RegExp(/-/gm), '/') + .replace('T', ' ') + .replace(new RegExp(/\.[\d]{3}/gm), '') + } + if (typeof time === 'number' && time.toString().length === 10) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { + let value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { + return ['日', '一', '二', '三', '四', '五', '六'][value] + } + if (result.length > 0 && value < 10) { + value = '0' + value + } + return value || 0 + }) + return time_str } const handleChangeStatus = async (row: JobApi.JobVO) => { const text = row.status === InfraJobStatusEnum.STOP ? '开启' : '关闭' - const status = row.status === InfraJobStatusEnum.STOP ? InfraJobStatusEnum.NORMAL : InfraJobStatusEnum.STOP message @@ -181,7 +249,7 @@ const handleChangeStatus = async (row: JobApi.JobVO) => { : InfraJobStatusEnum.STOP await JobApi.updateJobStatusApi(row.id, status) message.success(text + '成功') - await getList() + await reload() }) .catch(() => { row.status = @@ -190,43 +258,6 @@ const handleChangeStatus = async (row: JobApi.JobVO) => { : InfraJobStatusEnum.NORMAL }) } - -/** 搜索按钮操作 */ -const handleQuery = () => { - queryParams.pageNo = 1 - getList() -} - -/** 重置按钮操作 */ -const resetQuery = () => { - queryFormRef.value.resetFields() - handleQuery() -} - -/** 添加/修改操作 */ -const modalRef = ref() -const openModal = (type: string, id?: number) => { - modalRef.value.openModal(type, id) -} - -/** 删除按钮操作 */ -const handleDelete = async (id: number) => { - try { - // 删除的二次确认 - await message.delConfirm() - // 发起删除 - await JobApi.deleteJobApi(id) - message.success(t('common.delSuccess')) - // 刷新列表 - await getList() - } catch {} -} - -/** 查看操作 */ -const viewModalRef = ref() -const handleView = (rowId?: number) => { - viewModalRef.value.openModal(rowId) -} // 执行日志 const handleJobLog = (rowId?: number) => { if (rowId) { @@ -240,61 +271,32 @@ const handleRun = (row: JobApi.JobVO) => { message.confirm('确认要立即执行一次' + row.name + '?', t('common.reminder')).then(async () => { await JobApi.runJobApi(row.id) message.success('执行成功') - await getList() + await reload() }) } - -/** '更多'操作按钮 */ -const handleCommand = (command, row) => { - switch (command) { - case 'handleRun': - handleRun(row) - break - case 'handleView': - handleView(row?.id) - break - case 'handleJobLog': - handleJobLog(row?.id) - break - default: - break - } +// 提交按钮 +const submitForm = async () => { + const elForm = unref(formRef)?.getElFormRef() + if (!elForm) return + elForm.validate(async (valid) => { + if (valid) { + actionLoading.value = true + // 提交请求 + try { + const data = unref(formRef)?.formModel as JobApi.JobVO + if (actionType.value === 'create') { + await JobApi.createJobApi(data) + message.success(t('common.createSuccess')) + } else { + await JobApi.updateJobApi(data) + message.success(t('common.updateSuccess')) + } + dialogVisible.value = false + } finally { + actionLoading.value = false + await reload() + } + } + }) } - -/** 导出按钮操作 */ -const handleExport = async () => { - try { - // 导出的二次确认 - await message.exportConfirm() - // 发起导出 - exportLoading.value = true - const data = await JobApi.exportJobApi(queryParams) - download.excel(data, '定时任务.xls') - } catch { - } finally { - exportLoading.value = false - } -} - -// 权限判断:dropdown 与 v-hasPermi有冲突会造成大量的waring,改用v-if调用此方法 -const hasPermi = (permiKeys: string[]) => { - const { wsCache } = useCache() - const all_permission = '*:*:*' - const permissions = wsCache.get(CACHE_KEY.USER).permissions - - if (permiKeys && permiKeys instanceof Array && permiKeys.length > 0) { - const permissionFlag = permiKeys - - const hasPermissions = permissions.some((permission: string) => { - return all_permission === permission || permissionFlag.includes(permission) - }) - return hasPermissions - } - return false -} - -/** 初始化 **/ -onMounted(() => { - getList() -}) </script> diff --git a/src/views/infra/job/job.data.ts b/src/views/infra/job/job.data.ts new file mode 100644 index 00000000..1ca4a564 --- /dev/null +++ b/src/views/infra/job/job.data.ts @@ -0,0 +1,70 @@ +import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' +const { t } = useI18n() // 国际化 +// 表单校验 + +export const rules = reactive({ + name: [required], + handlerName: [required], + cronExpression: [required], + retryCount: [required], + retryInterval: [required] +}) +// CrudSchema +const crudSchemas = reactive<VxeCrudSchema>({ + primaryKey: 'id', + primaryType: 'id', + primaryTitle: '任务编号', + action: true, + actionWidth: '280px', + columns: [ + { + title: '任务名称', + field: 'name', + isSearch: true + }, + { + title: t('common.status'), + field: 'status', + dictType: DICT_TYPE.INFRA_JOB_STATUS, + dictClass: 'number', + isForm: false, + isSearch: true + }, + { + title: '处理器的名字', + field: 'handlerName', + isSearch: true + }, + { + title: '处理器的参数', + field: 'handlerParam', + isTable: false + }, + { + title: 'CRON 表达式', + field: 'cronExpression' + }, + { + title: '后续执行时间', + field: 'nextTimes', + isTable: false, + isForm: false + }, + { + title: '重试次数', + field: 'retryCount', + isTable: false + }, + { + title: '重试间隔', + field: 'retryInterval', + isTable: false + }, + { + title: '监控超时时间', + field: 'monitorTimeout', + isTable: false + } + ] +}) +export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/infra/job/jobLog.data.ts b/src/views/infra/job/jobLog.data.ts new file mode 100644 index 00000000..563c097c --- /dev/null +++ b/src/views/infra/job/jobLog.data.ts @@ -0,0 +1,76 @@ +import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' +// 国际化 + +const { t } = useI18n() +// CrudSchema +const crudSchemas = reactive<VxeCrudSchema>({ + primaryKey: 'id', + primaryType: 'id', + primaryTitle: '日志编号', + action: true, + columns: [ + { + title: '任务编号', + field: 'jobId', + isSearch: true + }, + { + title: '处理器的名字', + field: 'handlerName', + isSearch: true + }, + { + title: '处理器的参数', + field: 'handlerParam' + }, + { + title: '第几次执行', + field: 'executeIndex' + }, + { + title: '开始执行时间', + field: 'beginTime', + formatter: 'formatDate', + table: { + slots: { + default: 'beginTime_default' + } + }, + search: { + show: true, + itemRender: { + name: 'XDataPicker' + } + } + }, + { + title: '结束执行时间', + field: 'endTime', + formatter: 'formatDate', + isTable: false, + search: { + show: true, + itemRender: { + name: 'XDataPicker' + } + } + }, + { + title: '执行时长', + field: 'duration', + table: { + slots: { + default: 'duration_default' + } + } + }, + { + title: t('common.status'), + field: 'status', + dictType: DICT_TYPE.INFRA_JOB_LOG_STATUS, + dictClass: 'number', + isSearch: true + } + ] +}) +export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/infra/job/utils.ts b/src/views/infra/job/utils.ts deleted file mode 100644 index a3774f22..00000000 --- a/src/views/infra/job/utils.ts +++ /dev/null @@ -1,44 +0,0 @@ -export const parseTime = (time) => { - if (!time) { - return null - } - const format = '{y}-{m}-{d} {h}:{i}:{s}' - let date - if (typeof time === 'object') { - date = time - } else { - if (typeof time === 'string' && /^[0-9]+$/.test(time)) { - time = parseInt(time) - } else if (typeof time === 'string') { - time = time - .replace(new RegExp(/-/gm), '/') - .replace('T', ' ') - .replace(new RegExp(/\.[\d]{3}/gm), '') - } - if (typeof time === 'number' && time.toString().length === 10) { - time = time * 1000 - } - date = new Date(time) - } - const formatObj = { - y: date.getFullYear(), - m: date.getMonth() + 1, - d: date.getDate(), - h: date.getHours(), - i: date.getMinutes(), - s: date.getSeconds(), - a: date.getDay() - } - const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { - let value = formatObj[key] - // Note: getDay() returns 0 on Sunday - if (key === 'a') { - return ['日', '一', '二', '三', '四', '五', '六'][value] - } - if (result.length > 0 && value < 10) { - value = '0' + value - } - return value || 0 - }) - return time_str -} diff --git a/src/views/infra/job/view.vue b/src/views/infra/job/view.vue deleted file mode 100644 index d195e0e3..00000000 --- a/src/views/infra/job/view.vue +++ /dev/null @@ -1,89 +0,0 @@ -<template> - <!-- 任务详细 --> - <Dialog title="任务详细" v-model="modelVisible" width="700px" append-to-body> - <el-form ref="formRef" :model="formData" label-width="200px"> - <el-row> - <el-col :span="24"> - <el-form-item label="任务编号:">{{ formData.id }}</el-form-item> - <el-form-item label="任务名称:">{{ formData.name }}</el-form-item> - <el-form-item label="任务名称:"> - <dict-tag :type="DICT_TYPE.INFRA_JOB_STATUS" :value="formData.status" /> - </el-form-item> - <el-form-item label="处理器的名字:">{{ formData.handlerName }}</el-form-item> - <el-form-item label="处理器的参数:">{{ formData.handlerParam }}</el-form-item> - <el-form-item label="cron表达式:">{{ formData.cronExpression }}</el-form-item> - <el-form-item label="重试次数:">{{ formData.retryCount }}</el-form-item> - <el-form-item label="重试间隔:">{{ formData.retryInterval + ' 毫秒' }}</el-form-item> - <el-form-item label="监控超时时间:">{{ - formData.monitorTimeout > 0 ? formData.monitorTimeout + ' 毫秒' : '未开启' - }}</el-form-item> - <el-form-item label="后续执行时间:"> - <el-timeline class="pt-3"> - <el-timeline-item - v-for="(activity, index) in nextTimes" - :key="index" - :timestamp="parseTime(activity)" - > - 第{{ index + 1 }}次 - </el-timeline-item> - </el-timeline> - </el-form-item> - </el-col> - </el-row> - </el-form> - <template #footer> - <div class="dialog-footer"> - <el-button @click="close">关 闭</el-button> - </div> - </template> - </Dialog> -</template> -<script setup lang="ts" name="JobView"> -import * as JobApi from '@/api/infra/job' -import { parseTime } from './utils' -import { DICT_TYPE } from '@/utils/dict' - -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 - -const { t } = useI18n() // 国际化 - -const formRef = ref() // 表单 Ref -const modelVisible = ref(false) // 弹窗的是否展示 -const modelTitle = ref('') // 弹窗的标题 -const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const formData = ref({ - id: undefined, - name: '', - handlerParam: '', - handlerName: '', - cronExpression: '', - retryCount: true, - retryInterval: '', - monitorTimeout: 0, - status: 0 -}) -const nextTimes = ref([]) - -/** 打开弹窗 */ -const openModal = async (id?: number) => { - modelVisible.value = true - modelTitle.value = t('action.detail') - // 查看,设置数据 - if (id) { - formLoading.value = true - try { - formData.value = await JobApi.getJobApi(id) - // 获取下一次执行时间 - nextTimes.value = await JobApi.getJobNextTimesApi(id) - } finally { - formLoading.value = false - } - } -} -defineExpose({ openModal }) // 提供 openModal 方法,用于打开弹窗 - -const close = () => { - modelVisible.value = false - emit('success') -} -</script> diff --git a/src/views/system/notice/index.vue b/src/views/system/notice/index.vue index 8add0eab..aceadbba 100644 --- a/src/views/system/notice/index.vue +++ b/src/views/system/notice/index.vue @@ -1,159 +1,148 @@ <template> - <content-wrap> - <!-- 搜索工作栏 --> - <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px"> - <el-form-item label="公告标题" prop="title"> - <el-input - v-model="queryParams.title" - placeholder="请输入公告标题" - clearable - @keyup.enter="handleQuery" - /> - </el-form-item> - <el-form-item label="公告状态" prop="status"> - <el-select v-model="queryParams.status" placeholder="请选择公告状态" clearable> - <el-option - v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)" - :key="parseInt(dict.value)" - :label="dict.label" - :value="parseInt(dict.value)" - /> - </el-select> - </el-form-item> - <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> - <el-button - type="primary" - @click="openModal('create')" - v-hasPermi="['system:notice:create']" - > - <Icon icon="ep:plus" class="mr-5px" /> 新增 - </el-button> - </el-form-item> - </el-form> - + <ContentWrap> <!-- 列表 --> - <el-table v-loading="loading" :data="list" align="center"> - <el-table-column label="公告编号" align="center" prop="id" /> - <el-table-column label="公告标题" align="center" prop="title" /> - <el-table-column label="公告类型" align="center" prop="type"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.SYSTEM_NOTICE_TYPE" :value="scope.row.type" /> - </template> - </el-table-column> - <el-table-column label="状态" align="center" prop="status"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> - </template> - </el-table-column> - <el-table-column - label="创建时间" - align="center" - prop="createTime" - width="180" - :formatter="dateFormatter" - /> - <el-table-column label="操作" align="center"> - <template #default="scope"> - <el-button - link - type="primary" - @click="openModal('update', scope.row.id)" - v-hasPermi="['system:notice:update']" - > - 编辑 - </el-button> - <el-button - link - type="danger" - @click="handleDelete(scope.row.id)" - v-hasPermi="['system:notice:delete']" - > - 删除 - </el-button> - </template> - </el-table-column> - </el-table> - <!-- 分页 --> - <Pagination - :total="total" - v-model:page="queryParams.pageNo" - v-model:limit="queryParams.pageSize" - @pagination="getList" + <XTable @register="registerTable"> + <!-- 操作:新增 --> + <template #toolbar_buttons> + <XButton + type="primary" + preIcon="ep:zoom-in" + :title="t('action.add')" + v-hasPermi="['system:notice:create']" + @click="handleCreate()" + /> + </template> + <template #actionbtns_default="{ row }"> + <!-- 操作:修改 --> + <XTextButton + preIcon="ep:edit" + :title="t('action.edit')" + v-hasPermi="['system:notice:update']" + @click="handleUpdate(row.id)" + /> + <!-- 操作:详情 --> + <XTextButton + preIcon="ep:view" + :title="t('action.detail')" + v-hasPermi="['system:notice:query']" + @click="handleDetail(row.id)" + /> + <!-- 操作:删除 --> + <XTextButton + preIcon="ep:delete" + :title="t('action.del')" + v-hasPermi="['system:notice:delete']" + @click="deleteData(row.id)" + /> + </template> + </XTable> + </ContentWrap> + <!-- 弹窗 --> + <XModal id="noticeModel" v-model="dialogVisible" :title="dialogTitle"> + <!-- 对话框(添加 / 修改) --> + <Form + ref="formRef" + v-if="['create', 'update'].includes(actionType)" + :schema="allSchemas.formSchema" + :rules="rules" /> - </content-wrap> - - <!-- 表单弹窗:添加/修改 --> - <notice-form ref="modalRef" @success="getList" /> + <!-- 对话框(详情) --> + <Descriptions + v-if="actionType === 'detail'" + :schema="allSchemas.detailSchema" + :data="detailData" + > + <template #content="{ row }"> + <Editor :model-value="row.content" :readonly="true" /> + </template> + </Descriptions> + <template #footer> + <!-- 按钮:保存 --> + <XButton + v-if="['create', 'update'].includes(actionType)" + type="primary" + :title="t('action.save')" + :loading="actionLoading" + @click="submitForm()" + /> + <!-- 按钮:关闭 --> + <XButton :loading="actionLoading" :title="t('dialog.close')" @click="dialogVisible = false" /> + </template> + </XModal> </template> -<script setup lang="tsx"> -import { DICT_TYPE, getDictOptions } from '@/utils/dict' -import { dateFormatter } from '@/utils/formatTime' +<script setup lang="ts" name="Notice"> +import type { FormExpose } from '@/components/Form' +// 业务相关的 import import * as NoticeApi from '@/api/system/notice' -import { DictTag } from '@/components/DictTag' -import NoticeForm from './form.vue' -const message = useMessage() // 消息弹窗 +import { rules, allSchemas } from './notice.data' + const { t } = useI18n() // 国际化 - -const loading = ref(true) // 列表的加载中 -const total = ref(0) // 列表的总页数 -const list = ref([]) // 列表的数据 -const queryParams = reactive({ - title: '', - type: undefined, - status: undefined, - pageNo: 1, - pageSize: 100 +const message = useMessage() // 消息弹窗 +// 列表相关的变量 +const [registerTable, { reload, deleteData }] = useXTable({ + allSchemas: allSchemas, + getListApi: NoticeApi.getNoticePageApi, + deleteApi: NoticeApi.deleteNoticeApi }) -const queryFormRef = ref() // 搜索的表单 +// 弹窗相关的变量 +const dialogVisible = ref(false) // 是否显示弹出层 +const dialogTitle = ref('edit') // 弹出层标题 +const actionType = ref('') // 操作按钮的类型 +const actionLoading = ref(false) // 按钮 Loading +const formRef = ref<FormExpose>() // 表单 Ref +const detailData = ref() // 详情 Ref -/** 查询公告列表 */ -const getList = async () => { - loading.value = true - try { - const data = await NoticeApi.getNoticePage(queryParams) - - list.value = data.list - total.value = data.total - } finally { - loading.value = false - } +// 设置标题 +const setDialogTile = (type: string) => { + dialogTitle.value = t('action.' + type) + actionType.value = type + dialogVisible.value = true } -/** 搜索按钮操作 */ -const handleQuery = () => { - queryParams.pageNo = 1 - getList() +// 新增操作 +const handleCreate = () => { + setDialogTile('create') } -/** 重置按钮操作 */ -const resetQuery = () => { - queryFormRef.value.resetFields() - handleQuery() +// 修改操作 +const handleUpdate = async (rowId: number) => { + setDialogTile('update') + // 设置数据 + const res = await NoticeApi.getNoticeApi(rowId) + unref(formRef)?.setValues(res) } -/** 添加/修改操作 */ -const modalRef = ref() -const openModal = (type: string, id?: number) => { - modalRef.value.openModal(type, id) +// 详情操作 +const handleDetail = async (rowId: number) => { + setDialogTile('detail') + // 设置数据 + const res = await NoticeApi.getNoticeApi(rowId) + detailData.value = res } -/** 删除按钮操作 */ -const handleDelete = async (id: number) => { - try { - // 删除的二次确认 - await message.delConfirm() - // 发起删除 - await NoticeApi.deleteNotice(id) - message.success(t('common.delSuccess')) - // 刷新列表 - await getList() - } catch {} +// 提交新增/修改的表单 +const submitForm = async () => { + const elForm = unref(formRef)?.getElFormRef() + if (!elForm) return + elForm.validate(async (valid) => { + if (valid) { + actionLoading.value = true + // 提交请求 + try { + const data = unref(formRef)?.formModel as NoticeApi.NoticeVO + if (actionType.value === 'create') { + await NoticeApi.createNoticeApi(data) + message.success(t('common.createSuccess')) + } else { + await NoticeApi.updateNoticeApi(data) + message.success(t('common.updateSuccess')) + } + dialogVisible.value = false + } finally { + actionLoading.value = false + await reload() + } + } + }) } - -/** 初始化 **/ -onMounted(() => { - getList() -}) </script> diff --git a/src/views/system/notice/notice.data.ts b/src/views/system/notice/notice.data.ts new file mode 100644 index 00000000..7a72a723 --- /dev/null +++ b/src/views/system/notice/notice.data.ts @@ -0,0 +1,59 @@ +import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas' +const { t } = useI18n() // 国际化 + +// 表单校验 +export const rules = reactive({ + title: [required], + type: [required] +}) + +// CrudSchema +const crudSchemas = reactive<VxeCrudSchema>({ + primaryKey: 'id', + primaryType: 'seq', + action: true, + columns: [ + { + title: '公告标题', + field: 'title', + isSearch: true + }, + { + title: '公告类型', + field: 'type', + dictType: DICT_TYPE.SYSTEM_NOTICE_TYPE, + dictClass: 'number' + }, + { + title: t('common.status'), + field: 'status', + dictType: DICT_TYPE.COMMON_STATUS, + dictClass: 'number', + isSearch: true + }, + { + title: '公告内容', + field: 'content', + table: { + type: 'html' + }, + form: { + component: 'Editor', + colProps: { + span: 24 + }, + componentProps: { + valueHtml: '' + } + }, + isTable: false + }, + { + title: t('common.createTime'), + field: 'createTime', + formatter: 'formatDate', + isForm: false + } + ] +}) +export const { allSchemas } = useVxeCrudSchemas(crudSchemas) diff --git a/src/views/system/user/user.data.ts b/src/views/system/user/user.data.ts index 7f7384eb..2ea4d268 100644 --- a/src/views/system/user/user.data.ts +++ b/src/views/system/user/user.data.ts @@ -28,7 +28,7 @@ export const rules = reactive({ } ], status: [required], - postIds: [{ required: true, message: '请选择岗位', trigger: ['blur', 'change'] }], + postIds: [required], mobile: [ required, { @@ -86,11 +86,6 @@ const crudSchemas = reactive<VxeCrudSchema>({ field: 'deptId', isTable: false }, - { - title: '岗位', - field: 'postIds', - isTable: false - }, { title: t('common.status'), field: 'status', @@ -103,6 +98,11 @@ const crudSchemas = reactive<VxeCrudSchema>({ } } }, + { + title: '岗位', + field: 'postIds', + isTable: false + }, { title: '最后登录时间', field: 'loginDate',