Files
smart_grid_iot/client/src/views/system/job/index.vue
T
andyesfly 22d27a940d feat: 重构勘察业务为明细模式并新增定时任务人性化配置
- 重构勘察业务:移除SOP相关表,新增勘察明细表支持建筑/楼层/房间/照片管理
- 新增定时任务人性化频率配置,支持分钟/小时/天/周/月/自定义六种模式
- 新增频率预览接口,支持配置转换为cron表达式与执行时间预览
- 更新系统品牌名称为鑫潼智电,修改全局标题与登录页文案
2026-10-17 10:08:42 +08:00

996 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="job-management">
<a-card :bordered="false" class="search-card">
<a-form layout="inline" :model="queryParams">
<a-form-item label="任务名称">
<a-input
v-model:value="queryParams.jobName"
placeholder="请输入任务名称"
allow-clear
style="width: 180px"
@pressEnter="handleSearch"
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="queryParams.status"
placeholder="全部"
allow-clear
style="width: 120px"
>
<a-select-option :value="1">启用</a-select-option>
<a-select-option :value="0">停用</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="handleReset">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
<a-card :bordered="false" class="table-card">
<div class="table-wrapper" ref="wrapperRef">
<a-table
:columns="columns"
:data-source="jobList"
:loading="tableLoading"
:pagination="paginationConfig"
row-key="id"
:scroll="{ x: 'max-content', y: tableHeight }"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'jobName'">
<span>{{ record.jobName }}</span>
<a-tag v-if="record.registered === false" color="red" style="margin-left: 6px">未注册</a-tag>
</template>
<template v-else-if="column.key === 'frequency'">
<div class="freq-cell">
<span>{{ record.cronDescription || record.cronExpression }}</span>
<code class="freq-cron">{{ record.cronExpression }}</code>
</div>
</template>
<template v-else-if="column.key === 'params'">
<a-tooltip :title="formatParams(record.params, true)">
<span class="ellipsis-text">{{ formatParams(record.params) }}</span>
</a-tooltip>
</template>
<template v-else-if="column.key === 'status'">
<a-switch
:checked="record.status === 1"
:loading="statusLoadingId === record.id"
checked-children="启用"
un-checked-children="停用"
@change="(checked: boolean) => handleStatusChange(record, checked)"
/>
</template>
<template v-else-if="column.key === 'lastRunAt'">
{{ formatDateTime(record.lastRunAt) }}
</template>
<template v-else-if="column.key === 'lastRunStatus'">
<a-tag v-if="record.lastRunStatus" :color="runStatusColor(record.lastRunStatus)">
{{ runStatusText(record.lastRunStatus) }}
</a-tag>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'lastRunCostMs'">
{{ record.lastRunCostMs === null || record.lastRunCostMs === undefined ? '-' : `${record.lastRunCostMs} ms` }}
</template>
<template v-else-if="column.key === 'nextRunTime'">
{{ formatDateTime(record.nextRunTime) }}
</template>
<template v-else-if="column.key === 'retry'">
{{ `${record.retryTimes ?? 0} 次 / ${record.retryInterval ?? 0} 秒` }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" :disabled="record.registered === false" @click="handleEdit(record)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-button type="link" size="small" :disabled="record.registered === false" @click="handleRun(record)">
<template #icon><PlayCircleOutlined /></template>
立即执行
</a-button>
<a-button type="link" size="small" @click="handleOpenLogs(record)">
<template #icon><ProfileOutlined /></template>
日志
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</a-card>
<a-modal
v-model:open="editVisible"
title="编辑任务"
:confirm-loading="saving"
:mask-closable="false"
width="720px"
@ok="handleSave"
@cancel="handleCancelEdit"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="任务名称">
<span>{{ currentJob?.jobName || '-' }}</span>
</a-form-item>
<a-divider orientation="left" class="form-divider">执行频率</a-divider>
<a-form-item label="频率类型">
<a-select v-model:value="formData.freqType" :options="freqTypeOptions" style="width: 100%" />
</a-form-item>
<a-form-item v-if="formData.freqType === 'minute'" label="间隔">
<a-input-number
v-model:value="formData.interval"
:min="1"
:max="59"
addon-after="分钟"
style="width: 100%"
/>
</a-form-item>
<template v-if="formData.freqType === 'hour'">
<a-form-item label="间隔">
<a-input-number
v-model:value="formData.interval"
:min="1"
:max="23"
addon-after="小时"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="第几分钟">
<a-input-number
v-model:value="formData.minute"
:min="0"
:max="59"
addon-after="分"
style="width: 100%"
/>
</a-form-item>
</template>
<a-form-item v-if="formData.freqType === 'week'" label="星期">
<a-checkbox-group v-model:value="formData.weekdays" :options="weekdayOptions" />
</a-form-item>
<a-form-item v-if="formData.freqType === 'month'" label="日期">
<a-select
v-model:value="formData.days"
mode="multiple"
:options="dayOptions"
:max-tag-count="6"
placeholder="请选择每月的执行日期"
style="width: 100%"
/>
</a-form-item>
<a-form-item v-if="hasTimeList" label="执行时间">
<div class="time-list">
<div v-for="(_, index) in formData.times" :key="index" class="time-row">
<a-time-picker v-model:value="formData.times[index]" format="HH:mm" :allow-clear="false" />
<a-button
type="text"
danger
:disabled="formData.times.length <= 1"
@click="removeTime(index)"
>
<template #icon><MinusCircleOutlined /></template>
</a-button>
</div>
<a-button type="dashed" block :disabled="formData.times.length >= 10" @click="addTime">
<template #icon><PlusOutlined /></template>
添加时间
</a-button>
</div>
</a-form-item>
<a-form-item v-if="formData.freqType === 'custom'" label="cron 表达式">
<a-input v-model:value="formData.expression" placeholder="如 0 3 * * *(分 时 日 月 周)" :maxlength="64" />
</a-form-item>
<a-form-item label="执行预览">
<a-spin :spinning="previewLoading" size="small">
<div class="freq-preview">
<a-alert v-if="preview.error" type="warning" :message="preview.error" show-icon />
<template v-else>
<div class="preview-line">
<span class="preview-label">表达式</span>
<code>{{ preview.cronExpression || '-' }}</code>
</div>
<div class="preview-line">
<span class="preview-label">说明</span>
<span>{{ preview.description || '-' }}</span>
</div>
<div class="preview-line">
<span class="preview-label">下次执行</span>
<span v-if="!preview.nextRuns.length">-</span>
<ol v-else class="next-list">
<li v-for="(time, index) in preview.nextRuns" :key="index">{{ formatDateTime(time) }}</li>
</ol>
</div>
</template>
</div>
</a-spin>
</a-form-item>
<a-divider v-if="paramFields.length" orientation="left" class="form-divider">任务参数</a-divider>
<a-form-item
v-for="field in paramFields"
:key="field.key"
:label="field.label"
:name="['params', field.key]"
:rules="getParamRules(field)"
>
<a-input-number
v-if="field.type === 'number'"
v-model:value="formData.params[field.key]"
:min="field.min"
:max="field.max"
:placeholder="field.placeholder || `请输入${field.label}`"
style="width: 100%"
/>
<a-input
v-else
v-model:value="formData.params[field.key]"
:placeholder="field.placeholder || `请输入${field.label}`"
/>
</a-form-item>
<a-divider orientation="left" class="form-divider">失败重试与备注</a-divider>
<a-form-item label="重试次数" name="retryTimes">
<a-input-number v-model:value="formData.retryTimes" :min="0" :max="5" style="width: 100%" />
</a-form-item>
<a-form-item label="重试间隔(秒)" name="retryInterval">
<a-input-number v-model:value="formData.retryInterval" :min="10" :max="3600" style="width: 100%" />
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea
v-model:value="formData.remark"
placeholder="请输入备注"
:maxlength="255"
show-count
:rows="3"
/>
</a-form-item>
</a-form>
</a-modal>
<a-drawer
v-model:open="logVisible"
:title="`执行日志 - ${currentJob?.jobName || ''}`"
width="900"
>
<div class="log-toolbar">
<a-select
v-model:value="logQuery.status"
placeholder="全部状态"
allow-clear
style="width: 140px"
@change="handleLogSearch"
>
<a-select-option :value="1">成功</a-select-option>
<a-select-option :value="2">失败</a-select-option>
<a-select-option :value="3">跳过</a-select-option>
</a-select>
<a-button danger @click="handleClearLogs">
<template #icon><DeleteOutlined /></template>
清理日志
</a-button>
</div>
<a-table
:columns="logColumns"
:data-source="logList"
:loading="logLoading"
:pagination="logPaginationConfig"
:expandable="logExpandable"
row-key="id"
size="small"
@change="handleLogTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'createdAt'">
{{ formatDateTime(record.createdAt) }}
</template>
<template v-else-if="column.key === 'triggerType'">
{{ triggerTypeText(record.triggerType) }}
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="runStatusColor(record.status)">{{ runStatusText(record.status) }}</a-tag>
</template>
<template v-else-if="column.key === 'costMs'">
{{ record.costMs === null || record.costMs === undefined ? '-' : `${record.costMs} ms` }}
</template>
<template v-else-if="column.key === 'message'">
<a-tooltip :title="record.message || ''">
<span class="ellipsis-text">{{ record.message || '-' }}</span>
</a-tooltip>
</template>
</template>
<template #expandedRowRender="{ record }">
<pre class="error-stack">{{ record.errorStack }}</pre>
</template>
</a-table>
</a-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { message, Modal } from 'ant-design-vue'
import {
SearchOutlined,
ReloadOutlined,
EditOutlined,
PlayCircleOutlined,
ProfileOutlined,
DeleteOutlined,
PlusOutlined,
MinusCircleOutlined,
} from '@ant-design/icons-vue'
import dayjs, { type Dayjs } from 'dayjs'
import {
jobApi,
type JobItem,
type JobParamField,
type JobLogItem,
type FreqType,
type FreqConfig,
} from '@/api/job'
import { useTableScroll } from '@/composables/useTableScroll'
const { wrapperRef, tableHeight } = useTableScroll()
const tableLoading = ref(false)
const saving = ref(false)
const statusLoadingId = ref<number | null>(null)
const jobList = ref<JobItem[]>([])
const total = ref(0)
const queryParams = reactive({
page: 1,
pageSize: 10,
jobName: '',
status: undefined as number | undefined,
})
const columns = [
{ title: '任务名称', dataIndex: 'jobName', key: 'jobName', width: 180 },
{ title: '任务键', dataIndex: 'jobKey', key: 'jobKey', width: 150, ellipsis: true },
{ title: '执行频率', key: 'frequency', width: 200 },
{ title: '参数', dataIndex: 'params', key: 'params', width: 180, ellipsis: true },
{ title: '状态', key: 'status', width: 100, align: 'center' as const },
{ title: '上次执行时间', dataIndex: 'lastRunAt', key: 'lastRunAt', width: 170 },
{ title: '上次状态', key: 'lastRunStatus', width: 100, align: 'center' as const },
{ title: '耗时', key: 'lastRunCostMs', width: 100 },
{ title: '下次执行时间', key: 'nextRunTime', width: 170 },
{ title: '重试配置', key: 'retry', width: 140 },
{ title: '操作', key: 'action', width: 220, fixed: 'right' as const },
]
const paginationConfig = reactive({
current: queryParams.page,
pageSize: queryParams.pageSize,
total: total.value,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
showTotal: (t: number) => `共 ${t} 条记录`,
})
const editVisible = ref(false)
const currentJob = ref<JobItem | null>(null)
const paramFields = ref<JobParamField[]>([])
const formRef = ref()
/** HH:mm 文本 → Dayjs(TimePicker 取值),不依赖 dayjs 自定义解析插件 */
function toTimeValue(text: string): Dayjs {
const [hour, minute] = text.split(':')
return dayjs().hour(Number(hour)).minute(Number(minute)).second(0).millisecond(0)
}
/** 人性化频率表单:字段按 freqType 取用,提交时由 buildFreqConfig 组装成频率配置 */
const formData = reactive({
freqType: 'day' as FreqType,
interval: 5,
minute: 0,
times: [toTimeValue('08:00')] as Dayjs[],
weekdays: [1] as number[],
days: [1] as number[],
expression: '',
params: {} as Record<string, any>,
retryTimes: 0,
retryInterval: 60,
remark: '',
})
const freqTypeOptions: { value: FreqType; label: string }[] = [
{ value: 'minute', label: '每 N 分钟' },
{ value: 'hour', label: '每 N 小时' },
{ value: 'day', label: '每天' },
{ value: 'week', label: '每周' },
{ value: 'month', label: '每月' },
{ value: 'custom', label: '自定义(cron 表达式)' },
]
const weekdayOptions = [
{ value: 1, label: '周一' },
{ value: 2, label: '周二' },
{ value: 3, label: '周三' },
{ value: 4, label: '周四' },
{ value: 5, label: '周五' },
{ value: 6, label: '周六' },
{ value: 0, label: '周日' },
]
const dayOptions = Array.from({ length: 31 }, (_, index) => ({
value: index + 1,
label: `${index + 1} 日`,
}))
/** day/week/month 需要执行时间点列表 */
const hasTimeList = computed(() => ['day', 'week', 'month'].includes(formData.freqType))
/** 执行预览状态(由后端计算 cron / 中文描述 / 未来执行时间) */
const previewLoading = ref(false)
const preview = reactive({
valid: true,
error: '',
cronExpression: '',
description: '',
nextRuns: [] as string[],
})
const formRules = {
retryTimes: [{ required: true, type: 'number' as const, message: '请输入重试次数', trigger: 'change' }],
retryInterval: [{ required: true, type: 'number' as const, message: '请输入重试间隔', trigger: 'change' }],
remark: [{ max: 255, message: '备注长度不能超过 255', trigger: 'blur' }],
}
/** 表单 → 频率配置(提交给后端的结构) */
function buildFreqConfig(): FreqConfig {
switch (formData.freqType) {
case 'minute':
return { interval: formData.interval }
case 'hour':
return { interval: formData.interval, minute: formData.minute }
case 'day':
return { times: timeTexts() }
case 'week':
return { weekdays: [...formData.weekdays].sort((a, b) => a - b), times: timeTexts() }
case 'month':
return { days: [...formData.days].sort((a, b) => a - b), times: timeTexts() }
default:
return { expression: formData.expression.trim() }
}
}
function timeTexts(): string[] {
return formData.times.map(item => item.format('HH:mm'))
}
/** 频率配置 → 表单(编辑回填) */
function applyFreqConfig(job: JobItem) {
const config: FreqConfig = job.freqConfig || {}
const type: FreqType = job.freqType || 'custom'
formData.freqType = type
formData.interval = config.interval ?? (type === 'hour' ? 1 : 5)
formData.minute = config.minute ?? 0
formData.times = (config.times?.length ? config.times : ['08:00']).map(item => toTimeValue(item))
formData.weekdays = config.weekdays?.length ? [...config.weekdays] : [1]
formData.days = config.days?.length ? [...config.days] : [1]
formData.expression = config.expression || job.cronExpression || ''
}
/** 后端校验之外的本地必填提示,避免空表单直接提交 */
function validateFreqConfig(): string {
if (hasTimeList.value && !formData.times.length) return '请至少添加 1 个执行时间'
if (formData.freqType === 'week' && !formData.weekdays.length) return '请至少选择 1 个星期'
if (formData.freqType === 'month' && !formData.days.length) return '请至少选择 1 个日期'
if (formData.freqType === 'custom' && !formData.expression.trim()) return '请输入 cron 表达式'
return ''
}
function addTime() {
if (formData.times.length >= 10) return
formData.times.push(toTimeValue('12:00'))
}
function removeTime(index: number) {
if (formData.times.length <= 1) return
formData.times.splice(index, 1)
}
let previewTimer: ReturnType<typeof setTimeout> | undefined
/** 已预览过(且成功过)的频率签名,避免打开弹窗时重复请求 */
let previewSignature = ''
async function fetchPreview() {
if (!editVisible.value) return
const signature = JSON.stringify({ type: formData.freqType, config: buildFreqConfig() })
if (signature === previewSignature) return
previewLoading.value = true
try {
const res: any = await jobApi.previewCron({
freqType: formData.freqType,
freqConfig: buildFreqConfig(),
})
if (res.code === 200) {
preview.valid = !!res.data.valid
preview.error = res.data.error || ''
preview.cronExpression = res.data.cronExpression || ''
preview.description = res.data.description || ''
preview.nextRuns = res.data.nextRuns || []
previewSignature = signature
}
} catch (error) {
// 预览失败不打断编辑,保存时后端仍会兜底校验
previewSignature = ''
} finally {
previewLoading.value = false
}
}
/** 频率任一字段变化 → 防抖后重新预览 */
watch(
() => JSON.stringify({ type: formData.freqType, config: buildFreqConfig() }),
() => {
clearTimeout(previewTimer)
previewTimer = setTimeout(fetchPreview, 300)
}
)
function resetPreview() {
preview.valid = true
preview.error = ''
preview.cronExpression = ''
preview.description = ''
preview.nextRuns = []
}
const logVisible = ref(false)
const logLoading = ref(false)
const logList = ref<JobLogItem[]>([])
const logTotal = ref(0)
const logQuery = reactive({
page: 1,
pageSize: 10,
status: undefined as number | undefined,
triggerType: undefined as number | undefined,
})
const logColumns = [
{ title: '执行时间', dataIndex: 'createdAt', key: 'createdAt', width: 170 },
{ title: '触发方式', dataIndex: 'triggerType', key: 'triggerType', width: 90 },
{ title: '状态', key: 'status', width: 80, align: 'center' as const },
{ title: '尝试', dataIndex: 'attempt', key: 'attempt', width: 70 },
{ title: '耗时', key: 'costMs', width: 90 },
{ title: '摘要', dataIndex: 'message', key: 'message' },
]
const logExpandable = {
rowExpandable: (record: JobLogItem) => !!record.errorStack,
}
const logPaginationConfig = reactive({
current: logQuery.page,
pageSize: logQuery.pageSize,
total: logTotal.value,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50'],
showTotal: (t: number) => `共 ${t} 条记录`,
})
function formatDateTime(time?: string | null): string {
if (!time) return '-'
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
}
function formatParams(params?: Record<string, any>, full = false): string {
if (!params || !Object.keys(params).length) return '-'
const text = Object.entries(params)
.map(([key, value]) => `${key}=${value === null || value === undefined ? '' : value}`)
.join(full ? '\n' : ', ')
return text || '-'
}
function runStatusText(status: number): string {
return ({ 1: '成功', 2: '失败', 3: '跳过' } as Record<number, string>)[status] || '-'
}
function runStatusColor(status: number): string {
return ({ 1: 'green', 2: 'red', 3: 'orange' } as Record<number, string>)[status] || 'default'
}
function triggerTypeText(type: number): string {
return ({ 1: '定时', 2: '手动', 3: '重试' } as Record<number, string>)[type] || '-'
}
function getParamRules(field: JobParamField) {
if (field.type !== 'number') return []
return [
{
validator: (_rule: any, value: any) => {
if (value === undefined || value === null || value === '') return Promise.resolve()
if (field.min !== undefined && value < field.min) {
return Promise.reject(new Error(`${field.label}不能小于 ${field.min}`))
}
if (field.max !== undefined && value > field.max) {
return Promise.reject(new Error(`${field.label}不能大于 ${field.max}`))
}
return Promise.resolve()
},
trigger: 'change',
},
]
}
async function fetchJobList() {
tableLoading.value = true
try {
const res: any = await jobApi.getJobList({ ...queryParams })
if (res.code === 200) {
jobList.value = res.data.list
total.value = res.data.total
paginationConfig.total = res.data.total
}
} catch (error) {
message.error('获取任务列表失败')
} finally {
tableLoading.value = false
}
}
function handleSearch() {
queryParams.page = 1
paginationConfig.current = 1
fetchJobList()
}
function handleReset() {
queryParams.jobName = ''
queryParams.status = undefined
handleSearch()
}
function handleTableChange(pag: any) {
queryParams.page = pag.current
queryParams.pageSize = pag.pageSize
paginationConfig.current = pag.current
paginationConfig.pageSize = pag.pageSize
fetchJobList()
}
async function handleStatusChange(record: JobItem, checked: boolean) {
const status: 0 | 1 = checked ? 1 : 0
statusLoadingId.value = record.id
try {
const res: any = await jobApi.changeJobStatus(record.id, status)
if (res.code === 200) {
record.status = status
message.success(status === 1 ? '已启用' : '已停用')
fetchJobList()
} else {
message.error(res.message || '状态更新失败')
}
} catch (error) {
// 失败时 record.status 未变更,受控开关自动回滚
} finally {
statusLoadingId.value = null
}
}
function handleEdit(record: JobItem) {
currentJob.value = record
paramFields.value = record.paramFields || []
applyFreqConfig(record)
formData.params = { ...(record.params || {}) }
formData.retryTimes = record.retryTimes ?? 0
formData.retryInterval = record.retryInterval ?? 60
formData.remark = record.remark || ''
resetPreview()
previewSignature = ''
editVisible.value = true
nextTick(() => formRef.value?.clearValidate())
fetchPreview()
}
function handleCancelEdit() {
editVisible.value = false
}
async function handleSave() {
if (!currentJob.value) return
try {
await formRef.value.validate()
} catch {
return
}
const freqError = validateFreqConfig()
if (freqError) {
message.warning(freqError)
return
}
if (!preview.valid) {
message.warning(preview.error || '频率配置不合法')
return
}
saving.value = true
try {
const res: any = await jobApi.updateJob(currentJob.value.id, {
freqType: formData.freqType,
freqConfig: buildFreqConfig(),
params: { ...formData.params },
retryTimes: formData.retryTimes,
retryInterval: formData.retryInterval,
remark: formData.remark,
})
if (res.code === 200) {
message.success('保存成功')
editVisible.value = false
fetchJobList()
} else {
message.error(res.message || '保存失败')
}
} catch (error) {
// 保存失败保持弹窗打开,后端 message 由响应拦截器提示
} finally {
saving.value = false
}
}
function handleRun(record: JobItem) {
Modal.confirm({
title: '立即执行确认',
content: `确定立即执行任务「${record.jobName}」吗?`,
okText: '确定',
cancelText: '取消',
onOk: async () => {
const res: any = await jobApi.runJob(record.id)
if (res.code === 200) {
message.success(res.data?.message || '执行完成')
fetchJobList()
if (logVisible.value && currentJob.value?.id === record.id) {
fetchJobLogs()
}
}
},
})
}
function handleOpenLogs(record: JobItem) {
currentJob.value = record
logQuery.page = 1
logQuery.pageSize = 10
logQuery.status = undefined
logQuery.triggerType = undefined
logPaginationConfig.current = 1
logPaginationConfig.pageSize = 10
logVisible.value = true
fetchJobLogs()
}
async function fetchJobLogs() {
if (!currentJob.value) return
logLoading.value = true
try {
const res: any = await jobApi.getJobLogs(currentJob.value.id, {
page: logQuery.page,
pageSize: logQuery.pageSize,
status: logQuery.status,
triggerType: logQuery.triggerType,
})
if (res.code === 200) {
logList.value = res.data.list
logTotal.value = res.data.total
logPaginationConfig.total = res.data.total
}
} catch (error) {
message.error('获取执行日志失败')
} finally {
logLoading.value = false
}
}
function handleLogSearch() {
logQuery.page = 1
logPaginationConfig.current = 1
fetchJobLogs()
}
function handleLogTableChange(pag: any) {
logQuery.page = pag.current
logQuery.pageSize = pag.pageSize
logPaginationConfig.current = pag.current
logPaginationConfig.pageSize = pag.pageSize
fetchJobLogs()
}
function handleClearLogs() {
Modal.confirm({
title: '清理日志确认',
content: '确定清理 90 天前的执行日志吗?此操作不可恢复。',
okText: '确定',
cancelText: '取消',
okType: 'danger',
onOk: async () => {
const res: any = await jobApi.clearJobLogs(90)
if (res.code === 200) {
message.success(`已清理 ${res.data?.deleted ?? 0} 条日志`)
handleLogSearch()
}
},
})
}
onMounted(() => {
fetchJobList()
})
onBeforeUnmount(() => {
clearTimeout(previewTimer)
})
</script>
<style scoped lang="less">
.job-management {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 20px;
box-sizing: border-box;
.search-card {
flex-shrink: 0;
margin-bottom: 16px;
:deep(.ant-card-body) {
padding-bottom: 4px;
}
}
.table-card {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
:deep(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.table-wrapper {
flex: 1;
min-height: 0;
overflow: hidden;
}
:deep(.ant-table) {
font-size: 13px;
height: 100%;
}
}
.ellipsis-text {
max-width: 160px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
.log-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.freq-cell {
display: flex;
flex-direction: column;
line-height: 20px;
.freq-cron {
font-size: 12px;
color: #8c8c8c;
}
}
.error-stack {
margin: 0;
padding: 8px 12px;
background: #fafafa;
border-radius: 4px;
font-size: 12px;
color: #cf1322;
white-space: pre-wrap;
word-break: break-all;
}
}
/* 弹窗内容被 teleport 到 body,样式不能嵌套在 .job-management 下 */
.form-divider {
margin: 4px 0 16px;
font-size: 13px;
color: #595959;
}
.time-list {
width: 100%;
.time-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
}
.freq-preview {
padding: 8px 12px;
background: #fafafa;
border-radius: 4px;
font-size: 13px;
line-height: 22px;
.preview-line {
display: flex;
align-items: flex-start;
gap: 8px;
}
.preview-label {
flex-shrink: 0;
width: 56px;
color: #8c8c8c;
}
code {
color: #c41d7f;
}
.next-list {
margin: 0;
padding-left: 18px;
}
}
</style>