编辑计划详情
This commit is contained in:
		| @@ -19,7 +19,11 @@ | ||||
|           <div class="card-header"> | ||||
|             <span>{{ plan.name }} - 内容</span> | ||||
|             <div> | ||||
|               <el-button class="button" @click="toggleEditMode" v-if="!isSubPlan">{{ isEditingContent ? '完成编辑' : '编辑内容' }}</el-button> | ||||
|               <template v-if="!isSubPlan"> | ||||
|                 <el-button class="button" type="primary" @click="savePlanContent" v-if="isEditingContent">保存</el-button> | ||||
|                 <el-button class="button" @click="cancelEdit" v-if="isEditingContent">取消</el-button> | ||||
|                 <el-button class="button" @click="enterEditMode" v-else>编辑内容</el-button> | ||||
|               </template> | ||||
|  | ||||
|               <!-- Dynamic Add Buttons --> | ||||
|               <template v-if="isEditingContent"> | ||||
| @@ -51,10 +55,10 @@ | ||||
|               placement="top" | ||||
|             > | ||||
|               <el-card> | ||||
|                 <h5>{{ task.name }} ({{ task.type === 'delay_task' ? '延时任务' : '未知任务' }})</h5> | ||||
|                 <h5>{{ task.name }} ({{ task.type === 'waiting' ? '延时任务' : '未知任务' }})</h5> | ||||
|                 <p>{{ task.description }}</p> | ||||
|                 <p v-if="task.type === 'delay_task' && task.parameters?.delay_seconds"> | ||||
|                   延时: {{ task.parameters.delay_seconds }} 秒 | ||||
|                 <p v-if="task.type === 'waiting' && task.parameters?.delay_duration"> | ||||
|                   延时: {{ task.parameters.delay_duration }} 秒 | ||||
|                 </p> | ||||
|                 <el-button-group v-if="isEditingContent"> | ||||
|                   <el-button type="primary" size="small" @click="editTask(task)">编辑</el-button> | ||||
| @@ -70,12 +74,12 @@ | ||||
|         <div v-else-if="plan.content_type === 'sub_plans'"> | ||||
|           <h4>子计划列表</h4> | ||||
|           <div v-if="plan.sub_plans.length > 0"> | ||||
|             <div v-for="(subPlan, index) in plan.sub_plans" :key="subPlan.id || 'new-subplan-' + index" class="sub-plan-container"> | ||||
|             <div v-for="(subPlan, index) in plan.sub_plans" :key="subPlan.id || 'new-subplan-' + index" class="sub-plan-wrapper"> | ||||
|               <el-card> | ||||
|                 <div class="card-header"> | ||||
|                 <div class="sub-plan-card-content"> | ||||
|                   <!-- Pass child_plan_id to recursive PlanDetail --> | ||||
|                   <plan-detail :plan-id="subPlan.child_plan_id" :is-sub-plan="true" /> | ||||
|                   <el-button-group v-if="isEditingContent"> | ||||
|                   <el-button-group v-if="isEditingContent" class="sub-plan-actions"> | ||||
|                     <el-button type="danger" size="small" @click="deleteSubPlan(subPlan)">删除</el-button> | ||||
|                   </el-button-group> | ||||
|                 </div> | ||||
| @@ -147,9 +151,9 @@ | ||||
|           <DelayTaskEditor | ||||
|             :parameters="newTaskForm.parameters" | ||||
|             @update:parameters="val => newTaskForm.parameters = val" | ||||
|             prop-prefix="parameters." | ||||
|             :is-editing="true" /><!-- Always editable when adding a new task --> | ||||
|  | ||||
|             prop-path="parameters.delay_duration" | ||||
|             :is-editing="true"  | ||||
|           /> | ||||
|         </template> | ||||
|         <!-- More task types can be rendered here --> | ||||
|       </el-form> | ||||
| @@ -166,16 +170,16 @@ | ||||
| <script> | ||||
| import apiClient from '../api/index.js'; | ||||
| import { ElMessage, ElMessageBox } from 'element-plus'; | ||||
| import { ArrowDown } from '@element-plus/icons-vue'; // Keep ArrowDown for potential future use or if other dropdowns exist | ||||
| import DelayTaskEditor from './tasks/DelayTask.vue'; // Import the new component | ||||
| import { ArrowDown } from '@element-plus/icons-vue';  | ||||
| import DelayTaskEditor from './tasks/DelayTask.vue';  | ||||
|  | ||||
| export default { | ||||
|   name: 'PlanDetail', | ||||
|   components: { | ||||
|     DelayTaskEditor, // Register the new component | ||||
|     DelayTaskEditor,  | ||||
|     // Self-reference for recursion | ||||
|     'plan-detail': this, | ||||
|     ArrowDown, // Register ArrowDown | ||||
|     ArrowDown,  | ||||
|   }, | ||||
|   props: { | ||||
|     planId: { | ||||
| @@ -196,13 +200,14 @@ export default { | ||||
|         execution_type: 'automatic', | ||||
|         execute_num: 0, | ||||
|         cron_expression: '', | ||||
|         content_type: null, // Will be 'sub_plans', 'tasks', or null | ||||
|         sub_plans: [], // Array of plan.SubPlanResponse | ||||
|         tasks: [],     // Array of plan.TaskResponse | ||||
|         content_type: null,  | ||||
|         sub_plans: [],  | ||||
|         tasks: [],      | ||||
|       }, | ||||
|       loading: false, | ||||
|       error: null, | ||||
|       isEditingContent: false, | ||||
|       originalPlanState: null,  | ||||
|  | ||||
|       // Add Sub-plan dialog | ||||
|       addSubPlanDialogVisible: false, | ||||
| @@ -212,20 +217,21 @@ export default { | ||||
|       // Add Task dialog | ||||
|       addTaskDialogVisible: false, | ||||
|       newTaskForm: { | ||||
|         type: 'delay_task', // Default to delay_task since normal_task is removed | ||||
|         type: 'delay_task',  | ||||
|         name: '', | ||||
|         description: '', | ||||
|         parameters: {}, | ||||
|         parameters: { delay_duration: 1 }, // Initialize with delay_duration | ||||
|       }, | ||||
|       newTaskRules: { | ||||
|         type: [{ required: true, message: '请选择任务类型', trigger: 'change' }], | ||||
|         name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }], | ||||
|         // Add rule for delay_duration directly here, as it's the default type | ||||
|         'parameters.delay_duration': [{ required: true, message: '请输入延时时间', trigger: 'blur' }], | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     // Rules for delay_seconds, dynamically added/removed | ||||
|     delaySecondsRules() { | ||||
|     delayDurationRules() { | ||||
|       return [{ required: true, message: '请输入延时时间', trigger: 'blur' }]; | ||||
|     }, | ||||
|   }, | ||||
| @@ -238,23 +244,20 @@ export default { | ||||
|         } | ||||
|       }, | ||||
|     }, | ||||
|     // Watch for newTaskForm.type changes to update validation rules | ||||
|     'newTaskForm.type'(newType) { | ||||
|       // This watch is less critical now since delay_task is the only option | ||||
|       // but kept for extensibility. | ||||
|       if (newType === 'delay_task') { | ||||
|         // Direct assignment for reactive object | ||||
|         this.newTaskRules['parameters.delay_seconds'] = this.delaySecondsRules; | ||||
|         // Initialize parameters if not already present | ||||
|         if (!this.newTaskForm.parameters.delay_seconds) { | ||||
|           this.newTaskForm.parameters.delay_seconds = 1; | ||||
|         this.newTaskRules['parameters.delay_duration'] = this.delayDurationRules; | ||||
|         if (!this.newTaskForm.parameters.delay_duration) { | ||||
|           this.newTaskForm.parameters.delay_duration = 1; | ||||
|         } | ||||
|       } else { | ||||
|         // Remove delay_seconds rule if not delay_task | ||||
|         if (this.newTaskRules['parameters.delay_seconds']) { | ||||
|           delete this.newTaskRules['parameters.delay_seconds']; | ||||
|         if (this.newTaskRules['parameters.delay_duration']) { | ||||
|           delete this.newTaskRules['parameters.delay_duration']; | ||||
|         } | ||||
|         // Clear delay_seconds parameter if not delay_task | ||||
|         if (this.newTaskForm.parameters.delay_seconds) { | ||||
|           delete this.newTaskForm.parameters.delay_seconds; | ||||
|         if (this.newTaskForm.parameters.delay_duration) { | ||||
|           delete this.newTaskForm.parameters.delay_duration; | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
| @@ -270,7 +273,7 @@ export default { | ||||
|           sub_plans: response.data.sub_plans || [], | ||||
|           tasks: response.data.tasks || [], | ||||
|         }; | ||||
|         this.updateContentType(); // Set initial content_type based on fetched data | ||||
|         this.updateContentType();  | ||||
|       } catch (err) { | ||||
|         this.error = err.message || '未知错误'; | ||||
|         console.error(`加载计划 (ID: ${this.planId}) 失败:`, err); | ||||
| @@ -284,58 +287,61 @@ export default { | ||||
|       } else if (this.plan.tasks.length > 0) { | ||||
|         this.plan.content_type = 'tasks'; | ||||
|       } else { | ||||
|         this.plan.content_type = null; // Or an empty string to indicate no content type yet | ||||
|         this.plan.content_type = null;  | ||||
|       } | ||||
|     }, | ||||
|     toggleEditMode() { | ||||
|       this.isEditingContent = !this.isEditingContent; | ||||
|       if (!this.isEditingContent) { | ||||
|         this.savePlanContent(); | ||||
|       } | ||||
|     enterEditMode() { | ||||
|       this.isEditingContent = true; | ||||
|       this.originalPlanState = JSON.parse(JSON.stringify(this.plan)); | ||||
|     }, | ||||
|     async savePlanContent() { | ||||
|       this.updateContentType(); // Ensure content_type is correct before saving | ||||
|       this.updateContentType();  | ||||
|       try { | ||||
|         const submitData = { | ||||
|           id: this.plan.id, // Ensure ID is included for update | ||||
|           id: this.plan.id,  | ||||
|           name: this.plan.name, | ||||
|           description: this.plan.description, | ||||
|           execution_type: this.plan.execution_type, | ||||
|           execute_num: this.plan.execute_num, | ||||
|           cron_expression: this.plan.cron_expression, | ||||
|           content_type: this.plan.content_type, | ||||
|           // Only send sub_plan_ids or tasks based on content_type | ||||
|           sub_plan_ids: this.plan.content_type === 'sub_plans' | ||||
|             ? this.plan.sub_plans.map(sp => sp.child_plan_id) // Extract child_plan_id | ||||
|             ? this.plan.sub_plans.map(sp => sp.child_plan_id)  | ||||
|             : [], | ||||
|           tasks: this.plan.content_type === 'tasks' | ||||
|             ? this.plan.tasks.map((task, index) => ({ | ||||
|                 name: task.name, | ||||
|                 description: task.description, | ||||
|                 type: task.type, | ||||
|                 execution_order: index + 1, // Re-order before sending | ||||
|                 // Set type to 'waiting' for delay_task | ||||
|                 type: task.type === 'delay_task' ? 'waiting' : task.type, | ||||
|                 execution_order: index + 1,  | ||||
|                 parameters: task.parameters || {}, | ||||
|               })) | ||||
|             : [], | ||||
|         }; | ||||
|  | ||||
|         // Remove properties that are not part of UpdatePlanRequest but are in this.plan | ||||
|         // These are typically read-only or derived fields | ||||
|         delete submitData.execute_count; | ||||
|         delete submitData.status; | ||||
|  | ||||
|         await apiClient.plans.update(this.planId, submitData); | ||||
|         ElMessage.success('计划内容已保存'); | ||||
|         this.fetchPlan(); // Re-fetch to get updated data from backend | ||||
|         this.isEditingContent = false;  | ||||
|         this.originalPlanState = null;  | ||||
|         this.fetchPlan();  | ||||
|       } catch (error) { | ||||
|         ElMessage.error('保存计划内容失败: ' + (error.message || '未知错误')); | ||||
|         console.error('保存计划内容失败:', error); | ||||
|       } | ||||
|     }, | ||||
|     cancelEdit() { | ||||
|       this.plan = JSON.parse(JSON.stringify(this.originalPlanState)); | ||||
|       this.isEditingContent = false;  | ||||
|       this.originalPlanState = null;  | ||||
|       ElMessage.info('已取消编辑'); | ||||
|     }, | ||||
|  | ||||
|     // --- Sub-plan related methods --- | ||||
|     async showAddSubPlanDialog() { | ||||
|       // If there are existing tasks, warn the user | ||||
|       if (this.plan.tasks.length > 0) { | ||||
|         try { | ||||
|           await ElMessageBox.confirm('当前计划包含任务,添加子计划将清空现有任务。是否继续?', '警告', { | ||||
| @@ -343,10 +349,8 @@ export default { | ||||
|             cancelButtonText: '取消', | ||||
|             type: 'warning' | ||||
|           }); | ||||
|           this.plan.tasks = []; // Clear tasks | ||||
|           // No need to force content_type here, updateContentType will handle it | ||||
|           this.plan.tasks = [];  | ||||
|         } catch (e) { | ||||
|           // User cancelled | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
| @@ -356,10 +360,8 @@ export default { | ||||
|     async fetchAvailablePlans() { | ||||
|       try { | ||||
|         const response = await apiClient.plans.list(); | ||||
|         // Filter out current plan, already added sub-plans, and prevent circular references | ||||
|         this.availablePlans = response.data.plans.filter(p => | ||||
|           p.id !== this.planId && | ||||
|           !this.plan.sub_plans.some(sub => sub.child_plan_id === p.id) | ||||
|           p.id !== this.planId | ||||
|         ); | ||||
|       } catch (error) { | ||||
|         ElMessage.error('加载可用计划失败: ' + (error.message || '未知错误')); | ||||
| @@ -375,12 +377,12 @@ export default { | ||||
|       const selectedPlan = this.availablePlans.find(p => p.id === this.selectedSubPlanId); | ||||
|       if (selectedPlan) { | ||||
|         this.plan.sub_plans.push({ | ||||
|           id: Date.now(), // Temporary ID for UI, actual ID from backend after save | ||||
|           id: Date.now(),  | ||||
|           child_plan_id: selectedPlan.id, | ||||
|           child_plan: selectedPlan, // Store full plan for display if needed | ||||
|           child_plan: selectedPlan,  | ||||
|           execution_order: this.plan.sub_plans.length + 1, | ||||
|         }); | ||||
|         this.updateContentType(); // Update content type | ||||
|         this.updateContentType();  | ||||
|         ElMessage.success(`子计划 "${selectedPlan.name}" 已添加`); | ||||
|         this.addSubPlanDialogVisible = false; | ||||
|         this.resetAddSubPlanDialog(); | ||||
| @@ -399,17 +401,15 @@ export default { | ||||
|         type: 'warning' | ||||
|       }).then(() => { | ||||
|         this.plan.sub_plans = this.plan.sub_plans.filter(sub => sub.id !== subPlanToDelete.id); | ||||
|         this.plan.sub_plans.forEach((item, index) => item.execution_order = index + 1); // Re-order | ||||
|         this.updateContentType(); // Update content type | ||||
|         this.plan.sub_plans.forEach((item, index) => item.execution_order = index + 1);  | ||||
|         this.updateContentType();  | ||||
|         ElMessage.success('子计划已删除'); | ||||
|       }).catch(() => { | ||||
|         // User cancelled | ||||
|       }); | ||||
|     }, | ||||
|  | ||||
|     // --- Task related methods --- | ||||
|     async showAddTaskDialog() { | ||||
|       // If there are existing sub-plans, warn the user | ||||
|       if (this.plan.sub_plans.length > 0) { | ||||
|         try { | ||||
|           await ElMessageBox.confirm('当前计划包含子计划,添加任务将清空现有子计划。是否继续?', '警告', { | ||||
| @@ -417,10 +417,8 @@ export default { | ||||
|             cancelButtonText: '取消', | ||||
|             type: 'warning' | ||||
|           }); | ||||
|           this.plan.sub_plans = []; // Clear sub-plans | ||||
|           // No need to force content_type here, updateContentType will handle it | ||||
|           this.plan.sub_plans = [];  | ||||
|         } catch (e) { | ||||
|           // User cancelled | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
| @@ -430,16 +428,17 @@ export default { | ||||
|       this.$refs.newTaskFormRef.validate(async (valid) => { | ||||
|         if (valid) { | ||||
|           const newTask = { | ||||
|             id: Date.now(), // Temporary ID for UI, actual ID from backend after save | ||||
|             id: Date.now(),  | ||||
|             execution_order: this.plan.tasks.length + 1, | ||||
|             type: this.newTaskForm.type, | ||||
|             // Map 'delay_task' UI type to 'waiting' backend type | ||||
|             type: this.newTaskForm.type === 'delay_task' ? 'waiting' : this.newTaskForm.type, | ||||
|             name: this.newTaskForm.name, | ||||
|             description: this.newTaskForm.description, | ||||
|             parameters: this.newTaskForm.parameters, // Include parameters | ||||
|             parameters: this.newTaskForm.parameters,  | ||||
|           }; | ||||
|  | ||||
|           this.plan.tasks.push(newTask); | ||||
|           this.updateContentType(); // Update content type | ||||
|           this.updateContentType();  | ||||
|           ElMessage.success(`子任务 "${newTask.name}" 已添加`); | ||||
|           this.addTaskDialogVisible = false; | ||||
|           this.resetAddTaskDialog(); | ||||
| @@ -449,14 +448,13 @@ export default { | ||||
|     resetAddTaskDialog() { | ||||
|       this.$refs.newTaskFormRef.resetFields(); | ||||
|       this.newTaskForm = { | ||||
|         type: 'delay_task', // Default to delay_task | ||||
|         type: 'delay_task',  | ||||
|         name: '', | ||||
|         description: '', | ||||
|         parameters: {}, | ||||
|         parameters: { delay_duration: 1 }, // Reset with default delay_duration | ||||
|       }; | ||||
|     }, | ||||
|     editTask(task) { | ||||
|       // TODO: Implement edit task logic, possibly with a separate dialog | ||||
|       ElMessage.info('编辑任务功能正在开发中'); | ||||
|       console.log('编辑任务:', task); | ||||
|     }, | ||||
| @@ -467,11 +465,10 @@ export default { | ||||
|         type: 'warning' | ||||
|       }).then(() => { | ||||
|         this.plan.tasks = this.plan.tasks.filter(task => task.id !== taskToDelete.id); | ||||
|         this.plan.tasks.forEach((item, index) => item.execution_order = index + 1); // Re-order | ||||
|         this.updateContentType(); // Update content type | ||||
|         this.plan.tasks.forEach((item, index) => item.execution_order = index + 1);  | ||||
|         this.updateContentType();  | ||||
|         ElMessage.success('任务已删除'); | ||||
|       }).catch(() => { | ||||
|         // User cancelled | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
| @@ -489,12 +486,23 @@ export default { | ||||
| .retry-btn { | ||||
|   margin-top: 15px; | ||||
| } | ||||
| .sub-plan-wrapper { | ||||
|   margin-bottom: 10px;  | ||||
| } | ||||
| .sub-plan-container { | ||||
|   margin-left: 20px; | ||||
|   margin-top: 10px; | ||||
|   border-left: 2px solid #ebeef5; /* 增加视觉区分 */ | ||||
|   border-left: 2px solid #ebeef5;  | ||||
|   padding-left: 10px; | ||||
| } | ||||
| .sub-plan-card-content { | ||||
|   display: flex; | ||||
|   flex-direction: column;  | ||||
|   gap: 10px;  | ||||
| } | ||||
| .sub-plan-actions { | ||||
|   align-self: flex-end;  | ||||
| } | ||||
| .card-header { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
| @@ -502,6 +510,6 @@ export default { | ||||
| } | ||||
| /* 调整子计划卡片内部的header,避免重复样式 */ | ||||
| .sub-plan-container .card-header { | ||||
|   padding: 0; /* 移除内部card-header的padding */ | ||||
|   padding: 0;  | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -2,20 +2,20 @@ | ||||
|   <div class="delay-task-editor"> | ||||
|     <el-form-item | ||||
|       label="延时时间(秒)" | ||||
|       :prop="propPrefix + 'parameters.delay_seconds'" | ||||
|       :prop="propPath" | ||||
|       :rules="isEditing ? { | ||||
|         required: true, | ||||
|         message: '请输入延时时间', | ||||
|         trigger: 'blur' | ||||
|       } : null" | ||||
|       class="delay-time-form-item" | ||||
|     > | ||||
|       <el-input-number | ||||
|         :model-value="delaySeconds" | ||||
|         @update:model-value="updateDelaySeconds" | ||||
|         :model-value="delayDuration" | ||||
|         @update:model-value="updateDelayDuration" | ||||
|         :min="1" | ||||
|         placeholder="请输入延时时间" | ||||
|         style="width: 100%;" | ||||
|         :disabled="!isEditing" | ||||
|       ></el-input-number> | ||||
|     </el-form-item> | ||||
|   </div> | ||||
| @@ -30,10 +30,10 @@ export default { | ||||
|       type: Object, | ||||
|       required: true, | ||||
|     }, | ||||
|     // A prefix for the form item prop to ensure unique validation | ||||
|     propPrefix: { | ||||
|     // The full prop path for validation, e.g., 'parameters.delay_duration' | ||||
|     propPath: { | ||||
|       type: String, | ||||
|       default: "", | ||||
|       default: "parameters.delay_duration", // Updated default prop path | ||||
|     }, | ||||
|     // New prop to control editability | ||||
|     isEditing: { | ||||
| @@ -43,15 +43,15 @@ export default { | ||||
|   }, | ||||
|   emits: ["update:parameters"], | ||||
|   computed: { | ||||
|     delaySeconds() { | ||||
|       return this.parameters?.delay_seconds || 1; | ||||
|     delayDuration() { // Renamed from delaySeconds | ||||
|       return this.parameters?.delay_duration || 1; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     updateDelaySeconds(value) { | ||||
|     updateDelayDuration(value) { // Renamed from updateDelaySeconds | ||||
|       this.$emit("update:parameters", { | ||||
|         ...this.parameters, | ||||
|         delay_seconds: value, | ||||
|         delay_duration: value, | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
| @@ -62,4 +62,7 @@ export default { | ||||
| .delay-task-editor { | ||||
|   width: 100%; | ||||
| } | ||||
| </style> | ||||
| .delay-time-form-item :deep(.el-form-item__label) { | ||||
|   white-space: nowrap; | ||||
| } | ||||
| </style> | ||||
		Reference in New Issue
	
	Block a user