500 lines
11 KiB
Vue
500 lines
11 KiB
Vue
<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>
|