From dd763019ba79f940ce448a1d20c1e0f029c5716f Mon Sep 17 00:00:00 2001 From: Siyu Kong <boil@vip.qq.com> Date: Mon, 27 Mar 2023 11:46:33 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E4=BB=BB=E5=8A=A1=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.修改crud写法 2.修复dropdown组件与v-hasPermi冲突问题 3.优化详情页后续执行时间 --- src/views/infra/job/JobLog.vue | 237 ++++++++++---- src/views/infra/job/JobLogView.vue | 74 +++++ src/views/infra/job/form.vue | 172 ++++++++++ src/views/infra/job/index.vue | 490 ++++++++++++++--------------- src/views/infra/job/job.data.ts | 69 ---- src/views/infra/job/jobLog.data.ts | 75 ----- src/views/infra/job/utils.ts | 44 +++ src/views/infra/job/view.vue | 89 ++++++ 8 files changed, 792 insertions(+), 458 deletions(-) create mode 100644 src/views/infra/job/JobLogView.vue create mode 100644 src/views/infra/job/form.vue delete mode 100644 src/views/infra/job/job.data.ts delete mode 100644 src/views/infra/job/jobLog.data.ts create mode 100644 src/views/infra/job/utils.ts create mode 100644 src/views/infra/job/view.vue diff --git a/src/views/infra/job/JobLog.vue b/src/views/infra/job/JobLog.vue index 1bf9d745..daa20046 100644 --- a/src/views/infra/job/JobLog.vue +++ b/src/views/infra/job/JobLog.vue @@ -1,78 +1,179 @@ <template> - <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <template #toolbar_buttons> - <XButton - type="warning" - preIcon="ep:download" - :title="t('action.export')" + <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" v-hasPermi="['infra:job:export']" - @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> + > + <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" /> </template> + <script setup lang="ts" name="JobLog"> -import dayjs from 'dayjs' - +import { DICT_TYPE, getDictOptions } from '@/utils/dict' +import download from '@/utils/download' +import LogView from './JobLogView.vue' import * as JobLogApi from '@/api/infra/jobLog' -import { allSchemas } from './jobLog.data' +import { parseTime } from './utils' -const { t } = useI18n() // 国际化 -// 列表相关的变量 -const [registerTable, { exportList }] = useXTable({ - allSchemas: allSchemas, - getListApi: JobLogApi.getJobLogPageApi, - exportListApi: JobLogApi.exportJobLogApi +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 }) -// ========== CRUD 相关 ========== -const dialogVisible = ref(false) // 是否显示弹出层 -const dialogTitle = ref('') // 弹出层标题 +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(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 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 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 new file mode 100644 index 00000000..c66e0d80 --- /dev/null +++ b/src/views/infra/job/JobLogView.vue @@ -0,0 +1,74 @@ +<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 new file mode 100644 index 00000000..24488fd7 --- /dev/null +++ b/src/views/infra/job/form.vue @@ -0,0 +1,172 @@ +<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 613f84c4..702b31fe 100644 --- a/src/views/infra/job/index.vue +++ b/src/views/infra/job/index.vue @@ -1,243 +1,175 @@ <template> - <ContentWrap> - <!-- 列表 --> - <XTable @register="registerTable"> - <template #toolbar_buttons> - <!-- 操作:新增 --> - <XButton + <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 type="primary" - preIcon="ep:zoom-in" - :title="t('action.add')" + plain + @click="openModal('create')" v-hasPermi="['infra:job:create']" - @click="handleCreate()" - /> - <!-- 操作:导出 --> - <XButton - type="warning" - preIcon="ep:download" - :title="t('action.export')" + > + <Icon icon="ep:plus" class="mr-5px" /> 新增 + </el-button> + <el-button + type="success" + plain + @click="handleExport" + :loading="exportLoading" v-hasPermi="['infra:job:export']" - @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> + > + <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" /> </template> + <script setup lang="ts" name="Job"> -import type { FormExpose } from '@/components/Form' +import { DICT_TYPE, getDictOptions } from '@/utils/dict' +import JobForm from './form.vue' +import JobView from './view.vue' +import download from '@/utils/download' 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 [registerTable, { reload, deleteData, exportList }] = useXTable({ - allSchemas: allSchemas, - getListApi: JobApi.getJobPageApi, - deleteApi: JobApi.deleteJobApi, - exportListApi: JobApi.exportJobApi +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 queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(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 getList = async () => { + loading.value = true + try { + const data = await JobApi.getJobPageApi(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 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 @@ -249,7 +181,7 @@ const handleChangeStatus = async (row: JobApi.JobVO) => { : InfraJobStatusEnum.STOP await JobApi.updateJobStatusApi(row.id, status) message.success(text + '成功') - await reload() + await getList() }) .catch(() => { row.status = @@ -258,6 +190,43 @@ 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) { @@ -271,32 +240,61 @@ const handleRun = (row: JobApi.JobVO) => { message.confirm('确认要立即执行一次' + row.name + '?', t('common.reminder')).then(async () => { await JobApi.runJobApi(row.id) message.success('执行成功') - await reload() + await getList() }) } -// 提交按钮 -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 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 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 deleted file mode 100644 index 38761cd7..00000000 --- a/src/views/infra/job/job.data.ts +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index ccca3d08..00000000 --- a/src/views/infra/job/jobLog.data.ts +++ /dev/null @@ -1,75 +0,0 @@ -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 new file mode 100644 index 00000000..a3774f22 --- /dev/null +++ b/src/views/infra/job/utils.ts @@ -0,0 +1,44 @@ +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 new file mode 100644 index 00000000..d195e0e3 --- /dev/null +++ b/src/views/infra/job/view.vue @@ -0,0 +1,89 @@ +<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>