Compare commits

...

2 Commits

Author SHA1 Message Date
e10aa07367 feat(project): 实现任务和里程碑管理功能
Some checks failed
Lint Code / Lint Code (push) Failing after 2m14s
- 新增项目里程碑相关API接口,包括增删改查和状态进度更新
- 新增项目任务相关API接口,支持任务列表查询及增删改查
- 在项目详情页增加任务与里程碑权限控制及操作按钮展示
- 实现任务和里程碑的新增、编辑和删除模态框及表单校验
- 支持任务优先级、状态、进度、负责人等字段的管理和展示
- 里程碑支持关键标记、计划与实际日期、交付物等信息编辑
- 任务列表以表格形式展示,支持按状态展示颜色和标签
- 里程碑时间线增加操作按钮,支持权限校验后编辑和删除
- 任务状态显示对应颜色和文本,提升用户体验
- 优化项目详情页布局,新增任务列表分区和样式调整
2026-03-31 17:03:07 +08:00
df6970b71c feat(项目详情): 添加成员详情模态框并优化风险图表展示
feat: 在项目详情页添加成员详情模态框,支持点击头像查看详细信息
refactor: 重构风险图表展示逻辑,使用分类统计数据和状态数据
style: 为可点击元素添加悬停效果和指针样式
2026-03-31 16:26:26 +08:00
5 changed files with 2919 additions and 841 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 } }
);
};

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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" />