调整文件位置
This commit is contained in:
@@ -1,400 +0,0 @@
|
||||
<template>
|
||||
<div class="plan-list">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="title-container">
|
||||
<h2 class="page-title">计划管理</h2>
|
||||
<el-button type="text" @click="loadPlans" class="refresh-btn" title="刷新计划列表">
|
||||
<el-icon :size="20"><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button type="primary" @click="addPlan">添加计划</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading">
|
||||
<el-skeleton animated />
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error">
|
||||
<el-alert
|
||||
title="获取计划数据失败"
|
||||
:description="error"
|
||||
type="error"
|
||||
show-icon
|
||||
closable
|
||||
@close="error = null"
|
||||
/>
|
||||
<el-button type="primary" @click="loadPlans" class="retry-btn">重新加载</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
:data="plans"
|
||||
style="width: 100%"
|
||||
class="plan-list-table"
|
||||
:fit="true"
|
||||
:scrollbar-always-on="true"
|
||||
@sort-change="handleSortChange">
|
||||
<el-table-column prop="id" label="计划ID" min-width="100" sortable="custom" />
|
||||
<el-table-column prop="name" label="计划名称" min-width="120" sortable="custom" />
|
||||
<el-table-column prop="description" label="计划描述" min-width="150" />
|
||||
<el-table-column prop="execution_type" label="执行类型" min-width="150" sortable="custom">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.execution_type === 'manual'">手动</el-tag>
|
||||
<el-tag v-else-if="scope.row.execute_num === 0" type="success">自动(无限执行)</el-tag>
|
||||
<el-tag v-else type="warning">自动({{ scope.row.execute_num }}次)</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="execute_count" label="已执行次数" min-width="120" sortable="custom" />
|
||||
<el-table-column prop="status" label="状态" min-width="100" sortable="custom">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.status === 0" type="danger">禁用计划</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 1" type="success">启用计划</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 3" type="danger">执行失败</el-tag>
|
||||
<el-tag v-else type="info">执行完毕</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cron_expression" label="下次执行时间" min-width="150" sortable="custom">
|
||||
<template #default="scope">
|
||||
{{ formatNextExecutionTime(scope.row.cron_expression) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="editPlan(scope.row)">编辑</el-button>
|
||||
<el-button size="small" @click="showDetails(scope.row)">详情</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="scope.row.status === 1 ? 'warning' : 'primary'"
|
||||
@click="scope.row.status === 1 ? stopPlan(scope.row) : startPlan(scope.row)"
|
||||
:loading="stoppingPlanId === scope.row.id || startingPlanId === scope.row.id"
|
||||
>
|
||||
{{ scope.row.status === 1 ? '停止' : '启动' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="deletePlan(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
</el-card>
|
||||
|
||||
<!-- 计划表单 -->
|
||||
<PlanForm
|
||||
v-model:visible="dialogVisible"
|
||||
:plan-data="currentPlan"
|
||||
:is-edit="isEdit"
|
||||
@success="handlePlanSuccess"
|
||||
@cancel="handlePlanCancel"
|
||||
/>
|
||||
|
||||
<!-- 计划详情 -->
|
||||
<el-dialog
|
||||
v-model="detailsVisible"
|
||||
title="计划详情"
|
||||
width="70%"
|
||||
top="5vh"
|
||||
>
|
||||
<plan-detail
|
||||
v-if="detailsVisible"
|
||||
:plan-id="selectedPlanIdForDetails"
|
||||
/>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detailsVisible = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Refresh } from '@element-plus/icons-vue';
|
||||
import apiClient from '../api/index.js';
|
||||
import PlanForm from './PlanForm.vue';
|
||||
import PlanDetail from './PlanDetail.vue'; // 导入新组件
|
||||
import cronParser from 'cron-parser';
|
||||
|
||||
export default {
|
||||
name: 'PlanList',
|
||||
components: {
|
||||
PlanForm,
|
||||
PlanDetail, // 注册新组件
|
||||
Refresh
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
plans: [],
|
||||
originalPlans: [], // Store the original unsorted list
|
||||
dialogVisible: false,
|
||||
detailsVisible: false, // 控制详情对话框
|
||||
isEdit: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
currentPlan: {
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
execution_type: 'automatic',
|
||||
execute_num: 0,
|
||||
cron_expression: ''
|
||||
},
|
||||
selectedPlanIdForDetails: null, // 当前要查看详情的计划ID
|
||||
startingPlanId: null,
|
||||
stoppingPlanId: null
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadPlans();
|
||||
},
|
||||
methods: {
|
||||
// 加载计划列表
|
||||
async loadPlans() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await apiClient.plans.getPlans(); // 更正此处
|
||||
let fetchedPlans = response.data?.plans || [];
|
||||
// Default sort by ID ascending
|
||||
fetchedPlans.sort((a, b) => a.id - b.id);
|
||||
this.plans = fetchedPlans;
|
||||
this.originalPlans = [...this.plans]; // Keep a copy of the original order
|
||||
} catch (err) {
|
||||
this.error = err.message || '未知错误';
|
||||
console.error('加载计划列表失败:', err);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 处理表格排序
|
||||
handleSortChange({ prop, order }) {
|
||||
if (!order) {
|
||||
// 恢复原始顺序
|
||||
this.plans = [...this.originalPlans];
|
||||
return;
|
||||
}
|
||||
|
||||
const sortFactor = order === 'ascending' ? 1 : -1;
|
||||
|
||||
this.plans.sort((a, b) => {
|
||||
let valA = a[prop];
|
||||
let valB = b[prop];
|
||||
|
||||
// '下次执行时间' 列的特殊排序逻辑
|
||||
if (prop === 'cron_expression') {
|
||||
try {
|
||||
const parser = cronParser.default || cronParser;
|
||||
valA = valA ? parser.parse(valA).next().toDate().getTime() : 0;
|
||||
} catch (e) {
|
||||
valA = 0; // 无效的 cron 表达式排在前面
|
||||
}
|
||||
try {
|
||||
const parser = cronParser.default || cronParser;
|
||||
valB = valB ? parser.parse(valB).next().toDate().getTime() : 0;
|
||||
} catch (e) {
|
||||
valB = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (valA < valB) {
|
||||
return -1 * sortFactor;
|
||||
}
|
||||
if (valA > valB) {
|
||||
return 1 * sortFactor;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
|
||||
// 格式化下次执行时间
|
||||
formatNextExecutionTime(cronExpression) {
|
||||
if (!cronExpression) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
try {
|
||||
// 正确使用cron-parser库
|
||||
const parser = cronParser.default || cronParser;
|
||||
const interval = parser.parse(cronExpression);
|
||||
const next = interval.next().toDate();
|
||||
return next.toLocaleString('zh-CN');
|
||||
} catch (err) {
|
||||
console.error('解析cron表达式失败:', err);
|
||||
return '无效的表达式';
|
||||
}
|
||||
},
|
||||
|
||||
addPlan() {
|
||||
this.currentPlan = {
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
execution_type: 'automatic',
|
||||
execute_num: 0,
|
||||
cron_expression: ''
|
||||
};
|
||||
this.isEdit = false;
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
|
||||
showDetails(plan) {
|
||||
this.selectedPlanIdForDetails = plan.id;
|
||||
this.detailsVisible = true;
|
||||
},
|
||||
|
||||
editPlan(plan) {
|
||||
this.currentPlan = { ...plan };
|
||||
this.isEdit = true;
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
|
||||
async deletePlan(plan) {
|
||||
try {
|
||||
await this.$confirm('确认删除该计划吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
await apiClient.plans.deletePlan(plan.id);
|
||||
this.$message.success('删除成功');
|
||||
await this.loadPlans();
|
||||
} catch (err) {
|
||||
if (err !== 'cancel') {
|
||||
this.$message.error('删除失败: ' + (err.message || '未知错误'));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async startPlan(plan) {
|
||||
try {
|
||||
this.startingPlanId = plan.id;
|
||||
await apiClient.plans.startPlan(plan.id);
|
||||
this.$message.success('计划启动成功');
|
||||
await this.loadPlans();
|
||||
} catch (err) {
|
||||
this.$message.error('启动失败: ' + (err.message || '未知错误'));
|
||||
} finally {
|
||||
this.startingPlanId = null;
|
||||
}
|
||||
},
|
||||
|
||||
async stopPlan(plan) {
|
||||
try {
|
||||
this.stoppingPlanId = plan.id;
|
||||
await apiClient.plans.stopPlan(plan.id);
|
||||
this.$message.success('计划停止成功');
|
||||
await this.loadPlans();
|
||||
} catch (err) {
|
||||
this.$message.error('停止失败: ' + (err.message || '未知错误'));
|
||||
} finally {
|
||||
this.stoppingPlanId = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 处理计划表单提交成功
|
||||
async handlePlanSuccess(planData) {
|
||||
try {
|
||||
if (this.isEdit) {
|
||||
// 编辑计划
|
||||
await apiClient.plans.updatePlan(planData.id, planData);
|
||||
this.$message.success('计划更新成功');
|
||||
} else {
|
||||
// 添加新计划
|
||||
const planRequest = {
|
||||
...planData,
|
||||
content_type: 'tasks' // 默认使用任务类型
|
||||
};
|
||||
|
||||
await apiClient.plans.createPlan(planRequest);
|
||||
this.$message.success('计划添加成功');
|
||||
}
|
||||
|
||||
await this.loadPlans();
|
||||
} catch (err) {
|
||||
this.$message.error('保存失败: ' + (err.message || '未知错误'));
|
||||
}
|
||||
},
|
||||
|
||||
// 处理计划表单取消
|
||||
handlePlanCancel() {
|
||||
this.dialogVisible = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.plan-list {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
color: black;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.plan-list-table :deep(tbody)::after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.plan-list {
|
||||
padding: 10px;
|
||||
}
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
88
src/main.js
88
src/main.js
@@ -1,15 +1,15 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import {createApp} from 'vue';
|
||||
import {createRouter, createWebHistory} from 'vue-router';
|
||||
import ElementPlus from 'element-plus';
|
||||
import 'element-plus/dist/index.css';
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'; // 导入 Element Plus 中文语言包
|
||||
|
||||
import App from './App.vue';
|
||||
import Home from './components/Home.vue';
|
||||
import DeviceList from './components/DeviceList.vue';
|
||||
import PlanList from './components/PlanList.vue';
|
||||
import LoginForm from './components/LoginForm.vue';
|
||||
import DeviceTemplateList from './components/DeviceTemplateList.vue';
|
||||
import Home from './views/home/Home.vue';
|
||||
import DeviceList from './views/device/DeviceList.vue';
|
||||
import PlanList from './views/plan/PlanList.vue';
|
||||
import LoginForm from './views/home/LoginForm.vue';
|
||||
import DeviceTemplateList from './views/device/DeviceTemplateList.vue';
|
||||
|
||||
// --- 统一导入所有监控视图 ---
|
||||
import DeviceCommandLogView from './views/monitor/DeviceCommandLogView.vue';
|
||||
@@ -36,58 +36,58 @@ import './assets/styles/main.css';
|
||||
|
||||
// 配置路由
|
||||
const routes = [
|
||||
{ path: '/', component: Home, meta: { requiresAuth: true } },
|
||||
{ path: '/devices', component: DeviceList, meta: { requiresAuth: true } },
|
||||
{ path: '/device-templates', component: DeviceTemplateList, meta: { requiresAuth: true } },
|
||||
{ path: '/plans', component: PlanList, meta: { requiresAuth: true } },
|
||||
{ path: '/login', component: LoginForm },
|
||||
{path: '/', component: Home, meta: {requiresAuth: true}},
|
||||
{path: '/devices', component: DeviceList, meta: {requiresAuth: true}},
|
||||
{path: '/device-templates', component: DeviceTemplateList, meta: {requiresAuth: true}},
|
||||
{path: '/plans', component: PlanList, meta: {requiresAuth: true}},
|
||||
{path: '/login', component: LoginForm},
|
||||
|
||||
// --- 统一注册所有监控路由 ---
|
||||
{ path: '/monitor/device-command-logs', component: DeviceCommandLogView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/feed-usage-records', component: FeedUsageRecordsView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/medication-logs', component: MedicationLogsView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/pending-collections', component: PendingCollectionsView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/pig-batch-logs', component: PigBatchLogsView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/pig-purchases', component: PigPurchasesView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/pig-sales', component: PigSalesView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/pig-sick-logs', component: PigSickLogsView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/pig-transfer-logs', component: PigTransferLogsView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/plan-execution-logs', component: PlanExecutionLogsView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/raw-material-purchases', component: RawMaterialPurchasesView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/raw-material-stock-logs', component: RawMaterialStockLogsView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/sensor-data', component: SensorDataView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/task-execution-logs', component: TaskExecutionLogsView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/user-action-logs', component: UserActionLogsView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/weighing-batches', component: WeighingBatchesView, meta: { requiresAuth: true } },
|
||||
{ path: '/monitor/weighing-records', component: WeighingRecordsView, meta: { requiresAuth: true } },
|
||||
// ---------------------------
|
||||
// --- 统一注册所有监控路由 ---
|
||||
{path: '/monitor/device-command-logs', component: DeviceCommandLogView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/feed-usage-records', component: FeedUsageRecordsView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/medication-logs', component: MedicationLogsView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/pending-collections', component: PendingCollectionsView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/pig-batch-logs', component: PigBatchLogsView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/pig-purchases', component: PigPurchasesView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/pig-sales', component: PigSalesView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/pig-sick-logs', component: PigSickLogsView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/pig-transfer-logs', component: PigTransferLogsView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/plan-execution-logs', component: PlanExecutionLogsView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/raw-material-purchases', component: RawMaterialPurchasesView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/raw-material-stock-logs', component: RawMaterialStockLogsView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/sensor-data', component: SensorDataView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/task-execution-logs', component: TaskExecutionLogsView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/user-action-logs', component: UserActionLogsView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/weighing-batches', component: WeighingBatchesView, meta: {requiresAuth: true}},
|
||||
{path: '/monitor/weighing-records', component: WeighingRecordsView, meta: {requiresAuth: true}},
|
||||
// ---------------------------
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
// 全局路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const loggedIn = localStorage.getItem('jwt_token');
|
||||
const loggedIn = localStorage.getItem('jwt_token');
|
||||
|
||||
if (to.matched.some(record => record.meta.requiresAuth) && !loggedIn) {
|
||||
// 如果路由需要认证但用户未登录,则重定向到登录页
|
||||
next('/login');
|
||||
} else if (to.path === '/login' && loggedIn) {
|
||||
// 如果用户已登录但试图访问登录页,则重定向到首页
|
||||
next('/');
|
||||
} else {
|
||||
next(); // 正常放行
|
||||
}
|
||||
if (to.matched.some(record => record.meta.requiresAuth) && !loggedIn) {
|
||||
// 如果路由需要认证但用户未登录,则重定向到登录页
|
||||
next('/login');
|
||||
} else if (to.path === '/login' && loggedIn) {
|
||||
// 如果用户已登录但试图访问登录页,则重定向到首页
|
||||
next('/');
|
||||
} else {
|
||||
next(); // 正常放行
|
||||
}
|
||||
});
|
||||
|
||||
// 创建Vue应用实例
|
||||
const app = createApp(App);
|
||||
|
||||
// 全局配置 Element Plus 为中文
|
||||
app.use(ElementPlus, { locale: zhCn });
|
||||
app.use(ElementPlus, {locale: zhCn});
|
||||
|
||||
// 使用路由
|
||||
app.use(router);
|
||||
|
||||
@@ -81,8 +81,8 @@
|
||||
|
||||
<script>
|
||||
import { Refresh } from '@element-plus/icons-vue';
|
||||
import deviceService from '../services/deviceService.js';
|
||||
import DeviceForm from './DeviceForm.vue';
|
||||
import deviceService from '../../services/deviceService.js';
|
||||
import DeviceForm from '../../components/DeviceForm.vue';
|
||||
|
||||
export default {
|
||||
name: 'DeviceList',
|
||||
@@ -76,8 +76,8 @@
|
||||
|
||||
<script>
|
||||
import { Refresh } from '@element-plus/icons-vue';
|
||||
import deviceTemplateService from '../services/deviceTemplateService.js';
|
||||
import DeviceTemplateForm from './DeviceTemplateForm.vue';
|
||||
import deviceTemplateService from '../../services/deviceTemplateService.js';
|
||||
import DeviceTemplateForm from '../../components/DeviceTemplateForm.vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
export default {
|
||||
@@ -32,7 +32,7 @@
|
||||
import { ref, reactive } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import apiClient from '../api/index.js';
|
||||
import apiClient from '../../api';
|
||||
|
||||
export default {
|
||||
name: 'LoginForm',
|
||||
@@ -1,39 +1,400 @@
|
||||
<template>
|
||||
<el-table :data="planList">
|
||||
<el-table-column prop="executeType" label="执行类型">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.executeType === 'manual'"
|
||||
type=""
|
||||
class="full-width-tag"
|
||||
>
|
||||
手动
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else-if="row.executeType === 'auto-infinite'"
|
||||
type="success"
|
||||
class="full-width-tag"
|
||||
>
|
||||
自动(无限执行)
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else-if="row.executeType === 'auto-limited'"
|
||||
type="warning"
|
||||
class="full-width-tag"
|
||||
>
|
||||
自动({{ row.executeTimes }}次)
|
||||
</el-tag>
|
||||
<div class="plan-list">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="title-container">
|
||||
<h2 class="page-title">计划管理</h2>
|
||||
<el-button type="text" @click="loadPlans" class="refresh-btn" title="刷新计划列表">
|
||||
<el-icon :size="20"><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button type="primary" @click="addPlan">添加计划</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading">
|
||||
<el-skeleton animated />
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error">
|
||||
<el-alert
|
||||
title="获取计划数据失败"
|
||||
:description="error"
|
||||
type="error"
|
||||
show-icon
|
||||
closable
|
||||
@close="error = null"
|
||||
/>
|
||||
<el-button type="primary" @click="loadPlans" class="retry-btn">重新加载</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
:data="plans"
|
||||
style="width: 100%"
|
||||
class="plan-list-table"
|
||||
:fit="true"
|
||||
:scrollbar-always-on="true"
|
||||
@sort-change="handleSortChange">
|
||||
<el-table-column prop="id" label="计划ID" min-width="100" sortable="custom" />
|
||||
<el-table-column prop="name" label="计划名称" min-width="120" sortable="custom" />
|
||||
<el-table-column prop="description" label="计划描述" min-width="150" />
|
||||
<el-table-column prop="execution_type" label="执行类型" min-width="150" sortable="custom">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.execution_type === 'manual'">手动</el-tag>
|
||||
<el-tag v-else-if="scope.row.execute_num === 0" type="success">自动(无限执行)</el-tag>
|
||||
<el-tag v-else type="warning">自动({{ scope.row.execute_num }}次)</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="execute_count" label="已执行次数" min-width="120" sortable="custom" />
|
||||
<el-table-column prop="status" label="状态" min-width="100" sortable="custom">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.status === 0" type="danger">禁用计划</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 1" type="success">启用计划</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 3" type="danger">执行失败</el-tag>
|
||||
<el-tag v-else type="info">执行完毕</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cron_expression" label="下次执行时间" min-width="150" sortable="custom">
|
||||
<template #default="scope">
|
||||
{{ formatNextExecutionTime(scope.row.cron_expression) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="editPlan(scope.row)">编辑</el-button>
|
||||
<el-button size="small" @click="showDetails(scope.row)">详情</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="scope.row.status === 1 ? 'warning' : 'primary'"
|
||||
@click="scope.row.status === 1 ? stopPlan(scope.row) : startPlan(scope.row)"
|
||||
:loading="stoppingPlanId === scope.row.id || startingPlanId === scope.row.id"
|
||||
>
|
||||
{{ scope.row.status === 1 ? '停止' : '启动' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="deletePlan(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
</el-card>
|
||||
|
||||
<!-- 计划表单 -->
|
||||
<PlanForm
|
||||
v-model:visible="dialogVisible"
|
||||
:plan-data="currentPlan"
|
||||
:is-edit="isEdit"
|
||||
@success="handlePlanSuccess"
|
||||
@cancel="handlePlanCancel"
|
||||
/>
|
||||
|
||||
<!-- 计划详情 -->
|
||||
<el-dialog
|
||||
v-model="detailsVisible"
|
||||
title="计划详情"
|
||||
width="70%"
|
||||
top="5vh"
|
||||
>
|
||||
<plan-detail
|
||||
v-if="detailsVisible"
|
||||
:plan-id="selectedPlanIdForDetails"
|
||||
/>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detailsVisible = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Refresh } from '@element-plus/icons-vue';
|
||||
import apiClient from '../../api';
|
||||
import PlanForm from '../../components/PlanForm.vue';
|
||||
import PlanDetail from '../../components/PlanDetail.vue'; // 导入新组件
|
||||
import cronParser from 'cron-parser';
|
||||
|
||||
export default {
|
||||
name: 'PlanList',
|
||||
components: {
|
||||
PlanForm,
|
||||
PlanDetail, // 注册新组件
|
||||
Refresh
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
plans: [],
|
||||
originalPlans: [], // Store the original unsorted list
|
||||
dialogVisible: false,
|
||||
detailsVisible: false, // 控制详情对话框
|
||||
isEdit: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
currentPlan: {
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
execution_type: 'automatic',
|
||||
execute_num: 0,
|
||||
cron_expression: ''
|
||||
},
|
||||
selectedPlanIdForDetails: null, // 当前要查看详情的计划ID
|
||||
startingPlanId: null,
|
||||
stoppingPlanId: null
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadPlans();
|
||||
},
|
||||
methods: {
|
||||
// 加载计划列表
|
||||
async loadPlans() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await apiClient.plans.getPlans(); // 更正此处
|
||||
let fetchedPlans = response.data?.plans || [];
|
||||
// Default sort by ID ascending
|
||||
fetchedPlans.sort((a, b) => a.id - b.id);
|
||||
this.plans = fetchedPlans;
|
||||
this.originalPlans = [...this.plans]; // Keep a copy of the original order
|
||||
} catch (err) {
|
||||
this.error = err.message || '未知错误';
|
||||
console.error('加载计划列表失败:', err);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 处理表格排序
|
||||
handleSortChange({ prop, order }) {
|
||||
if (!order) {
|
||||
// 恢复原始顺序
|
||||
this.plans = [...this.originalPlans];
|
||||
return;
|
||||
}
|
||||
|
||||
const sortFactor = order === 'ascending' ? 1 : -1;
|
||||
|
||||
this.plans.sort((a, b) => {
|
||||
let valA = a[prop];
|
||||
let valB = b[prop];
|
||||
|
||||
// '下次执行时间' 列的特殊排序逻辑
|
||||
if (prop === 'cron_expression') {
|
||||
try {
|
||||
const parser = cronParser.default || cronParser;
|
||||
valA = valA ? parser.parse(valA).next().toDate().getTime() : 0;
|
||||
} catch (e) {
|
||||
valA = 0; // 无效的 cron 表达式排在前面
|
||||
}
|
||||
try {
|
||||
const parser = cronParser.default || cronParser;
|
||||
valB = valB ? parser.parse(valB).next().toDate().getTime() : 0;
|
||||
} catch (e) {
|
||||
valB = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (valA < valB) {
|
||||
return -1 * sortFactor;
|
||||
}
|
||||
if (valA > valB) {
|
||||
return 1 * sortFactor;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
|
||||
// 格式化下次执行时间
|
||||
formatNextExecutionTime(cronExpression) {
|
||||
if (!cronExpression) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
try {
|
||||
// 正确使用cron-parser库
|
||||
const parser = cronParser.default || cronParser;
|
||||
const interval = parser.parse(cronExpression);
|
||||
const next = interval.next().toDate();
|
||||
return next.toLocaleString('zh-CN');
|
||||
} catch (err) {
|
||||
console.error('解析cron表达式失败:', err);
|
||||
return '无效的表达式';
|
||||
}
|
||||
},
|
||||
|
||||
addPlan() {
|
||||
this.currentPlan = {
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
execution_type: 'automatic',
|
||||
execute_num: 0,
|
||||
cron_expression: ''
|
||||
};
|
||||
this.isEdit = false;
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
|
||||
showDetails(plan) {
|
||||
this.selectedPlanIdForDetails = plan.id;
|
||||
this.detailsVisible = true;
|
||||
},
|
||||
|
||||
editPlan(plan) {
|
||||
this.currentPlan = { ...plan };
|
||||
this.isEdit = true;
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
|
||||
async deletePlan(plan) {
|
||||
try {
|
||||
await this.$confirm('确认删除该计划吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
await apiClient.plans.deletePlan(plan.id);
|
||||
this.$message.success('删除成功');
|
||||
await this.loadPlans();
|
||||
} catch (err) {
|
||||
if (err !== 'cancel') {
|
||||
this.$message.error('删除失败: ' + (err.message || '未知错误'));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async startPlan(plan) {
|
||||
try {
|
||||
this.startingPlanId = plan.id;
|
||||
await apiClient.plans.startPlan(plan.id);
|
||||
this.$message.success('计划启动成功');
|
||||
await this.loadPlans();
|
||||
} catch (err) {
|
||||
this.$message.error('启动失败: ' + (err.message || '未知错误'));
|
||||
} finally {
|
||||
this.startingPlanId = null;
|
||||
}
|
||||
},
|
||||
|
||||
async stopPlan(plan) {
|
||||
try {
|
||||
this.stoppingPlanId = plan.id;
|
||||
await apiClient.plans.stopPlan(plan.id);
|
||||
this.$message.success('计划停止成功');
|
||||
await this.loadPlans();
|
||||
} catch (err) {
|
||||
this.$message.error('停止失败: ' + (err.message || '未知错误'));
|
||||
} finally {
|
||||
this.stoppingPlanId = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 处理计划表单提交成功
|
||||
async handlePlanSuccess(planData) {
|
||||
try {
|
||||
if (this.isEdit) {
|
||||
// 编辑计划
|
||||
await apiClient.plans.updatePlan(planData.id, planData);
|
||||
this.$message.success('计划更新成功');
|
||||
} else {
|
||||
// 添加新计划
|
||||
const planRequest = {
|
||||
...planData,
|
||||
content_type: 'tasks' // 默认使用任务类型
|
||||
};
|
||||
|
||||
await apiClient.plans.createPlan(planRequest);
|
||||
this.$message.success('计划添加成功');
|
||||
}
|
||||
|
||||
await this.loadPlans();
|
||||
} catch (err) {
|
||||
this.$message.error('保存失败: ' + (err.message || '未知错误'));
|
||||
}
|
||||
},
|
||||
|
||||
// 处理计划表单取消
|
||||
handlePlanCancel() {
|
||||
this.dialogVisible = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.full-width-tag {
|
||||
/* 移除默认的宽度限制和文本溢出处理 */
|
||||
max-width: none !important;
|
||||
overflow: visible !important;
|
||||
white-space: normal !important;
|
||||
word-wrap: break-word !important;
|
||||
.plan-list {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
color: black;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.plan-list-table :deep(tbody)::after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.plan-list {
|
||||
padding: 10px;
|
||||
}
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user