编辑计划详情

This commit is contained in:
2025-09-22 18:22:29 +08:00
parent 3affadad0d
commit 2deda9cb81
2 changed files with 101 additions and 90 deletions

View File

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

View File

@@ -2,20 +2,20 @@
<div class="delay-task-editor"> <div class="delay-task-editor">
<el-form-item <el-form-item
label="延时时间(秒)" label="延时时间(秒)"
:prop="propPrefix + 'parameters.delay_seconds'" :prop="propPath"
:rules="isEditing ? { :rules="isEditing ? {
required: true, required: true,
message: '请输入延时时间', message: '请输入延时时间',
trigger: 'blur' trigger: 'blur'
} : null" } : null"
class="delay-time-form-item"
> >
<el-input-number <el-input-number
:model-value="delaySeconds" :model-value="delayDuration"
@update:model-value="updateDelaySeconds" @update:model-value="updateDelayDuration"
:min="1" :min="1"
placeholder="请输入延时时间" placeholder="请输入延时时间"
style="width: 100%;" style="width: 100%;"
:disabled="!isEditing"
></el-input-number> ></el-input-number>
</el-form-item> </el-form-item>
</div> </div>
@@ -30,10 +30,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
// A prefix for the form item prop to ensure unique validation // The full prop path for validation, e.g., 'parameters.delay_duration'
propPrefix: { propPath: {
type: String, type: String,
default: "", default: "parameters.delay_duration", // Updated default prop path
}, },
// New prop to control editability // New prop to control editability
isEditing: { isEditing: {
@@ -43,15 +43,15 @@ export default {
}, },
emits: ["update:parameters"], emits: ["update:parameters"],
computed: { computed: {
delaySeconds() { delayDuration() { // Renamed from delaySeconds
return this.parameters?.delay_seconds || 1; return this.parameters?.delay_duration || 1;
}, },
}, },
methods: { methods: {
updateDelaySeconds(value) { updateDelayDuration(value) { // Renamed from updateDelaySeconds
this.$emit("update:parameters", { this.$emit("update:parameters", {
...this.parameters, ...this.parameters,
delay_seconds: value, delay_duration: value,
}); });
}, },
}, },
@@ -62,4 +62,7 @@ export default {
.delay-task-editor { .delay-task-editor {
width: 100%; width: 100%;
} }
.delay-time-form-item :deep(.el-form-item__label) {
white-space: nowrap;
}
</style> </style>