Compare commits
2 Commits
031dd03a62
...
e10aa07367
| Author | SHA1 | Date | |
|---|---|---|---|
| e10aa07367 | |||
| df6970b71c |
@@ -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 } }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -111,9 +111,9 @@ export type RiskStatisticsVO = {
|
|||||||
highCount: number;
|
highCount: number;
|
||||||
mediumCount: number;
|
mediumCount: number;
|
||||||
lowCount: number;
|
lowCount: number;
|
||||||
categoryStats: Record<string, number>;
|
categoryStats: Record<string, number | string>;
|
||||||
levelStats: Record<string, number>;
|
levelStats: Record<string, number | string>;
|
||||||
trendData: Record<string, number[]>;
|
trendData: Record<string, number[]> | null;
|
||||||
averageRiskScore: number;
|
averageRiskScore: number;
|
||||||
unresolvedHighCount: number;
|
unresolvedHighCount: number;
|
||||||
};
|
};
|
||||||
|
|||||||
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);
|
||||||
@@ -59,6 +76,69 @@ const marginStyle = computed(() => ({
|
|||||||
// 项目详情数据
|
// 项目详情数据
|
||||||
const projectDetail = ref<ProjectDetail | null>(null);
|
const projectDetail = ref<ProjectDetail | null>(null);
|
||||||
|
|
||||||
|
// 成员详情模态框
|
||||||
|
const memberDetailModal = ref(false);
|
||||||
|
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;
|
||||||
@@ -144,6 +224,12 @@ function goBack() {
|
|||||||
router.push("/project");
|
router.push("/project");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开成员详情模态框
|
||||||
|
function openMemberDetail(member: ProjectMember) {
|
||||||
|
selectedMember.value = member;
|
||||||
|
memberDetailModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取甘特图数据(现在使用项目详情中的任务数据)
|
// 获取甘特图数据(现在使用项目详情中的任务数据)
|
||||||
async function fetchGanttData() {
|
async function fetchGanttData() {
|
||||||
// 任务数据已从项目详情API获取,无需单独请求
|
// 任务数据已从项目详情API获取,无需单独请求
|
||||||
@@ -247,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 => ({
|
||||||
@@ -430,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();
|
||||||
@@ -437,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">
|
||||||
@@ -481,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')"
|
||||||
@@ -555,11 +827,13 @@ onMounted(() => {
|
|||||||
`https://api.dicebear.com/7.x/avataaars/svg?seed=${member.id}`
|
`https://api.dicebear.com/7.x/avataaars/svg?seed=${member.id}`
|
||||||
"
|
"
|
||||||
:title="getRoleText(member.roleCode)"
|
:title="getRoleText(member.roleCode)"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="openMemberDetail(member)"
|
||||||
/>
|
/>
|
||||||
<el-avatar
|
<el-avatar
|
||||||
v-if="memberList.length > 4"
|
v-if="memberList.length > 4"
|
||||||
:size="28"
|
:size="28"
|
||||||
class="bg-gray-200"
|
class="bg-gray-200 cursor-pointer"
|
||||||
>
|
>
|
||||||
+{{ memberList.length - 4 }}
|
+{{ memberList.length - 4 }}
|
||||||
</el-avatar>
|
</el-avatar>
|
||||||
@@ -599,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>
|
||||||
@@ -634,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"
|
||||||
@@ -655,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>
|
||||||
@@ -666,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"
|
||||||
@@ -743,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>
|
||||||
@@ -788,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>
|
||||||
@@ -817,9 +1236,344 @@ onMounted(() => {
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 成员详情模态框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="memberDetailModal"
|
||||||
|
title="成员详情"
|
||||||
|
width="500px"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<div v-if="selectedMember" class="member-detail-content">
|
||||||
|
<div class="flex items-center gap-4 mb-6">
|
||||||
|
<el-avatar
|
||||||
|
:size="80"
|
||||||
|
:src="
|
||||||
|
selectedMember.avatar ||
|
||||||
|
`https://api.dicebear.com/7.x/avataaars/svg?seed=${selectedMember.id}`
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold">{{ selectedMember.realName }}</h3>
|
||||||
|
<p class="text-gray-500">{{ selectedMember.userName }}</p>
|
||||||
|
<el-tag
|
||||||
|
size="small"
|
||||||
|
:type="
|
||||||
|
selectedMember.roleCode === 'manager'
|
||||||
|
? 'primary'
|
||||||
|
: selectedMember.roleCode === 'leader'
|
||||||
|
? 'success'
|
||||||
|
: 'info'
|
||||||
|
"
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
{{ getRoleText(selectedMember.roleCode) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<div class="member-info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">所属部门</span>
|
||||||
|
<span class="info-value">{{
|
||||||
|
selectedMember.department || "未设置"
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">职责</span>
|
||||||
|
<span class="info-value">{{
|
||||||
|
selectedMember.responsibility || "未设置"
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">每周工时</span>
|
||||||
|
<span class="info-value"
|
||||||
|
>{{ selectedMember.weeklyHours }} 小时</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">加入日期</span>
|
||||||
|
<span class="info-value">{{
|
||||||
|
selectedMember.joinDate || "未设置"
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">状态</span>
|
||||||
|
<span class="info-value">
|
||||||
|
<el-tag
|
||||||
|
size="small"
|
||||||
|
:type="selectedMember.status === 1 ? 'success' : 'danger'"
|
||||||
|
>
|
||||||
|
{{ selectedMember.status === 1 ? "活跃" : "非活跃" }}
|
||||||
|
</el-tag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center py-10">
|
||||||
|
<el-empty description="暂无成员信息" />
|
||||||
|
</div>
|
||||||
|
</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;
|
||||||
}
|
}
|
||||||
@@ -878,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;
|
||||||
@@ -926,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;
|
||||||
@@ -1006,4 +1773,39 @@ onMounted(() => {
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 成员详情模态框样式
|
||||||
|
.member-detail-content {
|
||||||
|
.member-info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标悬停效果
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -332,31 +332,43 @@ function initPieChart() {
|
|||||||
updatePieChart();
|
updatePieChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新饼图
|
// 更新饼图 - 使用 categoryStats 分类统计数据
|
||||||
function updatePieChart() {
|
function updatePieChart() {
|
||||||
if (!pieChart) return;
|
if (!pieChart) return;
|
||||||
const data = [
|
|
||||||
{
|
// 从 categoryStats 获取分类统计数据
|
||||||
value: statistics.value.criticalCount || 0,
|
const categoryStats = statistics.value.categoryStats || {};
|
||||||
name: "严重",
|
const categoryColors: Record<string, string> = {
|
||||||
itemStyle: { color: "#f56c6c" }
|
schedule: "#409eff", // 进度 - 蓝色
|
||||||
},
|
external: "#e6a23c", // 外部 - 橙色
|
||||||
{
|
technical: "#67c23a", // 技术 - 绿色
|
||||||
value: statistics.value.highCount || 0,
|
resource: "#909399", // 资源 - 灰色
|
||||||
name: "高",
|
personnel: "#f56c6c", // 人员 - 红色
|
||||||
itemStyle: { color: "#e6a23c" }
|
quality: "#9c27b0", // 质量 - 紫色
|
||||||
},
|
cost: "#ff9800", // 成本 - 深橙
|
||||||
{
|
other: "#795548" // 其他 - 棕色
|
||||||
value: statistics.value.mediumCount || 0,
|
};
|
||||||
name: "中",
|
const categoryNames: Record<string, string> = {
|
||||||
itemStyle: { color: "#409eff" }
|
schedule: "进度风险",
|
||||||
},
|
external: "外部风险",
|
||||||
{
|
technical: "技术风险",
|
||||||
value: statistics.value.lowCount || 0,
|
resource: "资源风险",
|
||||||
name: "低",
|
personnel: "人员风险",
|
||||||
itemStyle: { color: "#67c23a" }
|
quality: "质量风险",
|
||||||
}
|
cost: "成本风险",
|
||||||
].filter(item => item.value > 0);
|
other: "其他风险"
|
||||||
|
};
|
||||||
|
|
||||||
|
// 构建饼图数据
|
||||||
|
const data = Object.entries(categoryStats)
|
||||||
|
.map(([key, value]) => ({
|
||||||
|
value: parseInt(String(value)) || 0,
|
||||||
|
name: categoryNames[key] || key,
|
||||||
|
itemStyle: { color: categoryColors[key] || "#909399" }
|
||||||
|
}))
|
||||||
|
.filter(item => item.value > 0)
|
||||||
|
.sort((a, b) => b.value - a.value);
|
||||||
|
|
||||||
const option = {
|
const option = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "item",
|
trigger: "item",
|
||||||
@@ -371,7 +383,7 @@ function updatePieChart() {
|
|||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: "风险分布",
|
name: "风险分类分布",
|
||||||
type: "pie",
|
type: "pie",
|
||||||
radius: ["50%", "70%"],
|
radius: ["50%", "70%"],
|
||||||
center: ["50%", "45%"],
|
center: ["50%", "45%"],
|
||||||
@@ -394,7 +406,10 @@ function updatePieChart() {
|
|||||||
labelLine: {
|
labelLine: {
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
data
|
data:
|
||||||
|
data.length > 0
|
||||||
|
? data
|
||||||
|
: [{ value: 1, name: "暂无数据", itemStyle: { color: "#e0e0e0" } }]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -408,34 +423,61 @@ function initTrendChart() {
|
|||||||
updateTrendChart();
|
updateTrendChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新趋势图
|
// 更新趋势图 - 使用风险状态分布数据
|
||||||
function updateTrendChart() {
|
function updateTrendChart() {
|
||||||
if (!trendChart) return;
|
if (!trendChart) return;
|
||||||
const months = ["1月", "2月", "3月", "4月", "5月", "6月"];
|
|
||||||
|
// 使用状态统计数据展示风险状态分布
|
||||||
|
const statusData = [
|
||||||
|
{
|
||||||
|
name: "已识别",
|
||||||
|
value: statistics.value.identifiedCount || 0,
|
||||||
|
color: "#909399"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "已分派",
|
||||||
|
value: statistics.value.assignedCount || 0,
|
||||||
|
color: "#409eff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "缓解中",
|
||||||
|
value: statistics.value.mitigatingCount || 0,
|
||||||
|
color: "#e6a23c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "已解决",
|
||||||
|
value: statistics.value.resolvedCount || 0,
|
||||||
|
color: "#67c23a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "已关闭",
|
||||||
|
value: statistics.value.closedCount || 0,
|
||||||
|
color: "#13c2c2"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const option = {
|
const option = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "axis",
|
trigger: "axis",
|
||||||
axisPointer: { type: "shadow" }
|
axisPointer: { type: "shadow" },
|
||||||
},
|
formatter: (params: any) => {
|
||||||
legend: {
|
const data = params[0];
|
||||||
data: ["高风险", "中风险", "低风险"],
|
return `${data.name}: ${data.value} 个`;
|
||||||
right: 10,
|
}
|
||||||
top: 10,
|
|
||||||
itemWidth: 12,
|
|
||||||
itemHeight: 12
|
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: "3%",
|
left: "3%",
|
||||||
right: "4%",
|
right: "4%",
|
||||||
bottom: "3%",
|
bottom: "3%",
|
||||||
top: "15%",
|
top: "10%",
|
||||||
containLabel: true
|
containLabel: true
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
data: months,
|
data: statusData.map(item => item.name),
|
||||||
axisLine: { lineStyle: { color: "#dcdfe6" } },
|
axisLine: { lineStyle: { color: "#dcdfe6" } },
|
||||||
axisLabel: { color: "#606266" }
|
axisLabel: { color: "#606266", fontSize: 12 },
|
||||||
|
axisTick: { show: false }
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
@@ -445,25 +487,23 @@ function updateTrendChart() {
|
|||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: "高风险",
|
name: "风险数量",
|
||||||
type: "bar",
|
type: "bar",
|
||||||
stack: "total",
|
data: statusData.map((item, index) => ({
|
||||||
data: [5, 8, 6, 10, 12, 8],
|
value: item.value,
|
||||||
itemStyle: { color: "#f56c6c", borderRadius: [0, 0, 4, 4] }
|
itemStyle: {
|
||||||
},
|
color: item.color,
|
||||||
{
|
borderRadius: [4, 4, 0, 0]
|
||||||
name: "中风险",
|
}
|
||||||
type: "bar",
|
})),
|
||||||
stack: "total",
|
barWidth: "50%",
|
||||||
data: [8, 12, 10, 15, 18, 16],
|
label: {
|
||||||
itemStyle: { color: "#e6a23c" }
|
show: true,
|
||||||
},
|
position: "top",
|
||||||
{
|
color: "#606266",
|
||||||
name: "低风险",
|
fontSize: 12,
|
||||||
type: "bar",
|
formatter: "{c}"
|
||||||
stack: "total",
|
}
|
||||||
data: [10, 15, 12, 18, 20, 22],
|
|
||||||
itemStyle: { color: "#67c23a", borderRadius: [4, 4, 0, 0] }
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -708,7 +748,7 @@ onUnmounted(() => {
|
|||||||
<el-card shadow="hover" class="chart-card">
|
<el-card shadow="hover" class="chart-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex-bc">
|
<div class="flex-bc">
|
||||||
<span class="font-medium">风险分布</span>
|
<span class="font-medium">风险分类分布</span>
|
||||||
<el-button link>
|
<el-button link>
|
||||||
<component :is="useRenderIcon(MoreIcon)" />
|
<component :is="useRenderIcon(MoreIcon)" />
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -717,28 +757,46 @@ onUnmounted(() => {
|
|||||||
<div ref="pieChartRef" class="chart-container" />
|
<div ref="pieChartRef" class="chart-container" />
|
||||||
<div class="risk-legend">
|
<div class="risk-legend">
|
||||||
<div class="legend-item">
|
<div class="legend-item">
|
||||||
<span class="legend-dot" style="background-color: #f56c6c" />
|
<span class="legend-dot" style="background-color: #409eff" />
|
||||||
<span>严重</span>
|
<span>进度</span>
|
||||||
<span class="legend-value">{{
|
<span class="legend-value">{{
|
||||||
statistics.criticalCount || 0
|
statistics.categoryStats?.schedule || 0
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="legend-item">
|
<div class="legend-item">
|
||||||
<span class="legend-dot" style="background-color: #e6a23c" />
|
<span class="legend-dot" style="background-color: #e6a23c" />
|
||||||
<span>高</span>
|
<span>外部</span>
|
||||||
<span class="legend-value">{{ statistics.highCount || 0 }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="legend-item">
|
|
||||||
<span class="legend-dot" style="background-color: #409eff" />
|
|
||||||
<span>中</span>
|
|
||||||
<span class="legend-value">{{
|
<span class="legend-value">{{
|
||||||
statistics.mediumCount || 0
|
statistics.categoryStats?.external || 0
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="legend-item">
|
<div class="legend-item">
|
||||||
<span class="legend-dot" style="background-color: #67c23a" />
|
<span class="legend-dot" style="background-color: #67c23a" />
|
||||||
<span>低</span>
|
<span>技术</span>
|
||||||
<span class="legend-value">{{ statistics.lowCount || 0 }}</span>
|
<span class="legend-value">{{
|
||||||
|
statistics.categoryStats?.technical || 0
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #909399" />
|
||||||
|
<span>资源</span>
|
||||||
|
<span class="legend-value">{{
|
||||||
|
statistics.categoryStats?.resource || 0
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #f56c6c" />
|
||||||
|
<span>人员</span>
|
||||||
|
<span class="legend-value">{{
|
||||||
|
statistics.categoryStats?.personnel || 0
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-dot" style="background-color: #9c27b0" />
|
||||||
|
<span>质量</span>
|
||||||
|
<span class="legend-value">{{
|
||||||
|
statistics.categoryStats?.quality || 0
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -747,7 +805,7 @@ onUnmounted(() => {
|
|||||||
<el-card shadow="hover" class="chart-card">
|
<el-card shadow="hover" class="chart-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<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-radio-group v-model="trendPeriod" size="small">
|
<el-radio-group v-model="trendPeriod" size="small">
|
||||||
<el-radio-button label="月度" value="month" />
|
<el-radio-button label="月度" value="month" />
|
||||||
|
|||||||
Reference in New Issue
Block a user