- 新增项目里程碑相关API接口,包括增删改查和状态进度更新 - 新增项目任务相关API接口,支持任务列表查询及增删改查 - 在项目详情页增加任务与里程碑权限控制及操作按钮展示 - 实现任务和里程碑的新增、编辑和删除模态框及表单校验 - 支持任务优先级、状态、进度、负责人等字段的管理和展示 - 里程碑支持关键标记、计划与实际日期、交付物等信息编辑 - 任务列表以表格形式展示,支持按状态展示颜色和标签 - 里程碑时间线增加操作按钮,支持权限校验后编辑和删除 - 任务状态显示对应颜色和文本,提升用户体验 - 优化项目详情页布局,新增任务列表分区和样式调整
This commit is contained in:
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user