Files
pig-farm-controller-fe/src/components/PlanDetail.vue
2025-09-22 18:10:12 +08:00

508 lines
18 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>
<el-button class="button" @click="toggleEditMode" v-if="!isSubPlan">{{ isEditingContent ? '完成编辑' : '编辑内容' }}</el-button>
<!-- 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="showAddTaskDialog"
>增加子任务</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 === 'delay_task' ? '延时任务' : '未知任务' }})</h5>
<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 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-container">
<el-card>
<div class="card-header">
<!-- 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 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>
<!-- Add Task Dialog -->
<el-dialog
v-model="addTaskDialogVisible"
title="增加子任务"
width="600px"
@close="resetAddTaskDialog"
>
<el-form :model="newTaskForm" ref="newTaskFormRef" :rules="newTaskRules" label-width="100px">
<el-form-item label="任务类型" prop="type">
<el-select v-model="newTaskForm.type" placeholder="请选择任务类型" style="width: 100%;">
<!-- 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="newTaskForm.name" placeholder="请输入任务名称"></el-input>
</el-form-item>
<el-form-item label="任务描述" prop="description">
<el-input type="textarea" v-model="newTaskForm.description" placeholder="请输入任务描述"></el-input>
</el-form-item>
<!-- Dynamic task component for specific parameters -->
<template v-if="newTaskForm.type === 'delay_task'">
<DelayTaskEditor
:parameters="newTaskForm.parameters"
@update:parameters="val => newTaskForm.parameters = val"
prop-prefix="parameters."
:is-editing="true" /><!-- Always editable when adding a new task -->
</template>
<!-- More task types can be rendered here -->
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="addTaskDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmAddTask">确定</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'; // Keep ArrowDown for potential future use or if other dropdowns exist
import DelayTaskEditor from './tasks/DelayTask.vue'; // Import the new component
export default {
name: 'PlanDetail',
components: {
DelayTaskEditor, // Register the new component
// Self-reference for recursion
'plan-detail': this,
ArrowDown, // Register 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, // Will be 'sub_plans', 'tasks', or null
sub_plans: [], // Array of plan.SubPlanResponse
tasks: [], // Array of plan.TaskResponse
},
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' }],
name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
},
};
},
computed: {
// Rules for delay_seconds, dynamically added/removed
delaySecondsRules() {
return [{ required: true, message: '请输入延时时间', trigger: 'blur' }];
},
},
watch: {
planId: {
immediate: true,
handler(newId) {
if (newId) {
this.fetchPlan();
}
},
},
// 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: {
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(); // Set initial content_type based on fetched data
} 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; // Or an empty string to indicate no content type yet
}
},
toggleEditMode() {
this.isEditingContent = !this.isEditingContent;
if (!this.isEditingContent) {
this.savePlanContent();
}
},
async savePlanContent() {
this.updateContentType(); // Ensure content_type is correct before saving
try {
const submitData = {
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('计划内容已保存');
this.fetchPlan(); // Re-fetch to get updated data from backend
} catch (error) {
ElMessage.error('保存计划内容失败: ' + (error.message || '未知错误'));
console.error('保存计划内容失败:', error);
}
},
// --- Sub-plan related methods ---
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;
await this.fetchAvailablePlans();
},
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)
);
} 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(), // Temporary ID for UI, actual ID from backend after save
child_plan_id: selectedPlan.id,
child_plan: selectedPlan, // Store full plan for display if needed
execution_order: this.plan.sub_plans.length + 1,
});
this.updateContentType(); // Update content type
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); // Re-order
this.updateContentType(); // Update content type
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('当前计划包含子计划,添加任务将清空现有子计划。是否继续?', '警告', {
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;
},
confirmAddTask() {
this.$refs.newTaskFormRef.validate(async (valid) => {
if (valid) {
const newTask = {
id: Date.now(), // Temporary ID for UI, actual ID from backend after save
execution_order: this.plan.tasks.length + 1,
type: this.newTaskForm.type,
name: this.newTaskForm.name,
description: this.newTaskForm.description,
parameters: this.newTaskForm.parameters, // Include parameters
};
this.plan.tasks.push(newTask);
this.updateContentType(); // Update content type
ElMessage.success(`子任务 "${newTask.name}" 已添加`);
this.addTaskDialogVisible = false;
this.resetAddTaskDialog();
}
});
},
resetAddTaskDialog() {
this.$refs.newTaskFormRef.resetFields();
this.newTaskForm = {
type: 'delay_task', // Default to delay_task
name: '',
description: '',
parameters: {},
};
},
editTask(task) {
// TODO: Implement edit task logic, possibly with a separate dialog
ElMessage.info('编辑任务功能正在开发中');
console.log('编辑任务:', 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); // Re-order
this.updateContentType(); // Update content type
ElMessage.success('任务已删除');
}).catch(() => {
// User cancelled
});
},
},
};
</script>
<style scoped>
.plan-detail {
margin-top: 10px;
}
.loading, .error {
padding: 20px;
text-align: center;
}
.retry-btn {
margin-top: 15px;
}
.sub-plan-container {
margin-left: 20px;
margin-top: 10px;
border-left: 2px solid #ebeef5; /* 增加视觉区分 */
padding-left: 10px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
/* 调整子计划卡片内部的header避免重复样式 */
.sub-plan-container .card-header {
padding: 0; /* 移除内部card-header的padding */
}
</style>