| | |
| | | <template> |
| | | <div class="milestone-list-container"> |
| | | <el-timeline> |
| | | <el-timeline-item |
| | | v-for="milestone in milestoneList" |
| | | :key="milestone.phaseId" |
| | | :timestamp="milestone.endDate" |
| | | > |
| | | <el-timeline-item v-for="milestone in milestoneList" |
| | | :key="milestone.phaseId" |
| | | :timestamp="milestone.endDate"> |
| | | <el-card> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>{{ milestone.phaseName }}</span> |
| | | <div class="milestone-actions"> |
| | | <el-button type="text" size="small" @click="handleEdit(milestone)">编辑</el-button> |
| | | <el-button type="text" size="small" @click="handleDelete(milestone)" danger>删除</el-button> |
| | | <el-button type="text" |
| | | size="small" |
| | | @click="handleEdit(milestone)">编辑</el-button> |
| | | <el-button type="text" |
| | | size="small" |
| | | @click="handleDelete(milestone)" |
| | | danger>删除</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | </el-card> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | |
| | | <!-- 无里程碑时的提示 --> |
| | | <div v-if="milestoneList.length === 0" class="empty-tip"> |
| | | <div v-if="milestoneList.length === 0" |
| | | class="empty-tip"> |
| | | <el-empty description="暂无里程碑数据" /> |
| | | </div> |
| | | |
| | | <!-- 编辑里程碑对话框 --> |
| | | <el-dialog |
| | | v-model="dialogVisible" |
| | | :title="'编辑里程碑: ' + (form.phaseName || '')" |
| | | width="600px" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <el-form |
| | | ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="里程碑名称" prop="phaseName"> |
| | | <el-input v-model="form.phaseName" placeholder="请输入里程碑名称" /> |
| | | <el-dialog v-model="dialogVisible" |
| | | :title="'编辑里程碑: ' + (form.phaseName || '')" |
| | | width="600px" |
| | | :close-on-click-modal="false"> |
| | | <el-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="100px"> |
| | | <el-form-item label="里程碑名称" |
| | | prop="phaseName"> |
| | | <el-input v-model="form.phaseName" |
| | | placeholder="请输入里程碑名称" /> |
| | | </el-form-item> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="开始日期" prop="startDate"> |
| | | <el-date-picker |
| | | v-model="form.startDate" |
| | | type="date" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="选择开始日期" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="结束日期" prop="endDate"> |
| | | <el-date-picker |
| | | v-model="form.endDate" |
| | | type="date" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="选择结束日期" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="状态" prop="status"> |
| | | <el-select v-model="form.status" placeholder="请选择状态"> |
| | | <el-option label="未开始" value="notStarted" /> |
| | | <el-option label="已完成" value="completed" /> |
| | | <el-option label="已延迟" value="delayed" /> |
| | | <el-col :span="12"> |
| | | <el-form-item label="开始日期" |
| | | prop="startDate"> |
| | | <el-date-picker v-model="form.startDate" |
| | | type="date" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="选择开始日期" |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="结束日期" |
| | | prop="endDate"> |
| | | <el-date-picker v-model="form.endDate" |
| | | type="date" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="选择结束日期" |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="状态" |
| | | prop="status"> |
| | | <el-select v-model="form.status" |
| | | placeholder="请选择状态"> |
| | | <el-option label="未开始" |
| | | value="notStarted" /> |
| | | <el-option label="已完成" |
| | | value="completed" /> |
| | | <el-option label="已延迟" |
| | | value="delayed" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" |
| | | @click="submitEditForm">确定</el-button> |
| | | <el-button @click="dialogVisible = false">取消</el-button> |
| | | <el-button type="primary" @click="submitEditForm">确定</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, watch, reactive } from 'vue'; |
| | | import { ElMessage, ElMessageBox } from 'element-plus'; |
| | | import { getProject, listProjectPhase, updateProjectPhase,delProjectPhase } from '@/api/oaSystem/projectManagement'; |
| | | import { ref, onMounted, watch, reactive } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { |
| | | getProject, |
| | | listProjectPhase, |
| | | updateProjectPhase, |
| | | delProjectPhase, |
| | | } from "@/api/oaSystem/projectManagement"; |
| | | |
| | | const props = defineProps({ |
| | | projectId: { |
| | | type: String, |
| | | required: true |
| | | } |
| | | }); |
| | | |
| | | const emit = defineEmits(['refresh']); |
| | | |
| | | const milestoneList = ref([]); |
| | | const dialogVisible = ref(false); |
| | | const formRef = ref(null); |
| | | const form = reactive({ |
| | | phaseId: '', |
| | | phaseName: '', |
| | | startDate: '', |
| | | endDate: '', |
| | | status: 'notStarted', |
| | | projectId: props.projectId |
| | | }); |
| | | |
| | | // 表单验证规则 |
| | | const rules = { |
| | | phaseName: [ |
| | | { required: true, message: '请输入里程碑名称', trigger: 'blur' }, |
| | | { min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' } |
| | | ], |
| | | status: [ |
| | | { required: true, message: '请选择状态', trigger: 'change' } |
| | | ] |
| | | }; |
| | | |
| | | // 获取里程碑列表 |
| | | const getMilestoneList = async () => { |
| | | try { |
| | | listProjectPhase(props.projectId).then(res => { |
| | | milestoneList.value = res.data.rows || res.data; |
| | | // 按目标日期排序 |
| | | // milestoneList.value.sort((a, b) => new Date(a.endDate) - new Date(b.endDate)); |
| | | }) |
| | | } catch (error) { |
| | | ElMessage.error('获取里程碑列表失败'); |
| | | console.error('获取里程碑列表失败:', error); |
| | | } |
| | | }; |
| | | |
| | | // 编辑里程碑 |
| | | const handleEdit = (milestone) => { |
| | | // 复制里程碑数据到表单 |
| | | Object.assign(form, { |
| | | phaseId: milestone.phaseId, |
| | | phaseName: milestone.phaseName, |
| | | description: milestone.description, |
| | | endDate: milestone.endDate, |
| | | status: milestone.status, |
| | | projectId: props.projectId |
| | | const props = defineProps({ |
| | | projectId: { |
| | | type: String, |
| | | required: true, |
| | | }, |
| | | }); |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | // 提交编辑表单 |
| | | const submitEditForm = async () => { |
| | | try { |
| | | await formRef.value.validate(); |
| | | |
| | | // 发送更新请求 |
| | | const res = await updateProjectPhase(form); |
| | | |
| | | if (res.code === 200) { |
| | | ElMessage.success('里程碑编辑成功'); |
| | | dialogVisible.value = false; |
| | | getMilestoneList(); // 刷新列表 |
| | | emit('refresh'); // 通知父组件刷新 |
| | | } else { |
| | | ElMessage.error(res.msg || '里程碑编辑失败'); |
| | | } |
| | | } catch (error) { |
| | | if (error.name === 'ValidationError') { |
| | | // 表单验证失败,Element Plus会自动提示 |
| | | return; |
| | | } |
| | | ElMessage.error('里程碑编辑失败'); |
| | | console.error('编辑里程碑失败:', error); |
| | | } |
| | | }; |
| | | const emit = defineEmits(["refresh"]); |
| | | |
| | | // 删除里程碑 |
| | | const handleDelete = (milestone) => { |
| | | ElMessageBox.confirm( |
| | | `确定要删除里程碑 "${milestone.phaseName}" 吗?删除后将无法恢复。`, |
| | | '删除确认', |
| | | { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | const milestoneList = ref([]); |
| | | const dialogVisible = ref(false); |
| | | const formRef = ref(null); |
| | | const form = reactive({ |
| | | phaseId: "", |
| | | phaseName: "", |
| | | startDate: "", |
| | | endDate: "", |
| | | status: "notStarted", |
| | | projectId: props.projectId, |
| | | }); |
| | | |
| | | // 表单验证规则 |
| | | const rules = { |
| | | phaseName: [ |
| | | { required: true, message: "请输入里程碑名称", trigger: "blur" }, |
| | | { min: 2, max: 50, message: "长度在 2 到 50 个字符", trigger: "blur" }, |
| | | ], |
| | | status: [{ required: true, message: "请选择状态", trigger: "change" }], |
| | | }; |
| | | |
| | | // 获取里程碑列表 |
| | | const getMilestoneList = async () => { |
| | | try { |
| | | listProjectPhase(props.projectId).then(res => { |
| | | milestoneList.value = res.data.rows || res.data; |
| | | // 按目标日期排序 |
| | | // milestoneList.value.sort((a, b) => new Date(a.endDate) - new Date(b.endDate)); |
| | | }); |
| | | } catch (error) { |
| | | ElMessage.error("获取里程碑列表失败"); |
| | | console.error("获取里程碑列表失败:", error); |
| | | } |
| | | ) |
| | | .then(async () => { |
| | | try { |
| | | // 调用删除API |
| | | const res = await delProjectPhase(milestone.phaseId); |
| | | |
| | | if (res.code === 200) { |
| | | ElMessage.success('里程碑删除成功'); |
| | | getMilestoneList(); // 刷新列表 |
| | | emit('refresh'); // 通知父组件刷新 |
| | | } else { |
| | | ElMessage.error(res.msg || '里程碑删除失败'); |
| | | } |
| | | } catch (error) { |
| | | ElMessage.error('里程碑删除失败'); |
| | | console.error('删除里程碑失败:', error); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | // 用户取消删除 |
| | | ElMessage.info('已取消删除'); |
| | | }; |
| | | |
| | | // 编辑里程碑 |
| | | const handleEdit = milestone => { |
| | | // 复制里程碑数据到表单 |
| | | Object.assign(form, { |
| | | phaseId: milestone.phaseId, |
| | | phaseName: milestone.phaseName, |
| | | description: milestone.description, |
| | | endDate: milestone.endDate, |
| | | status: milestone.status, |
| | | projectId: props.projectId, |
| | | }); |
| | | }; |
| | | |
| | | // 获取状态标签类型 |
| | | const getStatusType = (status) => { |
| | | const statusTypeMap = { |
| | | notStarted: 'info', |
| | | completed: 'success', |
| | | delayed: 'danger' |
| | | dialogVisible.value = true; |
| | | }; |
| | | return statusTypeMap[status] || 'default'; |
| | | }; |
| | | |
| | | // 获取状态文本 |
| | | const getStatusText = (status) => { |
| | | const statusTextMap = { |
| | | notStarted: '未开始', |
| | | completed: '已完成', |
| | | delayed: '已延迟' |
| | | // 提交编辑表单 |
| | | const submitEditForm = async () => { |
| | | try { |
| | | await formRef.value.validate(); |
| | | |
| | | // 发送更新请求 |
| | | const res = await updateProjectPhase(form); |
| | | |
| | | if (res.code === 200) { |
| | | ElMessage.success("里程碑编辑成功"); |
| | | dialogVisible.value = false; |
| | | getMilestoneList(); // 刷新列表 |
| | | emit("refresh"); // 通知父组件刷新 |
| | | } else { |
| | | ElMessage.error(res.msg || "里程碑编辑失败"); |
| | | } |
| | | } catch (error) { |
| | | if (error.name === "ValidationError") { |
| | | // 表单验证失败,Element Plus会自动提示 |
| | | return; |
| | | } |
| | | ElMessage.error("里程碑编辑失败"); |
| | | console.error("编辑里程碑失败:", error); |
| | | } |
| | | }; |
| | | return statusTextMap[status] || status; |
| | | }; |
| | | |
| | | // 监听项目ID变化 |
| | | watch(() => props.projectId, () => { |
| | | if (props.projectId) { |
| | | getMilestoneList(); |
| | | } |
| | | }); |
| | | // 删除里程碑 |
| | | const handleDelete = milestone => { |
| | | ElMessageBox.confirm( |
| | | `确定要删除里程碑 "${milestone.phaseName}" 吗?删除后将无法恢复。`, |
| | | "删除确认", |
| | | { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | } |
| | | ) |
| | | .then(async () => { |
| | | try { |
| | | // 调用删除API |
| | | const res = await delProjectPhase(milestone.phaseId); |
| | | |
| | | // 初始化 |
| | | onMounted(() => { |
| | | if (props.projectId) { |
| | | getMilestoneList(); |
| | | } |
| | | }); |
| | | if (res.code === 200) { |
| | | ElMessage.success("里程碑删除成功"); |
| | | getMilestoneList(); // 刷新列表 |
| | | emit("refresh"); // 通知父组件刷新 |
| | | } else { |
| | | ElMessage.error(res.msg || "里程碑删除失败"); |
| | | } |
| | | } catch (error) { |
| | | ElMessage.error("里程碑删除失败"); |
| | | console.error("删除里程碑失败:", error); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | // 用户取消删除 |
| | | ElMessage.info("已取消删除"); |
| | | }); |
| | | }; |
| | | |
| | | // 获取状态标签类型 |
| | | const getStatusType = status => { |
| | | const statusTypeMap = { |
| | | notStarted: "info", |
| | | completed: "success", |
| | | delayed: "danger", |
| | | }; |
| | | return statusTypeMap[status] || "default"; |
| | | }; |
| | | |
| | | // 获取状态文本 |
| | | const getStatusText = status => { |
| | | const statusTextMap = { |
| | | notStarted: "未开始", |
| | | completed: "已完成", |
| | | delayed: "已延迟", |
| | | }; |
| | | return statusTextMap[status] || status; |
| | | }; |
| | | |
| | | // 监听项目ID变化 |
| | | watch( |
| | | () => props.projectId, |
| | | () => { |
| | | if (props.projectId) { |
| | | getMilestoneList(); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | // 初始化 |
| | | onMounted(() => { |
| | | if (props.projectId) { |
| | | getMilestoneList(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .milestone-list-container { |
| | | padding: 10px 0; |
| | | } |
| | | .milestone-list-container { |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .milestone-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | .milestone-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .milestone-content { |
| | | padding: 10px 0; |
| | | } |
| | | .milestone-content { |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .milestone-status { |
| | | margin-top: 10px; |
| | | } |
| | | .milestone-status { |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .empty-tip { |
| | | margin-top: 40px; |
| | | text-align: center; |
| | | } |
| | | .empty-tip { |
| | | margin-top: 40px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 10px; |
| | | } |
| | | .dialog-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 10px; |
| | | } |
| | | </style> |