调整文件位置
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