feat: 新增任务调度模块

SnailJob(灵活,可靠和快速的分布式任务重试和分布式任务调度平台)
This commit is contained in:
KAI
2024-07-18 07:22:09 +00:00
committed by Charles7c
parent 137f3a9684
commit e8c1d4b69b
21 changed files with 3166 additions and 1338 deletions

View File

@@ -0,0 +1,344 @@
<template>
<a-modal
v-model:visible="visible"
:title="title"
:mask-closable="false"
:esc-to-close="false"
:modal-style="{ maxWidth: '700px' }"
:body-style="{ maxHeight: width >= 700 ? '76vh' : '100vh' }"
:width="width >= 700 ? '90%' : '100%'"
@before-ok="save"
@close="reset"
>
<a-form ref="formRef" :model="form" :rules="rules" size="large" auto-label-width :layout="width >= 700 ? 'horizontal' : 'vertical'">
<fieldset>
<legend>基础配置</legend>
<a-row>
<a-col v-bind="colProps">
<a-form-item label="任务组" field="groupName">
<a-select v-model="form.groupName" placeholder="请选择任务组" :options="groupList" />
</a-form-item>
</a-col>
<a-col v-bind="colProps">
<a-form-item label="任务名称" field="jobName">
<a-input v-model.trim="form.jobName" placeholder="请输入任务名称" :max-length="64" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="描述" field="description">
<a-textarea
v-model.trim="form.description"
placeholder="请输入描述"
show-word-limit
:max-length="200"
:auto-size="{ minRows: 3, maxRows: 5 }"
/>
</a-form-item>
</fieldset>
<fieldset>
<legend>调度配置</legend>
<a-row>
<a-col v-bind="colProps">
<a-form-item label="触发类型" field="triggerType">
<a-select
v-model="form.triggerType"
placeholder="请选择触发类型"
:options="job_trigger_type_enum"
@change="triggerTypeChange"
/>
</a-form-item>
</a-col>
<a-col v-bind="colProps">
<a-form-item :label="form.triggerType === 2 ? '间隔时长' : 'CRON表达式'" field="triggerInterval">
<a-input-number
v-if="form.triggerType === 2"
v-model="triggerIntervalNumber"
placeholder="请输入间隔时长"
:min="1"
>
<template #suffix>秒</template>
</a-input-number>
<a-input
v-else
v-model="form.triggerInterval"
placeholder="请输入CRON表达式"
/>
</a-form-item>
</a-col>
</a-row>
</fieldset>
<fieldset>
<legend>任务配置</legend>
<a-row>
<a-col v-bind="colProps">
<a-form-item label="任务类型" field="taskType">
<a-select v-model="form.taskType" :options="job_task_type_enum" placeholder="请选择任务类型" />
</a-form-item>
</a-col>
<a-col v-bind="colProps">
<a-form-item label="执行器名称" field="executorInfo">
<a-input v-model.trim="form.executorInfo" placeholder="请输入执行器名称" :max-length="255" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="任务参数" field="argsStr">
<a-textarea
v-if="form.taskType !== 3"
v-model.trim="form.argsStr"
placeholder="请输入任务参数"
:auto-size="{ minRows: 3, maxRows: 5 }"
/>
<div v-else class="args-container">
<div v-for="(item, index) in args" :key="index" class="args-item">
<a-form-item hide-label :rules="[{ required: true, message: '请输入分片参数' }]">
<a-input v-model="item.value" :placeholder="`请输入分片参数 ${index + 1}`" />
</a-form-item>
<a-button status="danger" class="args-delete-button" @click="onDeleteArgs(index)">
<template #icon>
<icon-delete />
</template>
</a-button>
</div>
<a-button type="outline" class="add-button" style="width: 100%;" @click="onAddArgs">
<template #icon>
<icon-plus />
</template>
</a-button>
</div>
</a-form-item>
</fieldset>
<fieldset>
<legend>高级配置</legend>
<a-row>
<a-col v-bind="colProps">
<a-form-item label="路由策略" field="routeKey">
<a-select v-model.trim="form.routeKey" placeholder="请选择路由策略" :options="job_route_strategy_enum" />
</a-form-item>
</a-col>
<a-col v-bind="colProps">
<a-form-item label="阻塞策略" field="blockStrategy">
<a-select v-model.trim="form.blockStrategy" placeholder="请选择阻塞策略" :options="job_block_strategy_enum" />
</a-form-item>
</a-col>
<a-col v-bind="colProps">
<a-form-item label="超时时间" field="executorTimeout">
<a-input-number v-model.trim="form.executorTimeout" placeholder="请输入超时时间" :min="1">
<template #suffix>秒</template>
</a-input-number>
</a-form-item>
</a-col>
<a-col v-bind="colProps">
<a-form-item label="最大重试次数" field="maxRetryTimes">
<a-input-number v-model="form.maxRetryTimes" placeholder="请输入最大重试次数" :min="0">
</a-input-number>
</a-form-item>
</a-col>
<a-col v-bind="colProps">
<a-form-item label="重试间隔" field="retryInterval">
<a-input-number v-model.trim="form.retryInterval" placeholder="请输入重试间隔" :min="1">
<template #suffix>
</template>
</a-input-number>
</a-form-item>
</a-col>
<a-col v-bind="colProps">
<a-form-item label="并行数" field="parallelNum">
<a-input-number v-model="form.parallelNum" placeholder="请输入并行数" :min="1" />
</a-form-item>
</a-col>
</a-row>
</fieldset>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { type ColProps, type FormInstance, Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addJob, listGroup, updateJob } from '@/apis'
import { useForm } from '@/hooks'
import { useDict } from '@/hooks/app'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const colProps: ColProps = { xs: 24, sm: 24, md: 12, lg: 12, xl: 12, xxl: 12 }
const { width } = useWindowSize()
const { job_trigger_type_enum, job_task_type_enum, job_route_strategy_enum, job_block_strategy_enum } = useDict(
'job_trigger_type_enum',
'job_task_type_enum',
'job_route_strategy_enum',
'job_block_strategy_enum'
)
const dataId = ref()
const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改任务' : '新增任务'))
const formRef = ref<FormInstance>()
const rules: FormInstance['rules'] = {
groupName: [{ required: true, message: '请选择任务组' }],
jobName: [{ required: true, message: '请输入任务名称' }],
triggerType: [{ required: true, message: '请选择触发类型' }],
triggerInterval: [{ required: true, message: '请输入间隔时长' }],
taskType: [{ required: true, message: '请选择任务类型' }],
executorInfo: [{ required: true, message: '请输入执行器名称' }],
routeKey: [{ required: true, message: '请选择路由策略' }],
blockStrategy: [{ required: true, message: '请选择阻塞策略' }],
executorTimeout: [{ required: true, message: '请输入超时时间' }],
maxRetryTimes: [{ required: true, message: '请输入最大重试次数' }],
retryInterval: [{ required: true, message: '请输入重试间隔' }],
parallelNum: [{ required: true, message: '请输入并行数' }]
}
const { form, resetForm } = useForm({
triggerType: 2,
triggerInterval: 60,
taskType: 1,
routeKey: 4,
blockStrategy: 1,
executorTimeout: 60,
maxRetryTimes: 3,
retryInterval: 1,
parallelNum: 1
})
const args = ref<any[]>([])
// 重置
const reset = () => {
formRef.value?.resetFields()
args.value = [{ value: '' }]
resetForm()
}
const groupList = ref()
// 查询任务组列表
const getGroupList = async () => {
const { data } = await listGroup()
groupList.value = data?.map((item: string) => ({
label: item,
value: item
}))
}
const visible = ref(false)
// 新增
const onAdd = () => {
reset()
getGroupList()
dataId.value = undefined
visible.value = true
}
// 修改
const onUpdate = async (record: any) => {
await getGroupList()
reset()
dataId.value = record.id
Object.assign(form, record)
// 切片任务,解析 argsStr 并赋值给 args
if (form.taskType === 3 && form.argsStr) {
try {
const parsedArgs = JSON.parse(form.argsStr)
args.value = parsedArgs.map((arg: any) => ({ value: arg }))
} catch (error: any) {
Message.error(error)
}
}
visible.value = true
}
// 保存
const save = async () => {
try {
// 切片任务,将参数转换为 JSON 数组
if (form.taskType === 3) {
form.argsStr = JSON.stringify(args.value.map((arg) => arg.value))
}
const isInvalid = await formRef.value?.validate()
if (isInvalid) return false
if (isUpdate.value) {
await updateJob(form, dataId.value)
Message.success('修改成功')
} else {
await addJob(form)
Message.success('新增成功')
}
emit('save-success')
return true
} catch (error) {
return false
}
}
// 触发类型切换
const triggerTypeChange = () => {
switch (form.triggerType) {
case 2:
form.triggerInterval = 60
break
case 3:
form.triggerInterval = ''
break
}
}
// 间隔时长
const triggerIntervalNumber = computed({
get() {
return Number(form.triggerInterval)
},
set(newValue) {
form.triggerInterval = newValue.toString()
}
})
// 新增切片参数
const onAddArgs = () => {
args.value.push({ value: '' })
}
// 删除切片参数
const onDeleteArgs = (index) => {
args.value.splice(index, 1)
}
defineExpose({ onAdd, onUpdate })
</script>
<style scoped lang="scss">
fieldset {
padding: 15px 15px 0 15px;
margin-bottom: 15px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
fieldset legend {
color: rgb(var(--gray-10));
padding: 2px 5px 2px 5px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
.args-container {
display: flex;
flex-direction: column;
width: 100%;
}
.args-item {
display: flex;
align-items: center;
button {
margin-bottom: 20px;
}
}
.args-item > *:not(:last-child) {
margin-right: 10px;
}
.add-button {
align-self: flex-start;
width: 100px;
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<a-drawer v-model:visible="visible" title="任务详情" :width="width >= 600 ? 600 : '100%'" :footer="false">
<a-descriptions :column="2" size="large" class="general-description">
<a-descriptions-item label="ID" :span="2">
<a-typography-paragraph copyable>{{ dataDetail?.id }}</a-typography-paragraph>
</a-descriptions-item>
<a-descriptions-item label="任务组">{{ dataDetail?.groupName }}</a-descriptions-item>
<a-descriptions-item label="任务名称">{{ dataDetail?.jobName }}</a-descriptions-item>
<a-descriptions-item label="触发类型">
<GiCellTag :value="dataDetail?.triggerType" :dict="job_trigger_type_enum" />
</a-descriptions-item>
<a-descriptions-item v-if="dataDetail?.triggerType === 1" label="CRON">{{ dataDetail?.triggerInterval }}</a-descriptions-item>
<a-descriptions-item v-else-if="dataDetail?.triggerType === 2" label="间隔时长">{{ dataDetail?.triggerInterval }} </a-descriptions-item>
<a-descriptions-item label="任务类型">
<GiCellTag :value="dataDetail?.taskType" :dict="job_task_type_enum" />
</a-descriptions-item>
<a-descriptions-item label="执行器名称">{{ dataDetail?.executorInfo }}</a-descriptions-item>
<a-descriptions-item label="任务参数">{{ dataDetail?.argsStr }}</a-descriptions-item>
<a-descriptions-item label="路由策略">
<GiCellTag :value="dataDetail?.routeKey" :dict="job_route_strategy_enum" />
</a-descriptions-item>
<a-descriptions-item label="阻塞策略">
<GiCellTag :value="dataDetail?.blockStrategy" :dict="job_block_strategy_enum" />
</a-descriptions-item>
<a-descriptions-item label="超时时间">{{ dataDetail?.executorTimeout }} </a-descriptions-item>
<a-descriptions-item label="最大重试次数">{{ dataDetail?.maxRetryTimes }}</a-descriptions-item>
<a-descriptions-item label="重试间隔">{{ dataDetail?.retryInterval }} </a-descriptions-item>
<a-descriptions-item label="并行数">{{ dataDetail?.parallelNum }}</a-descriptions-item>
<a-descriptions-item label="任务状态">
<GiCellTag :value="dataDetail?.jobStatus" :dict="job_status_enum" />
</a-descriptions-item>
<a-descriptions-item label="描述" :span="2">{{ dataDetail?.description }}</a-descriptions-item>
</a-descriptions>
</a-drawer>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import type { JobResp } from '@/apis'
import { useDict } from '@/hooks/app'
const { width } = useWindowSize()
const { job_status_enum, job_trigger_type_enum, job_task_type_enum, job_route_strategy_enum, job_block_strategy_enum } = useDict(
'job_status_enum',
'job_trigger_type_enum',
'job_task_type_enum',
'job_route_strategy_enum',
'job_block_strategy_enum'
)
const visible = ref(false)
const dataDetail = ref<JobResp>()
// 详情
const onDetail = (record: JobResp) => {
dataDetail.value = record
visible.value = true
}
defineExpose({ onDetail })
</script>

View File

@@ -0,0 +1,195 @@
<template>
<div class="table-page">
<GiTable
title="任务管理"
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['name']"
@refresh="search"
>
<template #custom-left>
<a-select
v-model="queryForm.groupName"
placeholder="请选择任务组"
:options="groupList"
style="width: 200px"
@change="search"
/>
<a-input v-model="queryForm.jobName" placeholder="请输入任务名称" allow-clear @change="search" />
<a-select v-model="queryForm.jobStatus" placeholder="请选择任务状态" :options="job_status_enum" allow-clear style="width: 150px" @change="search" />
<a-button @click="reset">重置</a-button>
</template>
<template #custom-right>
<a-button v-permission="['schedule:job:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<span>新增</span>
</a-button>
</template>
<template #jobName="{ record }">
<a-link @click="onDetail(record)">{{ record.jobName }}</a-link>
</template>
<template #triggerType="{ record }">
<GiCellTag :value="record.triggerType" :dict="job_trigger_type_enum" />:&nbsp;
<span v-if="record.triggerType === 2">{{ record.triggerInterval }} </span>
<span v-else>{{ record.triggerInterval }}</span>
</template>
<template #taskType="{ record }">
<GiCellTag :value="record.taskType" :dict="job_task_type_enum" />
{{ record.executorInfo }}
</template>
<template #jobStatus="{ record }">
<a-switch
v-model="record.jobStatus"
:checked-value="1"
:unchecked-value="0"
:disabled="!has.hasPerm('tool:job:update')"
@change="onUpdateStatus(record)"
/>
</template>
<template #action="{ record }">
<a-space>
<a-link @click="onLog(record)">日志</a-link>
<a-popconfirm content="是否确定立即执行一次任务?" type="warning" @ok="onTrigger(record)">
<a-link v-permission="['schedule:job:trigger']">执行</a-link>
</a-popconfirm>
<a-link v-permission="['schedule:job:update']" @click="onUpdate(record)">修改</a-link>
<a-link v-permission="['schedule:job:delete']" status="danger" @click="onDelete(record)">删除</a-link>
</a-space>
</template>
</GiTable>
<JobAddModal ref="JobAddModalRef" @save-success="reset" />
<JobDetailDrawer ref="JobDetailDrawerRef" />
</div>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { useRouter } from 'vue-router'
import JobAddModal from './JobAddModal.vue'
import JobDetailDrawer from './JobDetailDrawer.vue'
import { type JobQuery, type JobResp, deleteJob, listGroup, listJob, triggerJob, updateJobStatus } from '@/apis'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
import { isMobile } from '@/utils'
import has from '@/utils/has'
defineOptions({ name: 'ScheduleJob' })
const { job_status_enum, job_trigger_type_enum, job_task_type_enum } = useDict('job_status_enum', 'job_trigger_type_enum', 'job_task_type_enum')
const queryForm = reactive<JobQuery>({
groupName: ''
})
const {
tableData: dataList,
loading,
pagination,
search,
handleDelete
} = useTable((page) => listJob({ ...queryForm, ...page }), { immediate: false })
const columns: TableInstanceColumns[] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize)
},
{ title: '任务名称', dataIndex: 'jobName', slotName: 'jobName', width: 100, ellipsis: true, tooltip: true },
{ title: '调度类型', dataIndex: 'triggerType', slotName: 'triggerType', width: 130 },
{ title: '任务类型', dataIndex: 'taskType', slotName: 'taskType', width: 130, ellipsis: true, tooltip: true },
{ title: '状态', dataIndex: 'jobStatus', width: 60, align: 'center', slotName: 'jobStatus' },
{ title: '描述', dataIndex: 'description', width: 130, ellipsis: true, tooltip: true },
{ title: '创建时间', dataIndex: 'createDt', width: 180 },
{ title: '修改时间', dataIndex: 'updateDt', width: 180, show: false },
{
title: '操作',
slotName: 'action',
width: 130,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['schedule:job:trigger', 'schedule:job:update', 'schedule:job:delete'])
}
]
const groupList = ref()
// 查询任务组列表
const getGroupList = async () => {
const { data } = await listGroup()
groupList.value = data?.map((item: string) => ({
label: item,
value: item
}))
queryForm.groupName = groupList.value[0].label
search()
}
// 重置
const reset = () => {
queryForm.jobName = undefined
queryForm.jobStatus = undefined
search()
}
// 删除
const onDelete = (record: JobResp) => {
return handleDelete(() => deleteJob(record.id), {
content: `是否确定删除任务 [${record.jobName}]`,
showModal: true
})
}
// 修改状态
const onUpdateStatus = (record: JobResp) => {
const msg = record.jobStatus === 1 ? '启用成功' : '禁用成功'
updateJobStatus({ jobStatus: record.jobStatus }, record.id)
.then(() => {
Message.success(msg)
}).catch(() => {
record.jobStatus = record.jobStatus === 1 ? 0 : 1
})
}
// 执行
const onTrigger = (record: JobResp) => {
triggerJob(record.id).then(() => {
Message.success('执行请求已下发')
})
}
const JobAddModalRef = ref<InstanceType<typeof JobAddModal>>()
// 新增
const onAdd = () => {
JobAddModalRef.value?.onAdd()
}
// 修改
const onUpdate = (record: JobResp) => {
JobAddModalRef.value?.onUpdate(record)
}
const JobDetailDrawerRef = ref<InstanceType<typeof JobDetailDrawer>>()
// 详情
const onDetail = (record: JobResp) => {
JobDetailDrawerRef.value?.onDetail(record)
}
const router = useRouter()
// 日志
const onLog = (record: JobResp) => {
router.push({ path: '/schedule/log', query: { jobId: record.id, jobName: record.jobName, groupName: record.groupName } })
}
onMounted(() => {
getGroupList()
})
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,79 @@
<template>
<a-modal
v-model:visible="visible"
title="任务日志详情"
:width="width >= 1500 ? 1500 : '100%'"
:footer="false"
>
<a-layout style="height: 500px">
<a-layout-sider :resize-directions="['right']">
<a-tabs size="large" position="left">
<a-tab-pane v-for="item in dataList" :key="item.id">
<template #title>
<span @click="onLogDetail(item)">{{ item.clientInfo.split('@')[1] }}</span>
</template>
</a-tab-pane>
</a-tabs>
</a-layout-sider>
<a-layout-content>
<GiCodeView :code-json="content" />
</a-layout-content>
</a-layout>
</a-modal>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import { type JobInstanceQuery, type JobInstanceResp, type JobLogResp, listJobInstance, listJobInstanceLog } from '@/apis'
import dayjs from "dayjs";
const { width } = useWindowSize()
const queryForm = reactive<JobInstanceQuery>({})
const dataList = ref<JobInstanceResp[]>([])
const loading = ref(false)
// 查询列表数据
const getInstanceList = async (query: JobInstanceQuery = { ...queryForm }) => {
try {
loading.value = true
const res = await listJobInstance(query)
dataList.value = res.data
} finally {
loading.value = false
}
}
const visible = ref(false)
// 详情
const onDetail = (record: JobLogResp) => {
visible.value = true
// 更新 queryForm
queryForm.jobId = record.jobId
queryForm.taskBatchId = record.id
getInstanceList()
}
// 格式化日志
const formatLog = (log: any) => {
const date = new Date(Number.parseInt(log.time_stamp))
return `${dayjs(date).format('YYYY-MM-DD HH:mm:ss')} ${log.level} [${log.thread}] ${log.location} - ${log.message}`
}
const content = ref('')
// 日志输出
const onLogDetail = async (record: JobInstanceResp) => {
// todo startId根据第一次查询 如果有返回!=0则需要在查一次
const res = await listJobInstanceLog({
taskBatchId: record.taskBatchId,
jobId: record.jobId,
taskId: record.id,
startId: 0,
fromIndex: 0,
size: 50
})
content.value = res.data.message.map(formatLog).join('\n')
}
defineExpose({ onDetail })
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,164 @@
<template>
<div class="table-page">
<GiTable
title="任务日志"
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }"
:pagination="pagination"
:disabled-tools="['size']"
@refresh="search"
>
<template #custom-left>
<a-select
v-model="queryForm.groupName"
placeholder="请选择任务组"
:options="groupList"
style="width: 200px"
@change="search"
/>
<a-input v-model="queryForm.jobName" placeholder="请输入任务名称" allow-clear @change="search" />
<a-select
v-model="queryForm.taskBatchStatus"
placeholder="请选择状态"
:options="job_execute_status_enum"
allow-clear
style="width: 150px"
@change="search"
/>
<DateRangePicker v-model="queryForm.datetimeRange" @change="search" />
<a-button @click="reset">重置</a-button>
</template>
<template #taskBatchStatus="{ record }">
<GiCellTag :value="record.taskBatchStatus" :dict="job_execute_status_enum" />
</template>
<template #operationReason="{ record }">
<GiCellTag :value="record.operationReason" :dict="job_execute_reason_enum" />
</template>
<template #action="{ record }">
<a-space>
<a-link @click="onDetail(record)">详情</a-link>
<a-popconfirm content="是否确定停止本次执行?" type="warning" @ok="onStop(record)">
<a-link v-if="record.taskBatchStatus === 2" v-permission="['schedule:log:stop']" status="danger">停止</a-link>
</a-popconfirm>
<a-popconfirm content="是否确定重试本次执行?" type="warning" @ok="onRetry(record)">
<a-link
v-if="record.taskBatchStatus === 4 || record.taskBatchStatus === 5 || record.taskBatchStatus === 6"
v-permission="['schedule:log:retry']"
status="danger"
>
重试
</a-link>
</a-popconfirm>
</a-space>
</template>
</GiTable>
<JobLogDetailModal ref="JobLogDetailModalRef" />
</div>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { useRoute } from 'vue-router'
import dayjs from 'dayjs'
import JobLogDetailModal from './LogDetailModal.vue'
import { type JobLogQuery, type JobLogResp, listGroup, listJobLog, retryJob, stopJob } from '@/apis'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
import { isMobile } from '@/utils'
import has from '@/utils/has'
defineOptions({ name: 'ScheduleLog' })
const { job_execute_reason_enum, job_execute_status_enum } = useDict('job_execute_reason_enum', 'job_execute_status_enum')
const queryForm = reactive<JobLogQuery>({
datetimeRange: [
dayjs().subtract(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss')
]
})
const {
tableData: dataList,
pagination,
loading,
search
} = useTable((page) => listJobLog({ ...queryForm, ...page }), { immediate: false })
const columns: TableInstanceColumns[] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize)
},
{ title: '任务组', dataIndex: 'groupName', width: 80, ellipsis: true, tooltip: true },
{ title: '任务名称', dataIndex: 'jobName', width: 80, ellipsis: true, tooltip: true },
{ title: '调度时间', dataIndex: 'createDt', width: 80 },
{ title: '执行状态', dataIndex: 'taskBatchStatus', slotName: 'taskBatchStatus', width: 50, align: 'center' },
{ title: '执行备注', dataIndex: 'operationReason', slotName: 'operationReason', width: 80, ellipsis: true, tooltip: true },
{ title: '执行时间', dataIndex: 'executionAt', width: 80 },
{
title: '操作',
slotName: 'action',
width: 60,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['schedule:log:stop', 'schedule:log:retry'])
}
]
const groupList = ref()
// 查询任务组列表
const getGroupList = async () => {
const { data } = await listGroup()
groupList.value = data?.map((item: string) => ({
label: item,
value: item
}))
}
// 重置
const reset = () => {
queryForm.taskBatchStatus = undefined
queryForm.datetimeRange = undefined
search()
}
// 停止
const onStop = (record: JobLogResp) => {
stopJob(record.id).then(() => {
Message.success('停止成功')
})
}
// 重试
const onRetry = (record: JobLogResp) => {
retryJob(record.id).then(() => {
Message.success('重试成功')
})
}
const JobLogDetailModalRef = ref<InstanceType<typeof JobLogDetailModal>>()
// 查看日志详情
const onDetail = (record: JobLogResp) => {
JobLogDetailModalRef.value?.onDetail(record)
}
const route = useRoute()
onMounted(() => {
if (route.query) {
queryForm.jobId = route.query.jobId ? Number.parseInt(route.query.jobId as string, 10) : undefined
queryForm.groupName = route.query.groupName ? route.query.groupName : undefined
queryForm.jobName = route.query.jobName ? route.query.jobName : undefined
}
getGroupList()
search()
})
</script>
<style scoped lang="scss"></style>