更新功能:数据字典和定时任务
This commit is contained in:
376
resources/admin/src/pages/system/tasks/index.vue
Normal file
376
resources/admin/src/pages/system/tasks/index.vue
Normal file
@@ -0,0 +1,376 @@
|
||||
<template>
|
||||
<div class="pages-base-layout task-page">
|
||||
<div class="tool-bar">
|
||||
<div class="left-panel">
|
||||
<a-space>
|
||||
<a-input v-model:value="searchForm.keyword" placeholder="任务名称/命令" allow-clear style="width: 180px" />
|
||||
<a-select v-model:value="searchForm.type" placeholder="任务类型" allow-clear style="width: 120px">
|
||||
<a-select-option value="command">命令</a-select-option>
|
||||
<a-select-option value="job">任务</a-select-option>
|
||||
<a-select-option value="closure">闭包</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="searchForm.is_active" placeholder="状态" allow-clear style="width: 100px">
|
||||
<a-select-option :value="true">启用</a-select-option>
|
||||
<a-select-option :value="false">禁用</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><RedoOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<a-dropdown :disabled="selectedRows.length === 0">
|
||||
<a-button :disabled="selectedRows.length === 0">
|
||||
批量操作
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="handleBatchStatus(true)">
|
||||
<CheckCircleOutlined />批量启用
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="handleBatchStatus(false)">
|
||||
<StopOutlined />批量禁用
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item @click="handleBatchDelete" danger>
|
||||
<DeleteOutlined />批量删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-content">
|
||||
<scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading"
|
||||
:pagination="pagination" :row-selection="rowSelection" :row-key="rowKey" @refresh="refreshTable"
|
||||
@paginationChange="handlePaginationChange">
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="getTypeColor(record.type)">{{ getTypeText(record.type) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template #is_active="{ record }">
|
||||
<a-switch :checked="record.is_active" :disabled="!canEdit" @change="handleToggleStatus(record)" />
|
||||
</template>
|
||||
|
||||
<template #expression="{ record }">
|
||||
<code class="cron-code">{{ record.expression }}</code>
|
||||
</template>
|
||||
|
||||
<template #last_run_at="{ record }">
|
||||
{{ record.last_run_at ? formatDate(record.last_run_at) : '-' }}
|
||||
</template>
|
||||
|
||||
<template #next_run_at="{ record }">
|
||||
<span v-if="record.next_run_at" class="next-run">
|
||||
<ClockCircleOutlined />
|
||||
{{ formatDate(record.next_run_at) }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #statistics="{ record }">
|
||||
<a-space>
|
||||
<a-tooltip title="成功次数">
|
||||
<a-tag color="success">
|
||||
<CheckCircleOutlined /> {{ record.run_count || 0 }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="失败次数">
|
||||
<a-tag :color="record.failed_count > 0 ? 'error' : 'default'">
|
||||
<CloseCircleOutlined /> {{ record.failed_count || 0 }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleView(record)">
|
||||
<EyeOutlined />查看
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<EditOutlined />编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定立即执行该任务吗?" @confirm="handleRun(record)">
|
||||
<a-button type="link" size="small">
|
||||
<PlayCircleOutlined />执行
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<a-popconfirm title="确定删除该任务吗?" @confirm="handleDelete(record)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<DeleteOutlined />删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</scTable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<TaskDialog v-if="dialog.save" v-model:visible="dialog.save" :record="currentRecord" @success="handleSaveSuccess" />
|
||||
|
||||
<!-- 查看详情弹窗 -->
|
||||
<TaskDetailDialog v-if="dialog.detail" v-model:visible="dialog.detail" :record="currentRecord" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
RedoOutlined,
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
DownOutlined,
|
||||
CheckCircleOutlined,
|
||||
StopOutlined,
|
||||
EyeOutlined,
|
||||
PlayCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useTable } from '@/hooks/useTable'
|
||||
import systemApi from '@/api/system'
|
||||
import scTable from '@/components/scTable/index.vue'
|
||||
import TaskDialog from './components/TaskDialog.vue'
|
||||
import TaskDetailDialog from './components/TaskDetailDialog.vue'
|
||||
|
||||
// ===== useTable Hook =====
|
||||
const {
|
||||
tableRef,
|
||||
searchForm,
|
||||
tableData,
|
||||
loading,
|
||||
pagination,
|
||||
selectedRows,
|
||||
rowSelection,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handlePaginationChange,
|
||||
refreshTable
|
||||
} = useTable({
|
||||
api: systemApi.tasks.list.get,
|
||||
searchForm: {
|
||||
keyword: '',
|
||||
type: undefined,
|
||||
is_active: undefined
|
||||
},
|
||||
columns: [],
|
||||
needPagination: true,
|
||||
needSelection: true
|
||||
})
|
||||
|
||||
// ===== 表格列配置 =====
|
||||
const rowKey = 'id'
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80, align: 'center' },
|
||||
{ title: '任务名称', dataIndex: 'name', key: 'name', width: 150, ellipsis: true },
|
||||
{ title: '任务类型', dataIndex: 'type', key: 'type', width: 100, align: 'center', slot: 'type' },
|
||||
{ title: '命令/类', dataIndex: 'command', key: 'command', ellipsis: true },
|
||||
{ title: 'Cron表达式', dataIndex: 'expression', key: 'expression', width: 120, align: 'center', slot: 'expression' },
|
||||
{ title: '状态', dataIndex: 'is_active', key: 'is_active', width: 80, align: 'center', slot: 'is_active' },
|
||||
{ title: '上次运行', dataIndex: 'last_run_at', key: 'last_run_at', width: 160, align: 'center', slot: 'last_run_at' },
|
||||
{ title: '下次运行', dataIndex: 'next_run_at', key: 'next_run_at', width: 160, align: 'center', slot: 'next_run_at' },
|
||||
{ title: '统计', dataIndex: 'statistics', key: 'statistics', width: 120, align: 'center', slot: 'statistics' },
|
||||
{ title: '操作', dataIndex: 'action', key: 'action', width: 200, align: 'center', fixed: 'right', slot: 'action' }
|
||||
]
|
||||
|
||||
// ===== 弹窗状态 =====
|
||||
const dialog = reactive({
|
||||
save: false,
|
||||
detail: false
|
||||
})
|
||||
|
||||
const currentRecord = ref(null)
|
||||
|
||||
// ===== 权限控制 =====
|
||||
const canEdit = ref(true)
|
||||
|
||||
// ===== 方法:获取任务类型文本 =====
|
||||
const getTypeText = (type) => {
|
||||
const typeMap = {
|
||||
command: '命令',
|
||||
job: '任务',
|
||||
closure: '闭包'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// ===== 方法:获取任务类型颜色 =====
|
||||
const getTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
command: 'blue',
|
||||
job: 'green',
|
||||
closure: 'orange'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
}
|
||||
|
||||
// ===== 方法:格式化日期 =====
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// ===== 方法:切换状态 =====
|
||||
const handleToggleStatus = async (record) => {
|
||||
try {
|
||||
const res = await systemApi.tasks.batchStatus.post({
|
||||
ids: [record.id],
|
||||
status: record.is_active
|
||||
})
|
||||
if (res.code === 200) {
|
||||
message.success(record.is_active ? '已启用' : '已禁用')
|
||||
refreshTable()
|
||||
} else {
|
||||
message.error(res.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 方法:新增 =====
|
||||
const handleAdd = () => {
|
||||
currentRecord.value = null
|
||||
dialog.save = true
|
||||
}
|
||||
|
||||
// ===== 方法:编辑 =====
|
||||
const handleEdit = (record) => {
|
||||
currentRecord.value = { ...record }
|
||||
dialog.save = true
|
||||
}
|
||||
|
||||
// ===== 方法:查看 =====
|
||||
const handleView = (record) => {
|
||||
currentRecord.value = { ...record }
|
||||
dialog.detail = true
|
||||
}
|
||||
|
||||
// ===== 方法:删除 =====
|
||||
const handleDelete = async (record) => {
|
||||
try {
|
||||
const res = await systemApi.tasks.delete.delete(record.id)
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功')
|
||||
refreshTable()
|
||||
} else {
|
||||
message.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 方法:执行任务 =====
|
||||
const handleRun = async (record) => {
|
||||
try {
|
||||
const res = await systemApi.tasks.run.post(record.id)
|
||||
if (res.code === 200) {
|
||||
message.success('任务执行成功')
|
||||
refreshTable()
|
||||
} else {
|
||||
message.error(res.message || '任务执行失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('任务执行失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 方法:批量删除 =====
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
message.warning('请选择要删除的任务')
|
||||
return
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定删除选中的 ${selectedRows.value.length} 个任务吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const ids = selectedRows.value.map(item => item.id)
|
||||
const res = await systemApi.tasks.batchDelete.post({ ids })
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功')
|
||||
selectedRows.value = []
|
||||
refreshTable()
|
||||
} else {
|
||||
message.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 方法:批量更新状态 =====
|
||||
const handleBatchStatus = (status) => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
message.warning('请选择要操作的任务')
|
||||
return
|
||||
}
|
||||
|
||||
const statusText = status ? '启用' : '禁用'
|
||||
|
||||
Modal.confirm({
|
||||
title: `确认${statusText}`,
|
||||
content: `确定要${statusText}选中的 ${selectedRows.value.length} 个任务吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const ids = selectedRows.value.map(item => item.id)
|
||||
const res = await systemApi.tasks.batchStatus.post({ ids, status })
|
||||
if (res.code === 200) {
|
||||
message.success(`${statusText}成功`)
|
||||
selectedRows.value = []
|
||||
refreshTable()
|
||||
} else {
|
||||
message.error(res.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 方法:保存成功回调 =====
|
||||
const handleSaveSuccess = () => {
|
||||
dialog.save = false
|
||||
refreshTable()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.task-page {
|
||||
.cron-code {
|
||||
padding: 2px 6px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 3px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.next-run {
|
||||
color: #1890ff;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user