编辑计划详情

This commit is contained in:
2025-09-22 18:10:12 +08:00
parent 4c6ca7d836
commit 3affadad0d
2 changed files with 293 additions and 120 deletions

View File

@@ -13,40 +13,49 @@
/> />
<el-button type="primary" @click="fetchPlan" class="retry-btn">重新加载</el-button> <el-button type="primary" @click="fetchPlan" class="retry-btn">重新加载</el-button>
</div> </div>
<div v-else-if="plan"> <div v-else-if="plan && plan.id">
<el-card class="box-card"> <el-card class="box-card">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>{{ plan.name }} - 内容</span> <span>{{ plan.name }} - 内容</span>
<div> <div>
<el-button class="button" @click="toggleEditMode">{{ isEditingContent ? '完成编辑' : '编辑内容' }}</el-button> <el-button class="button" @click="toggleEditMode" v-if="!isSubPlan">{{ isEditingContent ? '完成编辑' : '编辑内容' }}</el-button>
<el-button
v-if="isEditingContent && plan.content_type === 'sub_plans'" <!-- Dynamic Add Buttons -->
type="primary" <template v-if="isEditingContent">
size="small" <el-button
@click="showAddSubPlanDialog" v-if="plan.content_type === 'sub_plans' || !plan.content_type"
>增加子计划</el-button> type="primary"
<el-button size="small"
v-if="isEditingContent && plan.content_type === 'tasks'" @click="showAddSubPlanDialog"
type="primary" >增加子计划</el-button>
size="small" <el-button
@click="showAddTaskDialog" v-if="plan.content_type === 'tasks' || !plan.content_type"
>增加子任务</el-button> type="primary"
size="small"
@click="showAddTaskDialog"
>增加子任务</el-button>
</template>
</div> </div>
</div> </div>
</template> </template>
<!-- Display Tasks -->
<div v-if="plan.content_type === 'tasks'"> <div v-if="plan.content_type === 'tasks'">
<h4>任务列表</h4> <h4>任务列表</h4>
<el-timeline> <el-timeline v-if="plan.tasks.length > 0">
<el-timeline-item <el-timeline-item
v-for="(task, index) in sortedContent" v-for="(task, index) in plan.tasks"
:key="task.id || 'new-task-' + index" :key="task.id || 'new-task-' + index"
:timestamp="'执行顺序: ' + (task.order !== undefined ? task.order : index + 1)" :timestamp="'执行顺序: ' + (task.execution_order !== undefined ? task.execution_order : index + 1)"
placement="top" placement="top"
> >
<el-card> <el-card>
<h5>{{ task.name }}</h5> <h5>{{ task.name }} ({{ task.type === 'delay_task' ? '延时任务' : '未知任务' }})</h5>
<p>{{ task.description }}</p> <p>{{ task.description }}</p>
<p v-if="task.type === 'delay_task' && task.parameters?.delay_seconds">
延时: {{ task.parameters.delay_seconds }}
</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>
<el-button type="danger" size="small" @click="deleteTask(task)">删除</el-button> <el-button type="danger" size="small" @click="deleteTask(task)">删除</el-button>
@@ -54,24 +63,30 @@
</el-card> </el-card>
</el-timeline-item> </el-timeline-item>
</el-timeline> </el-timeline>
<el-empty v-if="!sortedContent.length && !isEditingContent" description="暂无任务"></el-empty> <el-empty v-else description="暂无任务"></el-empty>
</div> </div>
<!-- Display Sub-plans -->
<div v-else-if="plan.content_type === 'sub_plans'"> <div v-else-if="plan.content_type === 'sub_plans'">
<h4>子计划列表</h4> <h4>子计划列表</h4>
<div v-for="(subPlan, index) in sortedContent" :key="subPlan.id || 'new-subplan-' + index" class="sub-plan-container"> <div v-if="plan.sub_plans.length > 0">
<el-card> <div v-for="(subPlan, index) in plan.sub_plans" :key="subPlan.id || 'new-subplan-' + index" class="sub-plan-container">
<div class="card-header"> <el-card>
<plan-detail :plan-id="subPlan.id" /> <div class="card-header">
<el-button-group v-if="isEditingContent"> <!-- Pass child_plan_id to recursive PlanDetail -->
<el-button type="danger" size="small" @click="deleteSubPlan(subPlan)">删除</el-button> <plan-detail :plan-id="subPlan.child_plan_id" :is-sub-plan="true" />
</el-button-group> <el-button-group v-if="isEditingContent">
</div> <el-button type="danger" size="small" @click="deleteSubPlan(subPlan)">删除</el-button>
</el-card> </el-button-group>
</div>
</el-card>
</div>
</div> </div>
<el-empty v-if="!sortedContent.length && !isEditingContent" description="暂无子计划"></el-empty> <el-empty v-else description="暂无子计划"></el-empty>
</div> </div>
<div v-else>
<el-empty description="未知的计划内容类型"></el-empty> <div v-else-if="!plan.content_type">
<el-empty description="请添加子计划或子任务"></el-empty>
</div> </div>
</el-card> </el-card>
</div> </div>
@@ -79,7 +94,7 @@
<el-empty description="没有计划数据"></el-empty> <el-empty description="没有计划数据"></el-empty>
</div> </div>
<!-- 增加子计划对话框 --> <!-- Add Sub-plan Dialog -->
<el-dialog <el-dialog
v-model="addSubPlanDialogVisible" v-model="addSubPlanDialogVisible"
title="选择子计划" title="选择子计划"
@@ -107,7 +122,7 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 增加子任务对话框 --> <!-- Add Task Dialog -->
<el-dialog <el-dialog
v-model="addTaskDialogVisible" v-model="addTaskDialogVisible"
title="增加子任务" title="增加子任务"
@@ -117,9 +132,8 @@
<el-form :model="newTaskForm" ref="newTaskFormRef" :rules="newTaskRules" label-width="100px"> <el-form :model="newTaskForm" ref="newTaskFormRef" :rules="newTaskRules" label-width="100px">
<el-form-item label="任务类型" prop="type"> <el-form-item label="任务类型" prop="type">
<el-select v-model="newTaskForm.type" placeholder="请选择任务类型" style="width: 100%;"> <el-select v-model="newTaskForm.type" placeholder="请选择任务类型" style="width: 100%;">
<el-option label="普通任务" value="normal_task"></el-option> <!-- Only Delay Task for now -->
<el-option label="延时任务" value="delay_task"></el-option> <el-option label="延时任务" value="delay_task"></el-option>
<!-- 更多任务类型可以在这里添加 -->
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="任务名称" prop="name"> <el-form-item label="任务名称" prop="name">
@@ -128,21 +142,16 @@
<el-form-item label="任务描述" prop="description"> <el-form-item label="任务描述" prop="description">
<el-input type="textarea" v-model="newTaskForm.description" placeholder="请输入任务描述"></el-input> <el-input type="textarea" v-model="newTaskForm.description" placeholder="请输入任务描述"></el-input>
</el-form-item> </el-form-item>
<!-- 延时任务特有字段 --> <!-- Dynamic task component for specific parameters -->
<el-form-item <template v-if="newTaskForm.type === 'delay_task'">
v-if="newTaskForm.type === 'delay_task'" <DelayTaskEditor
label="延时时间(秒)" :parameters="newTaskForm.parameters"
prop="delay_seconds" @update:parameters="val => newTaskForm.parameters = val"
> prop-prefix="parameters."
<el-input-number :is-editing="true" /><!-- Always editable when adding a new task -->
v-model="newTaskForm.delay_seconds"
:min="1" </template>
:max="3600" <!-- More task types can be rendered here -->
placeholder="请输入延时时间"
style="width: 100%;"
></el-input-number>
</el-form-item>
<!-- 更多任务类型的特定表单项可以在这里动态渲染 -->
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
@@ -156,48 +165,69 @@
<script> <script>
import apiClient from '../api/index.js'; import apiClient from '../api/index.js';
import { ElMessage, ElMessageBox } from 'element-plus'; // 导入ElMessage和ElMessageBox 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
export default { export default {
name: 'PlanDetail', // 必须设置name属性以供递归调用 name: 'PlanDetail',
components: {
DelayTaskEditor, // Register the new component
// Self-reference for recursion
'plan-detail': this,
ArrowDown, // Register ArrowDown
},
props: { props: {
planId: { planId: {
type: [Number, String], type: [Number, String],
required: true, required: true,
}, },
isSubPlan: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
plan: null, plan: {
loading: false, id: null,
error: null,
isEditingContent: false, // 新增:是否处于编辑内容模式
addSubPlanDialogVisible: false, // 控制增加子计划对话框
selectedSubPlanId: null, // 选中的子计划ID
availablePlans: [], // 可供选择的计划列表
addTaskDialogVisible: false, // 控制增加子任务对话框
newTaskForm: { // 新任务表单数据
type: 'normal_task', // 默认任务类型
name: '', name: '',
description: '', description: '',
delay_seconds: 1, // 延时任务特有字段 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
}, },
newTaskRules: { // 新任务表单验证规则 loading: false,
error: null,
isEditingContent: false,
// Add Sub-plan dialog
addSubPlanDialogVisible: false,
selectedSubPlanId: null,
availablePlans: [],
// Add Task dialog
addTaskDialogVisible: false,
newTaskForm: {
type: 'delay_task', // Default to delay_task since normal_task is removed
name: '',
description: '',
parameters: {},
},
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' }],
delay_seconds: [{ required: true, message: '请输入延时时间', trigger: 'blur' }],
}, },
}; };
}, },
computed: { computed: {
sortedContent() { // Rules for delay_seconds, dynamically added/removed
if (this.plan && Array.isArray(this.plan.content)) { delaySecondsRules() {
// 假设 `content` 是一个包含 `order` 属性的对象的数组 return [{ required: true, message: '请输入延时时间', trigger: 'blur' }];
return [...this.plan.content].sort((a, b) => (a.order || 0) - (b.order || 0)); },
}
return [];
}
}, },
watch: { watch: {
planId: { planId: {
@@ -208,6 +238,26 @@ export default {
} }
}, },
}, },
// Watch for newTaskForm.type changes to update validation rules
'newTaskForm.type'(newType) {
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;
}
} else {
// Remove delay_seconds rule if not delay_task
if (this.newTaskRules['parameters.delay_seconds']) {
delete this.newTaskRules['parameters.delay_seconds'];
}
// Clear delay_seconds parameter if not delay_task
if (this.newTaskForm.parameters.delay_seconds) {
delete this.newTaskForm.parameters.delay_seconds;
}
}
},
}, },
methods: { methods: {
async fetchPlan() { async fetchPlan() {
@@ -215,11 +265,12 @@ export default {
this.error = null; this.error = null;
try { try {
const response = await apiClient.plans.get(this.planId); const response = await apiClient.plans.get(this.planId);
this.plan = response.data; this.plan = {
// 确保content是一个数组如果后端返回null或undefined ...response.data,
if (!this.plan.content) { sub_plans: response.data.sub_plans || [],
this.plan.content = []; tasks: response.data.tasks || [],
} };
this.updateContentType(); // Set initial content_type based on fetched data
} catch (err) { } catch (err) {
this.error = err.message || '未知错误'; this.error = err.message || '未知错误';
console.error(`加载计划 (ID: ${this.planId}) 失败:`, err); console.error(`加载计划 (ID: ${this.planId}) 失败:`, err);
@@ -227,34 +278,88 @@ export default {
this.loading = false; this.loading = false;
} }
}, },
updateContentType() {
if (this.plan.sub_plans.length > 0) {
this.plan.content_type = 'sub_plans';
} 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
}
},
toggleEditMode() { toggleEditMode() {
this.isEditingContent = !this.isEditingContent; this.isEditingContent = !this.isEditingContent;
if (!this.isEditingContent) { if (!this.isEditingContent) {
// 如果退出编辑模式,保存更改
this.savePlanContent(); this.savePlanContent();
} }
}, },
async savePlanContent() { async savePlanContent() {
this.updateContentType(); // Ensure content_type is correct before saving
try { try {
// 假设有一个API来更新计划的内容 const submitData = {
await apiClient.plans.updateContent(this.planId, this.plan.content); id: this.plan.id, // Ensure ID is included for update
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
: [],
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
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('计划内容已保存'); ElMessage.success('计划内容已保存');
this.fetchPlan(); // Re-fetch to get updated data from backend
} catch (error) { } catch (error) {
ElMessage.error('保存计划内容失败: ' + (error.message || '未知错误')); ElMessage.error('保存计划内容失败: ' + (error.message || '未知错误'));
console.error('保存计划内容失败:', error); console.error('保存计划内容失败:', error);
} }
}, },
// --- Sub-plan related methods ---
async showAddSubPlanDialog() { async showAddSubPlanDialog() {
// If there are existing tasks, warn the user
if (this.plan.tasks.length > 0) {
try {
await ElMessageBox.confirm('当前计划包含任务,添加子计划将清空现有任务。是否继续?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
this.plan.tasks = []; // Clear tasks
// No need to force content_type here, updateContentType will handle it
} catch (e) {
// User cancelled
return;
}
}
this.addSubPlanDialogVisible = true; this.addSubPlanDialogVisible = true;
await this.fetchAvailablePlans(); await this.fetchAvailablePlans();
}, },
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.content.some(sub => sub.id === p.id) !this.plan.sub_plans.some(sub => sub.child_plan_id === p.id)
); );
} catch (error) { } catch (error) {
ElMessage.error('加载可用计划失败: ' + (error.message || '未知错误')); ElMessage.error('加载可用计划失败: ' + (error.message || '未知错误'));
@@ -269,17 +374,13 @@ 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) {
// 确保content_type是sub_plans this.plan.sub_plans.push({
if (this.plan.content_type !== 'sub_plans') { id: Date.now(), // Temporary ID for UI, actual ID from backend after save
this.plan.content_type = 'sub_plans'; child_plan_id: selectedPlan.id,
this.plan.content = []; // 清空原有内容 child_plan: selectedPlan, // Store full plan for display if needed
} execution_order: this.plan.sub_plans.length + 1,
this.plan.content.push({
id: selectedPlan.id,
name: selectedPlan.name,
order: this.plan.content.length + 1, // 简单的排序
// 可以根据需要添加其他字段
}); });
this.updateContentType(); // Update content type
ElMessage.success(`子计划 "${selectedPlan.name}" 已添加`); ElMessage.success(`子计划 "${selectedPlan.name}" 已添加`);
this.addSubPlanDialogVisible = false; this.addSubPlanDialogVisible = false;
this.resetAddSubPlanDialog(); this.resetAddSubPlanDialog();
@@ -292,46 +393,53 @@ export default {
this.availablePlans = []; this.availablePlans = [];
}, },
deleteSubPlan(subPlanToDelete) { deleteSubPlan(subPlanToDelete) {
ElMessageBox.confirm(`确认删除子计划 "${subPlanToDelete.name}" 吗?`, '提示', { ElMessageBox.confirm(`确认删除子计划 "${subPlanToDelete.child_plan?.name || '未知子计划'}" 吗?`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.plan.content = this.plan.content.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.content.forEach((item, index) => item.order = index + 1); this.updateContentType(); // Update content type
ElMessage.success('子计划已删除'); ElMessage.success('子计划已删除');
}).catch(() => { }).catch(() => {
// 用户取消 // User cancelled
}); });
}, },
showAddTaskDialog() { // --- 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('当前计划包含子计划,添加任务将清空现有子计划。是否继续?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
this.plan.sub_plans = []; // Clear sub-plans
// No need to force content_type here, updateContentType will handle it
} catch (e) {
// User cancelled
return;
}
}
this.addTaskDialogVisible = true; this.addTaskDialogVisible = true;
}, },
confirmAddTask() { confirmAddTask() {
this.$refs.newTaskFormRef.validate(async (valid) => { this.$refs.newTaskFormRef.validate(async (valid) => {
if (valid) { if (valid) {
// 确保content_type是tasks
if (this.plan.content_type !== 'tasks') {
this.plan.content_type = 'tasks';
this.plan.content = []; // 清空原有内容
}
const newTask = { const newTask = {
id: Date.now(), // 临时ID实际应由后端生成 id: Date.now(), // Temporary ID for UI, actual ID from backend after save
order: this.plan.content.length + 1, execution_order: this.plan.tasks.length + 1,
type: this.newTaskForm.type, type: 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
}; };
if (this.newTaskForm.type === 'delay_task') { this.plan.tasks.push(newTask);
newTask.delay_seconds = this.newTaskForm.delay_seconds; this.updateContentType(); // Update content type
}
// 可以在这里根据任务类型添加更多字段
this.plan.content.push(newTask);
ElMessage.success(`子任务 "${newTask.name}" 已添加`); ElMessage.success(`子任务 "${newTask.name}" 已添加`);
this.addTaskDialogVisible = false; this.addTaskDialogVisible = false;
this.resetAddTaskDialog(); this.resetAddTaskDialog();
@@ -341,14 +449,14 @@ export default {
resetAddTaskDialog() { resetAddTaskDialog() {
this.$refs.newTaskFormRef.resetFields(); this.$refs.newTaskFormRef.resetFields();
this.newTaskForm = { this.newTaskForm = {
type: 'normal_task', type: 'delay_task', // Default to delay_task
name: '', name: '',
description: '', description: '',
delay_seconds: 1, parameters: {},
}; };
}, },
editTask(task) { editTask(task) {
// TODO: 实现编辑任务的逻辑,可能需要一个单独的对话框 // TODO: Implement edit task logic, possibly with a separate dialog
ElMessage.info('编辑任务功能正在开发中'); ElMessage.info('编辑任务功能正在开发中');
console.log('编辑任务:', task); console.log('编辑任务:', task);
}, },
@@ -358,12 +466,12 @@ export default {
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.plan.content = this.plan.content.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.content.forEach((item, index) => item.order = index + 1); this.updateContentType(); // Update content type
ElMessage.success('任务已删除'); ElMessage.success('任务已删除');
}).catch(() => { }).catch(() => {
// 用户取消 // User cancelled
}); });
}, },
}, },

View File

@@ -0,0 +1,65 @@
<template>
<div class="delay-task-editor">
<el-form-item
label="延时时间(秒)"
:prop="propPrefix + 'parameters.delay_seconds'"
:rules="isEditing ? {
required: true,
message: '请输入延时时间',
trigger: 'blur'
} : null"
>
<el-input-number
:model-value="delaySeconds"
@update:model-value="updateDelaySeconds"
:min="1"
placeholder="请输入延时时间"
style="width: 100%;"
:disabled="!isEditing"
></el-input-number>
</el-form-item>
</div>
</template>
<script>
export default {
name: "DelayTaskEditor",
props: {
// The parameters object for the task
parameters: {
type: Object,
required: true,
},
// A prefix for the form item prop to ensure unique validation
propPrefix: {
type: String,
default: "",
},
// New prop to control editability
isEditing: {
type: Boolean,
default: true,
},
},
emits: ["update:parameters"],
computed: {
delaySeconds() {
return this.parameters?.delay_seconds || 1;
},
},
methods: {
updateDelaySeconds(value) {
this.$emit("update:parameters", {
...this.parameters,
delay_seconds: value,
});
},
},
};
</script>
<style scoped>
.delay-task-editor {
width: 100%;
}
</style>