Files
pig-farm-controller-fe/src/components/PlanDetail.vue

577 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="plan-detail">
<div v-if="loading" class="loading">
<el-skeleton animated />
</div>
<div v-else-if="error" class="error">
<el-alert
:title="'加载计划内容失败 (ID: ' + planId + ')'"
:description="error"
type="error"
show-icon
@close="error = null"
/>
<el-button type="primary" @click="fetchPlan" class="retry-btn">重新加载</el-button>
</div>
<div v-else-if="plan && plan.id">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>{{ plan.name }} - 内容</span>
<div>
<template v-if="!isSubPlan">
<el-button class="button" type="primary" @click="savePlanContent" v-if="isEditingContent">保存</el-button>
<el-button class="button" type="danger" @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">
<el-button
v-if="plan.content_type === 'sub_plans' || !plan.content_type"
type="primary"
size="small"
@click="showAddSubPlanDialog"
>增加子计划</el-button>
<el-button
v-if="plan.content_type === 'tasks' || !plan.content_type"
type="primary"
size="small"
@click="showTaskEditorDialog()"
>增加子任务</el-button>
</template>
</div>
</div>
</template>
<!-- Display Tasks -->
<div v-if="plan.content_type === 'tasks'">
<h4>任务列表</h4>
<el-timeline v-if="plan.tasks.length > 0">
<el-timeline-item
v-for="(task, index) in plan.tasks"
:key="task.id || 'new-task-' + index"
:timestamp="'执行顺序: ' + (task.execution_order !== undefined ? task.execution_order : index + 1)"
placement="top"
>
<el-card>
<h5>{{ task.name }} ({{ task.type === 'waiting' ? '延时任务' : '未知任务' }})</h5>
<p>{{ task.description }}</p>
<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>
<el-button type="danger" size="small" @click="deleteTask(task)">删除</el-button>
</el-button-group>
</el-card>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无任务"></el-empty>
</div>
<!-- Display Sub-plans -->
<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-wrapper">
<el-card>
<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" class="sub-plan-actions">
<el-button type="danger" size="small" @click="deleteSubPlan(subPlan)">删除</el-button>
</el-button-group>
</div>
</el-card>
</div>
</div>
<el-empty v-else description="暂无子计划"></el-empty>
</div>
<div v-else-if="!plan.content_type">
<el-empty description="请添加子计划或子任务"></el-empty>
</div>
</el-card>
</div>
<div v-else>
<el-empty description="没有计划数据"></el-empty>
</div>
<!-- Add Sub-plan Dialog -->
<el-dialog
v-model="addSubPlanDialogVisible"
title="选择子计划"
width="600px"
@close="resetAddSubPlanDialog"
>
<el-select
v-model="selectedSubPlanId"
placeholder="请选择一个计划作为子计划"
filterable
style="width: 100%;"
>
<el-option
v-for="item in availablePlans"
:key="item.id"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
<template #footer>
<span class="dialog-footer">
<el-button @click="addSubPlanDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmAddSubPlan">确定</el-button>
</span>
</template>
</el-dialog>
<!-- Task Editor Dialog (for Add and Edit) -->
<el-dialog
v-model="taskEditorDialogVisible"
:title="isEditingTask ? '编辑子任务' : '增加子任务'"
width="600px"
@close="resetTaskEditorDialog"
>
<el-form :model="currentTaskForm" ref="taskFormRef" :rules="taskFormRules" label-width="100px">
<el-form-item label="任务类型" prop="type">
<el-select v-model="currentTaskForm.type" placeholder="请选择任务类型" style="width: 100%;" :disabled="isEditingTask">
<!-- Only Delay Task for now -->
<el-option label="延时任务" value="delay_task"></el-option>
</el-select>
</el-form-item>
<el-form-item label="任务名称" prop="name">
<el-input v-model="currentTaskForm.name" placeholder="请输入任务名称"></el-input>
</el-form-item>
<el-form-item label="任务描述" prop="description">
<el-input type="textarea" v-model="currentTaskForm.description" placeholder="请输入任务描述"></el-input>
</el-form-item>
<!-- Dynamic task component for specific parameters -->
<template v-if="currentTaskForm.type === 'delay_task'">
<DelayTaskEditor
:parameters="currentTaskForm.parameters"
@update:parameters="val => currentTaskForm.parameters = val"
prop-path="parameters.delay_duration"
:is-editing="true"
/>
</template>
<!-- More task types can be rendered here -->
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="taskEditorDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmTaskEdit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import apiClient from '../api/index.js';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ArrowDown } from '@element-plus/icons-vue';
import DelayTaskEditor from './tasks/DelayTask.vue';
export default {
name: 'PlanDetail',
components: {
DelayTaskEditor,
// Self-reference for recursion
'plan-detail': this,
ArrowDown,
},
props: {
planId: {
type: [Number, String],
required: true,
},
isSubPlan: {
type: Boolean,
default: false,
},
},
data() {
return {
plan: {
id: null,
name: '',
description: '',
execution_type: 'automatic',
execute_num: 0,
cron_expression: '',
content_type: null,
sub_plans: [],
tasks: [],
},
loading: false,
error: null,
isEditingContent: false,
// Add Sub-plan dialog
addSubPlanDialogVisible: false,
selectedSubPlanId: null,
availablePlans: [],
// Task Editor dialog (for Add and Edit)
taskEditorDialogVisible: false,
isEditingTask: false,
editingTaskOriginalId: null,
currentTaskForm: {
type: 'delay_task',
name: '',
description: '',
parameters: {},
},
taskFormRules: {
type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
// Rule for delay_duration will be added/removed dynamically
},
};
},
computed: {
delayDurationRules() {
return [{ required: true, message: '请输入延时时间', trigger: 'blur' }];
},
},
watch: {
planId: {
immediate: true,
handler(newId) {
if (newId) {
this.fetchPlan();
}
},
},
'currentTaskForm.type'(newType) {
console.log("PlanDetail: currentTaskForm.type changed to", newType);
if (newType === 'delay_task') {
this.taskFormRules['parameters.delay_duration'] = this.delayDurationRules;
} else {
if (this.taskFormRules['parameters.delay_duration']) {
delete this.taskFormRules['parameters.delay_duration'];
console.log("PlanDetail: Removed delay_duration rule.");
}
if (this.currentTaskForm.parameters.delay_duration !== undefined) {
delete this.currentTaskForm.parameters.delay_duration;
console.log("PlanDetail: Removed delay_duration parameter.");
}
}
},
},
methods: {
async fetchPlan() {
this.loading = true;
this.error = null;
try {
const response = await apiClient.plans.get(this.planId);
this.plan = {
...response.data,
sub_plans: response.data.sub_plans || [],
tasks: response.data.tasks || [],
};
this.updateContentType();
} catch (err) {
this.error = err.message || '未知错误';
console.error(`加载计划 (ID: ${this.planId}) 失败:`, err);
} finally {
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;
}
},
enterEditMode() {
this.isEditingContent = true;
console.log("PlanDetail: Entered edit mode.");
},
async savePlanContent() {
this.updateContentType();
try {
const submitData = {
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,
sub_plan_ids: this.plan.content_type === 'sub_plans'
? 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,
parameters: task.parameters || {},
}))
: [],
};
delete submitData.execute_count;
delete submitData.status;
console.log("PlanDetail: Submitting data", submitData);
await apiClient.plans.update(this.planId, submitData);
ElMessage.success('计划内容已保存');
this.isEditingContent = false;
this.fetchPlan();
} catch (error) {
ElMessage.error('保存计划内容失败: ' + (error.message || '未知错误'));
console.error('保存计划内容失败:', error);
}
},
async cancelEdit() {
console.log("PlanDetail: Cancelled edit, re-fetching plan.");
await this.fetchPlan();
this.isEditingContent = false;
ElMessage.info('已取消编辑');
},
// --- Sub-plan related methods ---
async showAddSubPlanDialog() {
if (this.plan.tasks.length > 0) {
try {
await ElMessageBox.confirm('当前计划包含任务,添加子计划将清空现有任务。是否继续?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
this.plan.tasks = [];
} catch (e) {
return;
}
}
this.addSubPlanDialogVisible = true;
await this.fetchAvailablePlans();
},
async fetchAvailablePlans() {
try {
const response = await apiClient.plans.list();
this.availablePlans = response.data.plans.filter(p =>
p.id !== this.planId
);
} catch (error) {
ElMessage.error('加载可用计划失败: ' + (error.message || '未知错误'));
console.error('加载可用计划失败:', error);
}
},
confirmAddSubPlan() {
if (!this.selectedSubPlanId) {
ElMessage.warning('请选择一个子计划');
return;
}
const selectedPlan = this.availablePlans.find(p => p.id === this.selectedSubPlanId);
if (selectedPlan) {
this.plan.sub_plans.push({
id: Date.now(),
child_plan_id: selectedPlan.id,
child_plan: selectedPlan,
execution_order: this.plan.sub_plans.length + 1,
});
this.updateContentType();
ElMessage.success(`子计划 "${selectedPlan.name}" 已添加`);
this.addSubPlanDialogVisible = false;
this.resetAddSubPlanDialog();
} else {
ElMessage.error('未找到选中的计划');
}
},
resetAddSubPlanDialog() {
this.selectedSubPlanId = null;
this.availablePlans = [];
},
deleteSubPlan(subPlanToDelete) {
ElMessageBox.confirm(`确认删除子计划 "${subPlanToDelete.child_plan?.name || '未知子计划'}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
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);
this.updateContentType();
ElMessage.success('子计划已删除');
}).catch(() => {
});
},
// --- Task related methods ---
showTaskEditorDialog(task = null) {
console.log("PlanDetail: Showing task editor dialog.");
if (this.plan.sub_plans.length > 0 && !task) {
ElMessageBox.confirm('当前计划包含子计划,添加任务将清空现有子计划。是否继续?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.plan.sub_plans = [];
this.taskEditorDialogVisible = true;
this.prepareTaskForm(task);
}).catch(() => {
// User cancelled
});
return;
}
this.taskEditorDialogVisible = true;
this.prepareTaskForm(task);
},
prepareTaskForm(task = null) {
// Reset properties of the existing reactive object
this.currentTaskForm.type = 'delay_task';
this.currentTaskForm.name = '';
this.currentTaskForm.description = '';
if (task) {
this.isEditingTask = true;
this.editingTaskOriginalId = task.id;
// Update properties of the existing reactive object
this.currentTaskForm.type = task.type === 'waiting' ? 'delay_task' : task.type; // Convert backend type to UI type
this.currentTaskForm.name = task.name;
this.currentTaskForm.description = task.description;
// Deep copy parameters to ensure reactivity for nested changes
this.currentTaskForm.parameters = JSON.parse(JSON.stringify(task.parameters || {}));
console.log("PlanDetail: Prepared currentTaskForm for editing:", JSON.parse(JSON.stringify(this.currentTaskForm)));
} else {
this.isEditingTask = false;
this.editingTaskOriginalId = null;
// For new tasks, ensure delay_duration is reactive from start
this.currentTaskForm.parameters = { delay_duration: null };
console.log("PlanDetail: Prepared currentTaskForm for adding:", JSON.parse(JSON.stringify(this.currentTaskForm)));
}
// Manually trigger watch for type to ensure rules and default parameters are set
this.updateTaskFormRules();
},
updateTaskFormRules() {
// Clear existing dynamic rules
if (this.taskFormRules['parameters.delay_duration']) {
delete this.taskFormRules['parameters.delay_duration'];
}
// Apply rules based on current type
if (this.currentTaskForm.type === 'delay_task') {
this.taskFormRules['parameters.delay_duration'] = this.delayDurationRules;
}
console.log("PlanDetail: Updated taskFormRules:", JSON.parse(JSON.stringify(this.taskFormRules)));
},
confirmTaskEdit() {
console.log("PlanDetail: confirmTaskEdit called. currentTaskForm before validation:", JSON.parse(JSON.stringify(this.currentTaskForm)));
this.$refs.taskFormRef.validate(async (valid) => {
console.log("PlanDetail: Form validation result:", valid);
if (valid) {
if (this.isEditingTask) {
// Find and update the existing task
const index = this.plan.tasks.findIndex(t => t.id === this.editingTaskOriginalId);
if (index !== -1) {
// Create a new task object to ensure reactivity
const updatedTask = {
...this.plan.tasks[index], // Keep existing properties
name: this.currentTaskForm.name,
description: this.currentTaskForm.description,
type: this.currentTaskForm.type === 'delay_task' ? 'waiting' : this.currentTaskForm.type,
parameters: { ...this.currentTaskForm.parameters }, // Deep copy parameters to ensure new reference
};
this.plan.tasks.splice(index, 1, updatedTask); // Replace the old task with the new one
ElMessage.success(`子任务 "${this.currentTaskForm.name}" 已更新`);
} else {
ElMessage.error('未找到要编辑的任务');
}
} else {
// Add a new task
const newTask = {
id: Date.now(),
execution_order: this.plan.tasks.length + 1,
type: this.currentTaskForm.type === 'delay_task' ? 'waiting' : this.currentTaskForm.type,
name: this.currentTaskForm.name,
description: this.currentTaskForm.description,
parameters: { ...this.currentTaskForm.parameters }, // Deep copy parameters to ensure new reference
};
this.plan.tasks = [...this.plan.tasks, newTask]; // Create a new array reference
ElMessage.success(`子任务 "${newTask.name}" 已添加`);
}
this.updateContentType();
this.taskEditorDialogVisible = false;
this.resetTaskEditorDialog();
}
});
},
resetTaskEditorDialog() {
console.log("PlanDetail: Resetting task editor dialog.");
this.$refs.taskFormRef.resetFields();
this.isEditingTask = false;
this.editingTaskOriginalId = null;
// Manually reset properties to ensure clean state for next use
this.currentTaskForm.type = 'delay_task';
this.currentTaskForm.name = '';
this.currentTaskForm.description = '';
this.currentTaskForm.parameters = {};
console.log("PlanDetail: currentTaskForm after full reset:", JSON.parse(JSON.stringify(this.currentTaskForm)));
this.updateTaskFormRules();
},
editTask(task) {
console.log('PlanDetail: Calling showTaskEditorDialog for editing task:', task);
this.showTaskEditorDialog(task);
},
deleteTask(taskToDelete) {
ElMessageBox.confirm(`确认删除任务 "${taskToDelete.name}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
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);
this.updateContentType();
ElMessage.success('任务已删除');
}).catch(() => {
});
},
},
};
</script>
<style scoped>
.plan-detail {
margin-top: 10px;
}
.loading, .error {
padding: 20px;
text-align: center;
}
.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;
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;
align-items: center;
}
/* 调整子计划卡片内部的header避免重复样式 */
.sub-plan-container .card-header {
padding: 0;
}
</style>