feat(project): 实现任务和里程碑管理功能
Some checks failed
Lint Code / Lint Code (push) Failing after 2m14s

- 新增项目里程碑相关API接口,包括增删改查和状态进度更新
- 新增项目任务相关API接口,支持任务列表查询及增删改查
- 在项目详情页增加任务与里程碑权限控制及操作按钮展示
- 实现任务和里程碑的新增、编辑和删除模态框及表单校验
- 支持任务优先级、状态、进度、负责人等字段的管理和展示
- 里程碑支持关键标记、计划与实际日期、交付物等信息编辑
- 任务列表以表格形式展示,支持按状态展示颜色和标签
- 里程碑时间线增加操作按钮,支持权限校验后编辑和删除
- 任务状态显示对应颜色和文本,提升用户体验
- 优化项目详情页布局,新增任务列表分区和样式调整
This commit is contained in:
2026-03-31 17:03:07 +08:00
parent df6970b71c
commit e10aa07367
3 changed files with 2745 additions and 850 deletions

View File

@@ -441,3 +441,167 @@ export const getTaskStatus = (taskId: string) => {
`/api/v1/project-init/task/${taskId}` `/api/v1/project-init/task/${taskId}`
); );
}; };
// ==================== 里程碑管理 API ====================
/** 里程碑查询参数 */
export type MilestoneQueryParams = {
pageNum?: number;
pageSize?: number;
projectId?: string;
status?: string;
};
/** 分页查询里程碑列表 */
export const getMilestoneList = (params?: MilestoneQueryParams) => {
return http.request<Result<TableDataInfo<ProjectMilestone>>>(
"get",
"/api/v1/milestone/list",
{ params }
);
};
/** 根据ID查询里程碑详情 */
export const getMilestoneById = (id: string) => {
return http.request<Result<ProjectMilestone>>(
"get",
`/api/v1/milestone/${id}`
);
};
/** 新增里程碑 */
export const createMilestone = (data: ProjectMilestone) => {
return http.request<Result<string>>("post", "/api/v1/milestone", { data });
};
/** 修改里程碑 */
export const updateMilestone = (data: ProjectMilestone) => {
return http.request<Result<void>>("put", "/api/v1/milestone", { data });
};
/** 删除里程碑 */
export const deleteMilestone = (id: string) => {
return http.request<Result<void>>("delete", `/api/v1/milestone/${id}`);
};
/** 更新里程碑进度 */
export const updateMilestoneProgress = (id: string, progress: number) => {
return http.request<Result<void>>("put", `/api/v1/milestone/${id}/progress`, {
params: { progress }
});
};
/** 更新里程碑状态 */
export const updateMilestoneStatus = (id: string, status: string) => {
return http.request<Result<void>>("put", `/api/v1/milestone/${id}/status`, {
params: { status }
});
};
/** 查询已延期的关键里程碑 */
export const getDelayedKeyMilestones = (projectId: string) => {
return http.request<Result<ProjectMilestone[]>>(
"get",
"/api/v1/milestone/delayed-key",
{ params: { projectId } }
);
};
/** 查询即将到期的里程碑 */
export const getUpcomingMilestones = (projectId: string, days: number = 7) => {
return http.request<Result<ProjectMilestone[]>>(
"get",
"/api/v1/milestone/upcoming",
{ params: { projectId, days } }
);
};
/** 查询里程碑完成进度统计 */
export const getMilestoneProgressStats = (projectId: string) => {
return http.request<Result<Record<string, any>>>(
"get",
"/api/v1/milestone/stats/progress",
{ params: { projectId } }
);
};
// ==================== 任务管理 API ====================
/** 任务查询参数 */
export type TaskQueryParams = {
pageNum?: number;
pageSize?: number;
projectId?: string;
milestoneId?: string;
assigneeId?: string;
status?: string;
priority?: string;
keyword?: string;
};
/** 分页查询任务列表 */
export const getTaskList = (params?: TaskQueryParams) => {
return http.request<Result<TableDataInfo<ProjectTask>>>(
"get",
"/api/v1/task/list",
{ params }
);
};
/** 根据ID查询任务详情 */
export const getTaskById = (id: string) => {
return http.request<Result<ProjectTask>>("get", `/api/v1/task/${id}`);
};
/** 新增任务 */
export const createTask = (data: ProjectTask) => {
return http.request<Result<string>>("post", "/api/v1/task", { data });
};
/** 修改任务 */
export const updateTask = (data: ProjectTask) => {
return http.request<Result<void>>("put", "/api/v1/task", { data });
};
/** 删除任务 */
export const deleteTask = (id: string) => {
return http.request<Result<void>>("delete", `/api/v1/task/${id}`);
};
/** 查询我的待办任务 */
export const getMyTodoTasks = (userId: string, projectId?: string) => {
return http.request<Result<ProjectTask[]>>("get", "/api/v1/task/my-tasks", {
params: { userId, projectId }
});
};
/** 更新任务进度 */
export const updateTaskProgress = (id: string, progress: number) => {
return http.request<Result<void>>("put", `/api/v1/task/${id}/progress`, {
params: { progress }
});
};
/** 更新任务状态 */
export const updateTaskStatus = (id: string, status: string) => {
return http.request<Result<void>>("put", `/api/v1/task/${id}/status`, {
params: { status }
});
};
/** 查询任务依赖关系 */
export const getTaskDependencies = (id: string) => {
return http.request<Result<Record<string, any>[]>>(
"get",
`/api/v1/task/${id}/dependencies`
);
};
/** 统计项目任务状态分布 */
export const getTaskStatusStats = (projectId: string) => {
return http.request<Result<Record<string, any>[]>>(
"get",
"/api/v1/task/stats/status",
{ params: { projectId } }
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,12 @@ import { useRoute, useRouter } from "vue-router";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { import {
getProjectDetail, getProjectDetail,
createTask,
updateTask,
deleteTask,
createMilestone,
updateMilestone,
deleteMilestone,
type ProjectDetail, type ProjectDetail,
type ProjectMember, type ProjectMember,
type ProjectMilestone, type ProjectMilestone,
@@ -11,6 +17,7 @@ import {
type ProjectResource, type ProjectResource,
type ProjectRisk type ProjectRisk
} from "@/api/project"; } from "@/api/project";
import { hasPerms } from "@/utils/auth";
import { GGanttChart, GGanttRow } from "@infectoone/vue-ganttastic"; import { GGanttChart, GGanttRow } from "@infectoone/vue-ganttastic";
import { message } from "@/utils/message"; import { message } from "@/utils/message";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -39,6 +46,16 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
const projectId = ref<string>(route.params.id as string); const projectId = ref<string>(route.params.id as string);
// 权限控制 - 任务(基础权限)
const canCreateTask = computed(() => hasPerms("project:task:create"));
const canUpdateTask = computed(() => hasPerms("project:task:update"));
const canDeleteTask = computed(() => hasPerms("project:task:delete"));
// 权限控制 - 里程碑(基础权限)
const canCreateMilestone = computed(() => hasPerms("project:milestone:create"));
const canUpdateMilestone = computed(() => hasPerms("project:milestone:update"));
const canDeleteMilestone = computed(() => hasPerms("project:milestone:delete"));
// 加载状态 // 加载状态
const loading = ref(false); const loading = ref(false);
const ganttLoading = ref(false); const ganttLoading = ref(false);
@@ -63,6 +80,65 @@ const projectDetail = ref<ProjectDetail | null>(null);
const memberDetailModal = ref(false); const memberDetailModal = ref(false);
const selectedMember = ref<ProjectMember | null>(null); const selectedMember = ref<ProjectMember | null>(null);
// 任务编辑模态框
const taskEditModal = ref(false);
const taskEditForm = ref<ProjectTask>({
id: "",
taskName: "",
description: "",
taskType: "",
milestoneId: "",
assigneeId: "",
assigneeName: "",
planStartDate: "",
planEndDate: "",
actualStartDate: "",
actualEndDate: "",
planHours: 0,
actualHours: 0,
progress: 0,
priority: "medium",
status: "pending",
sortOrder: 0
});
const taskEditLoading = ref(false);
const isTaskEdit = ref(false); // true=编辑, false=新增
// 里程碑编辑模态框
const milestoneEditModal = ref(false);
const milestoneEditForm = ref<ProjectMilestone>({
id: "",
milestoneName: "",
description: "",
planDate: "",
actualDate: "",
status: "pending",
progress: 0,
isKey: 0,
deliverables: "",
sortOrder: 0
});
const milestoneEditLoading = ref(false);
const isMilestoneEdit = ref(false); // true=编辑, false=新增
// 权限控制 - 派生权限(需要放在 isTaskEdit/isMilestoneEdit 定义之后)
// 任务编辑权限:新增或编辑任一即可显示按钮
const canEditTask = computed(() => canCreateTask.value || canUpdateTask.value);
// 任务保存权限新增时需要create权限编辑时需要update权限
const canSaveTask = computed(() => {
return isTaskEdit.value ? canUpdateTask.value : canCreateTask.value;
});
// 里程碑编辑权限:新增或编辑任一即可显示按钮
const canEditMilestone = computed(
() => canCreateMilestone.value || canUpdateMilestone.value
);
// 里程碑保存权限新增时需要create权限编辑时需要update权限
const canSaveMilestone = computed(() => {
return isMilestoneEdit.value
? canUpdateMilestone.value
: canCreateMilestone.value;
});
// 项目基本信息(计算属性) // 项目基本信息(计算属性)
const projectInfo = computed(() => { const projectInfo = computed(() => {
const data = projectDetail.value; const data = projectDetail.value;
@@ -257,6 +333,42 @@ function getTaskColor(status?: string, progress?: number): string {
return "#409eff"; // 默认 - 蓝色 return "#409eff"; // 默认 - 蓝色
} }
// 获取任务状态标签类型
function getTaskStatusType(
status?: string
): "success" | "warning" | "info" | "primary" | "danger" {
switch (status) {
case "completed":
return "success";
case "in_progress":
case "ongoing":
return "primary";
case "pending":
return "warning";
case "delayed":
return "danger";
case "paused":
case "cancelled":
return "info";
default:
return "info";
}
}
// 获取任务状态文本
function getTaskStatusText(status?: string): string {
const statusMap: Record<string, string> = {
completed: "已完成",
in_progress: "进行中",
ongoing: "进行中",
pending: "待开始",
delayed: "延期",
paused: "暂停",
cancelled: "已取消"
};
return statusMap[status || ""] || "待开始";
}
// 将里程碑数据转换为 vue-ganttastic 格式 // 将里程碑数据转换为 vue-ganttastic 格式
const ganttMilestones = computed(() => { const ganttMilestones = computed(() => {
return milestoneList.value.map(milestone => ({ return milestoneList.value.map(milestone => ({
@@ -440,6 +552,153 @@ function getRiskLevelType(
} }
} }
// ==================== 任务编辑功能 ====================
/** 打开新增任务对话框 */
function openAddTaskModal() {
isTaskEdit.value = false;
taskEditForm.value = {
id: "",
taskName: "",
description: "",
taskType: "",
milestoneId: "",
assigneeId: "",
assigneeName: "",
planStartDate: "",
planEndDate: "",
actualStartDate: "",
actualEndDate: "",
planHours: 0,
actualHours: 0,
progress: 0,
priority: "medium",
status: "pending",
sortOrder: taskList.value.length
};
taskEditModal.value = true;
}
/** 打开编辑任务对话框 */
function openEditTaskModal(task: ProjectTask) {
isTaskEdit.value = true;
taskEditForm.value = { ...task };
taskEditModal.value = true;
}
/** 保存任务 */
async function saveTask() {
if (!taskEditForm.value.taskName) {
message("请输入任务名称", { type: "warning" });
return;
}
taskEditLoading.value = true;
try {
const data = {
...taskEditForm.value,
projectId: projectId.value
};
if (isTaskEdit.value) {
await updateTask(data);
message("任务更新成功", { type: "success" });
} else {
await createTask(data);
message("任务创建成功", { type: "success" });
}
taskEditModal.value = false;
await fetchProjectDetail();
} catch (error) {
console.error("保存任务失败:", error);
message("保存任务失败", { type: "error" });
} finally {
taskEditLoading.value = false;
}
}
/** 删除任务 */
async function handleDeleteTask(taskId: string) {
try {
await deleteTask(taskId);
message("任务删除成功", { type: "success" });
await fetchProjectDetail();
} catch (error) {
console.error("删除任务失败:", error);
message("删除任务失败", { type: "error" });
}
}
// ==================== 里程碑编辑功能 ====================
/** 打开新增里程碑对话框 */
function openAddMilestoneModal() {
isMilestoneEdit.value = false;
milestoneEditForm.value = {
id: "",
milestoneName: "",
description: "",
planDate: "",
actualDate: "",
status: "pending",
progress: 0,
isKey: 0,
deliverables: "",
sortOrder: milestoneList.value.length
};
milestoneEditModal.value = true;
}
/** 打开编辑里程碑对话框 */
function openEditMilestoneModal(milestone: ProjectMilestone) {
isMilestoneEdit.value = true;
milestoneEditForm.value = { ...milestone };
milestoneEditModal.value = true;
}
/** 保存里程碑 */
async function saveMilestone() {
if (!milestoneEditForm.value.milestoneName) {
message("请输入里程碑名称", { type: "warning" });
return;
}
if (!milestoneEditForm.value.planDate) {
message("请选择计划日期", { type: "warning" });
return;
}
milestoneEditLoading.value = true;
try {
const data = {
...milestoneEditForm.value,
projectId: projectId.value
};
if (isMilestoneEdit.value) {
await updateMilestone(data);
message("里程碑更新成功", { type: "success" });
} else {
await createMilestone(data);
message("里程碑创建成功", { type: "success" });
}
milestoneEditModal.value = false;
await fetchProjectDetail();
} catch (error) {
console.error("保存里程碑失败:", error);
message("保存里程碑失败", { type: "error" });
} finally {
milestoneEditLoading.value = false;
}
}
/** 删除里程碑 */
async function handleDeleteMilestone(milestoneId: string) {
try {
await deleteMilestone(milestoneId);
message("里程碑删除成功", { type: "success" });
await fetchProjectDetail();
} catch (error) {
console.error("删除里程碑失败:", error);
message("删除里程碑失败", { type: "error" });
}
}
onMounted(() => { onMounted(() => {
fetchProjectDetail(); fetchProjectDetail();
fetchGanttData(); fetchGanttData();
@@ -447,6 +706,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="project-detail-wrapper">
<div class="project-detail w-full" :style="marginStyle"> <div class="project-detail w-full" :style="marginStyle">
<!-- 顶部导航 --> <!-- 顶部导航 -->
<div class="flex-bc mb-4"> <div class="flex-bc mb-4">
@@ -491,7 +751,9 @@ onMounted(() => {
<div class="flex-bc"> <div class="flex-bc">
<div> <div>
<p class="text-gray-500 text-sm">项目进度</p> <p class="text-gray-500 text-sm">项目进度</p>
<p class="text-2xl font-bold mt-1">{{ projectInfo.progress }}%</p> <p class="text-2xl font-bold mt-1">
{{ projectInfo.progress }}%
</p>
<p class="text-xs text-green-500 mt-1"> <p class="text-xs text-green-500 mt-1">
<el-icon <el-icon
><component :is="useRenderIcon('ri/arrow-up-line')" ><component :is="useRenderIcon('ri/arrow-up-line')"
@@ -611,6 +873,17 @@ onMounted(() => {
<div class="flex-bc"> <div class="flex-bc">
<span class="font-medium">项目进度甘特图</span> <span class="font-medium">项目进度甘特图</span>
<div class="flex gap-2"> <div class="flex gap-2">
<el-button
v-if="canCreateTask"
type="primary"
size="small"
@click="openAddTaskModal"
>
<template #icon>
<component :is="useRenderIcon('ri/add-line')" />
</template>
新增任务
</el-button>
<el-button link @click="fetchGanttData"> <el-button link @click="fetchGanttData">
<component :is="useRenderIcon(RefreshIcon)" /> <component :is="useRenderIcon(RefreshIcon)" />
</el-button> </el-button>
@@ -646,7 +919,10 @@ onMounted(() => {
</div> </div>
<!-- 任务甘特图主体 - 使用 vue-ganttastic --> <!-- 任务甘特图主体 - 使用 vue-ganttastic -->
<div v-if="taskList.length > 0" class="gantt-chart-container mb-6"> <div
v-if="taskList.length > 0"
class="gantt-chart-container mb-6"
>
<g-gantt-chart <g-gantt-chart
:chart-start="ganttDateRange.start" :chart-start="ganttDateRange.start"
:chart-end="ganttDateRange.end" :chart-end="ganttDateRange.end"
@@ -667,9 +943,99 @@ onMounted(() => {
</div> </div>
<el-empty v-else description="暂无任务数据" class="mb-6" /> <el-empty v-else description="暂无任务数据" class="mb-6" />
<!-- 任务列表 -->
<div v-if="taskList.length > 0" class="task-list-section">
<div class="flex-bc mb-3">
<div class="flex items-center gap-2">
<el-icon :size="18" color="#409eff">
<component :is="useRenderIcon('ri/task-line')" />
</el-icon>
<span class="font-medium text-base">任务列表</span>
<el-tag size="small" type="info"
>{{ taskList.length }} </el-tag
>
</div>
</div>
<el-table :data="taskList" size="small" stripe>
<el-table-column label="任务名称" min-width="150">
<template #default="{ row }">
<div class="flex items-center gap-2">
<div
class="size-2 rounded-full"
:style="{
backgroundColor: getTaskColor(
row.status,
row.progress
)
}"
/>
<span class="font-medium">{{ row.taskName }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="负责人" width="100">
<template #default="{ row }">
{{ row.assigneeName || "未分配" }}
</template>
</el-table-column>
<el-table-column label="计划日期" width="180">
<template #default="{ row }">
<div class="text-xs">
{{ row.planStartDate || "-" }} ~
{{ row.planEndDate || "-" }}
</div>
</template>
</el-table-column>
<el-table-column label="进度" width="120">
<template #default="{ row }">
<el-progress
:percentage="row.progress || 0"
:status="row.progress === 100 ? 'success' : ''"
:stroke-width="6"
/>
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag
:type="getTaskStatusType(row.status)"
size="small"
>
{{ getTaskStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button
v-if="canUpdateTask"
link
type="primary"
size="small"
@click="openEditTaskModal(row)"
>
编辑
</el-button>
<el-popconfirm
v-if="canDeleteTask"
title="确定要删除该任务吗?"
@confirm="handleDeleteTask(row.id)"
>
<template #reference>
<el-button link type="danger" size="small">
删除
</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
<!-- 里程碑时间线 --> <!-- 里程碑时间线 -->
<div v-if="milestoneList.length > 0" class="milestone-section"> <div class="milestone-section">
<div class="flex items-center gap-2 mb-4"> <div class="flex-bc mb-4">
<div class="flex items-center gap-2">
<el-icon :size="18" color="#f56c6c"> <el-icon :size="18" color="#f56c6c">
<component :is="useRenderIcon('ri/flag-line')" /> <component :is="useRenderIcon('ri/flag-line')" />
</el-icon> </el-icon>
@@ -678,6 +1044,18 @@ onMounted(() => {
>{{ milestoneList.length }} </el-tag >{{ milestoneList.length }} </el-tag
> >
</div> </div>
<el-button
v-if="canCreateMilestone"
type="primary"
size="small"
@click="openAddMilestoneModal"
>
<template #icon>
<component :is="useRenderIcon('ri/add-line')" />
</template>
新增里程碑
</el-button>
</div>
<div class="milestone-timeline"> <div class="milestone-timeline">
<div <div
v-for="(milestone, index) in sortedMilestones" v-for="(milestone, index) in sortedMilestones"
@@ -755,6 +1133,32 @@ onMounted(() => {
<div v-if="milestone.description" class="milestone-desc"> <div v-if="milestone.description" class="milestone-desc">
{{ milestone.description }} {{ milestone.description }}
</div> </div>
<!-- 里程碑操作按钮 -->
<div
v-if="canUpdateMilestone || canDeleteMilestone"
class="milestone-actions"
>
<el-button
v-if="canUpdateMilestone"
link
type="primary"
size="small"
@click="openEditMilestoneModal(milestone)"
>
编辑
</el-button>
<el-popconfirm
v-if="canDeleteMilestone"
title="确定要删除该里程碑吗?"
@confirm="handleDeleteMilestone(milestone.id)"
>
<template #reference>
<el-button link type="danger" size="small">
删除
</el-button>
</template>
</el-popconfirm>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -800,7 +1204,10 @@ onMounted(() => {
</el-table-column> </el-table-column>
<el-table-column label="状态" width="100"> <el-table-column label="状态" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="getResourceStatusType(row.status)" size="small"> <el-tag
:type="getResourceStatusType(row.status)"
size="small"
>
{{ getResourceStatusText(row.status) }} {{ getResourceStatusText(row.status) }}
</el-tag> </el-tag>
</template> </template>
@@ -882,7 +1289,9 @@ onMounted(() => {
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="info-label">每周工时</span> <span class="info-label">每周工时</span>
<span class="info-value">{{ selectedMember.weeklyHours }} 小时</span> <span class="info-value"
>{{ selectedMember.weeklyHours }} 小时</span
>
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="info-label">加入日期</span> <span class="info-label">加入日期</span>
@@ -907,9 +1316,264 @@ onMounted(() => {
<el-empty description="暂无成员信息" /> <el-empty description="暂无成员信息" />
</div> </div>
</el-dialog> </el-dialog>
<!-- 任务编辑模态框 -->
<el-dialog
v-model="taskEditModal"
:title="isTaskEdit ? '编辑任务' : '新增任务'"
width="600px"
destroy-on-close
>
<el-form
ref="taskFormRef"
:model="taskEditForm"
label-width="100px"
class="task-edit-form"
>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="任务名称" prop="taskName" required>
<el-input
v-model="taskEditForm.taskName"
placeholder="请输入任务名称"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="任务类型">
<el-select
v-model="taskEditForm.taskType"
placeholder="请选择任务类型"
clearable
>
<el-option label="设计" value="design" />
<el-option label="开发" value="development" />
<el-option label="测试" value="testing" />
<el-option label="部署" value="deployment" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属里程碑">
<el-select
v-model="taskEditForm.milestoneId"
placeholder="请选择所属里程碑"
clearable
>
<el-option
v-for="ms in milestoneList"
:key="ms.id"
:label="ms.milestoneName"
:value="ms.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划开始">
<el-date-picker
v-model="taskEditForm.planStartDate"
type="date"
placeholder="选择开始日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划结束">
<el-date-picker
v-model="taskEditForm.planEndDate"
type="date"
placeholder="选择结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="负责人">
<el-input
v-model="taskEditForm.assigneeName"
placeholder="请输入负责人姓名"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="优先级">
<el-select
v-model="taskEditForm.priority"
placeholder="请选择优先级"
>
<el-option label="关键" value="critical" />
<el-option label="高" value="high" />
<el-option label="中" value="medium" />
<el-option label="低" value="low" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-select v-model="taskEditForm.status" placeholder="请选择状态">
<el-option label="待开始" value="pending" />
<el-option label="进行中" value="in_progress" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="进度">
<el-slider
v-model="taskEditForm.progress"
:max="100"
show-input
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划工时">
<el-input-number
v-model="taskEditForm.planHours"
:min="0"
placeholder="小时"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实际工时">
<el-input-number
v-model="taskEditForm.actualHours"
:min="0"
placeholder="小时"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="描述">
<el-input
v-model="taskEditForm.description"
type="textarea"
:rows="3"
placeholder="请输入任务描述"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="taskEditModal = false">取消</el-button>
<el-button
v-if="canSaveTask"
type="primary"
:loading="taskEditLoading"
@click="saveTask"
>
保存
</el-button>
</template>
</el-dialog>
<!-- 里程碑编辑模态框 -->
<el-dialog
v-model="milestoneEditModal"
:title="isMilestoneEdit ? '编辑里程碑' : '新增里程碑'"
width="500px"
destroy-on-close
>
<el-form
ref="milestoneFormRef"
:model="milestoneEditForm"
label-width="100px"
class="milestone-edit-form"
>
<el-form-item label="里程碑名称" prop="milestoneName" required>
<el-input
v-model="milestoneEditForm.milestoneName"
placeholder="请输入里程碑名称"
/>
</el-form-item>
<el-form-item label="计划日期" prop="planDate" required>
<el-date-picker
v-model="milestoneEditForm.planDate"
type="date"
placeholder="选择计划日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="实际日期">
<el-date-picker
v-model="milestoneEditForm.actualDate"
type="date"
placeholder="选择实际日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="状态">
<el-select
v-model="milestoneEditForm.status"
placeholder="请选择状态"
>
<el-option label="待开始" value="pending" />
<el-option label="进行中" value="in_progress" />
<el-option label="已完成" value="completed" />
<el-option label="延期" value="delayed" />
</el-select>
</el-form-item>
<el-form-item label="进度">
<el-slider
v-model="milestoneEditForm.progress"
:max="100"
show-input
/>
</el-form-item>
<el-form-item label="关键里程碑">
<el-switch
v-model="milestoneEditForm.isKey"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item label="交付物">
<el-input
v-model="milestoneEditForm.deliverables"
placeholder="请输入交付物"
/>
</el-form-item>
<el-form-item label="描述">
<el-input
v-model="milestoneEditForm.description"
type="textarea"
:rows="3"
placeholder="请输入描述"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="milestoneEditModal = false">取消</el-button>
<el-button
v-if="canSaveMilestone"
type="primary"
:loading="milestoneEditLoading"
@click="saveMilestone"
>
保存
</el-button>
</template>
</el-dialog>
</div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.project-detail-wrapper {
width: 100%;
}
.project-detail { .project-detail {
padding: 16px; padding: 16px;
} }
@@ -968,6 +1632,13 @@ onMounted(() => {
border-top: 1px dashed #e4e7ed; border-top: 1px dashed #e4e7ed;
} }
// 任务列表区域样式
.task-list-section {
padding-top: 16px;
margin-top: 16px;
border-top: 1px dashed #e4e7ed;
}
// 里程碑时间线样式 // 里程碑时间线样式
.milestone-timeline { .milestone-timeline {
padding: 8px 0; padding: 8px 0;
@@ -1016,6 +1687,12 @@ onMounted(() => {
padding-bottom: 8px; padding-bottom: 8px;
} }
.milestone-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.milestone-header { .milestone-header {
display: flex; display: flex;
gap: 8px; gap: 8px;