feat(项目详情): 添加成员详情模态框并优化风险图表展示

feat: 在项目详情页添加成员详情模态框,支持点击头像查看详细信息
refactor: 重构风险图表展示逻辑,使用分类统计数据和状态数据
style: 为可点击元素添加悬停效果和指针样式
This commit is contained in:
2026-03-31 16:26:26 +08:00
parent 031dd03a62
commit df6970b71c
3 changed files with 257 additions and 74 deletions

View File

@@ -111,9 +111,9 @@ export type RiskStatisticsVO = {
highCount: number;
mediumCount: number;
lowCount: number;
categoryStats: Record<string, number>;
levelStats: Record<string, number>;
trendData: Record<string, number[]>;
categoryStats: Record<string, number | string>;
levelStats: Record<string, number | string>;
trendData: Record<string, number[]> | null;
averageRiskScore: number;
unresolvedHighCount: number;
};

View File

@@ -59,6 +59,10 @@ const marginStyle = computed(() => ({
// 项目详情数据
const projectDetail = ref<ProjectDetail | null>(null);
// 成员详情模态框
const memberDetailModal = ref(false);
const selectedMember = ref<ProjectMember | null>(null);
// 项目基本信息(计算属性)
const projectInfo = computed(() => {
const data = projectDetail.value;
@@ -144,6 +148,12 @@ function goBack() {
router.push("/project");
}
// 打开成员详情模态框
function openMemberDetail(member: ProjectMember) {
selectedMember.value = member;
memberDetailModal.value = true;
}
// 获取甘特图数据(现在使用项目详情中的任务数据)
async function fetchGanttData() {
// 任务数据已从项目详情API获取无需单独请求
@@ -555,11 +565,13 @@ onMounted(() => {
`https://api.dicebear.com/7.x/avataaars/svg?seed=${member.id}`
"
:title="getRoleText(member.roleCode)"
class="cursor-pointer"
@click="openMemberDetail(member)"
/>
<el-avatar
v-if="memberList.length > 4"
:size="28"
class="bg-gray-200"
class="bg-gray-200 cursor-pointer"
>
+{{ memberList.length - 4 }}
</el-avatar>
@@ -817,6 +829,84 @@ onMounted(() => {
</el-col>
</el-row>
</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>
</template>
<style scoped lang="scss">
@@ -1006,4 +1096,39 @@ onMounted(() => {
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>

View File

@@ -332,31 +332,43 @@ function initPieChart() {
updatePieChart();
}
// 更新饼图
// 更新饼图 - 使用 categoryStats 分类统计数据
function updatePieChart() {
if (!pieChart) return;
const data = [
{
value: statistics.value.criticalCount || 0,
name: "严重",
itemStyle: { color: "#f56c6c" }
},
{
value: statistics.value.highCount || 0,
name: "高",
itemStyle: { color: "#e6a23c" }
},
{
value: statistics.value.mediumCount || 0,
name: "中",
itemStyle: { color: "#409eff" }
},
{
value: statistics.value.lowCount || 0,
name: "",
itemStyle: { color: "#67c23a" }
}
].filter(item => item.value > 0);
// 从 categoryStats 获取分类统计数据
const categoryStats = statistics.value.categoryStats || {};
const categoryColors: Record<string, string> = {
schedule: "#409eff", // 进度 - 蓝色
external: "#e6a23c", // 外部 - 橙色
technical: "#67c23a", // 技术 - 绿色
resource: "#909399", // 资源 - 灰色
personnel: "#f56c6c", // 人员 - 红色
quality: "#9c27b0", // 质量 - 紫色
cost: "#ff9800", // 成本 - 深橙
other: "#795548" // 其他 - 棕色
};
const categoryNames: Record<string, string> = {
schedule: "进度风险",
external: "外部风险",
technical: "技术风险",
resource: "资源风险",
personnel: "人员风险",
quality: "质量风险",
cost: "成本风险",
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 = {
tooltip: {
trigger: "item",
@@ -371,7 +383,7 @@ function updatePieChart() {
},
series: [
{
name: "风险分布",
name: "风险分类分布",
type: "pie",
radius: ["50%", "70%"],
center: ["50%", "45%"],
@@ -394,7 +406,10 @@ function updatePieChart() {
labelLine: {
show: false
},
data
data:
data.length > 0
? data
: [{ value: 1, name: "暂无数据", itemStyle: { color: "#e0e0e0" } }]
}
]
};
@@ -408,34 +423,61 @@ function initTrendChart() {
updateTrendChart();
}
// 更新趋势图
// 更新趋势图 - 使用风险状态分布数据
function updateTrendChart() {
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 = {
tooltip: {
trigger: "axis",
axisPointer: { type: "shadow" }
},
legend: {
data: ["高风险", "中风险", "低风险"],
right: 10,
top: 10,
itemWidth: 12,
itemHeight: 12
axisPointer: { type: "shadow" },
formatter: (params: any) => {
const data = params[0];
return `${data.name}: ${data.value}`;
}
},
grid: {
left: "3%",
right: "4%",
bottom: "3%",
top: "15%",
top: "10%",
containLabel: true
},
xAxis: {
type: "category",
data: months,
data: statusData.map(item => item.name),
axisLine: { lineStyle: { color: "#dcdfe6" } },
axisLabel: { color: "#606266" }
axisLabel: { color: "#606266", fontSize: 12 },
axisTick: { show: false }
},
yAxis: {
type: "value",
@@ -445,25 +487,23 @@ function updateTrendChart() {
},
series: [
{
name: "风险",
name: "风险数量",
type: "bar",
stack: "total",
data: [5, 8, 6, 10, 12, 8],
itemStyle: { color: "#f56c6c", borderRadius: [0, 0, 4, 4] }
},
{
name: "中风险",
type: "bar",
stack: "total",
data: [8, 12, 10, 15, 18, 16],
itemStyle: { color: "#e6a23c" }
},
{
name: "低风险",
type: "bar",
stack: "total",
data: [10, 15, 12, 18, 20, 22],
itemStyle: { color: "#67c23a", borderRadius: [4, 4, 0, 0] }
data: statusData.map((item, index) => ({
value: item.value,
itemStyle: {
color: item.color,
borderRadius: [4, 4, 0, 0]
}
})),
barWidth: "50%",
label: {
show: true,
position: "top",
color: "#606266",
fontSize: 12,
formatter: "{c}"
}
}
]
};
@@ -708,7 +748,7 @@ onUnmounted(() => {
<el-card shadow="hover" class="chart-card">
<template #header>
<div class="flex-bc">
<span class="font-medium">风险分布</span>
<span class="font-medium">风险分类分</span>
<el-button link>
<component :is="useRenderIcon(MoreIcon)" />
</el-button>
@@ -717,28 +757,46 @@ onUnmounted(() => {
<div ref="pieChartRef" class="chart-container" />
<div class="risk-legend">
<div class="legend-item">
<span class="legend-dot" style="background-color: #f56c6c" />
<span>严重</span>
<span class="legend-dot" style="background-color: #409eff" />
<span>进度</span>
<span class="legend-value">{{
statistics.criticalCount || 0
statistics.categoryStats?.schedule || 0
}}</span>
</div>
<div class="legend-item">
<span class="legend-dot" style="background-color: #e6a23c" />
<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>外部</span>
<span class="legend-value">{{
statistics.mediumCount || 0
statistics.categoryStats?.external || 0
}}</span>
</div>
<div class="legend-item">
<span class="legend-dot" style="background-color: #67c23a" />
<span></span>
<span class="legend-value">{{ statistics.lowCount || 0 }}</span>
<span>技术</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>
</el-card>
@@ -747,7 +805,7 @@ onUnmounted(() => {
<el-card shadow="hover" class="chart-card">
<template #header>
<div class="flex-bc">
<span class="font-medium">风险趋势</span>
<span class="font-medium">风险状态分布</span>
<div class="flex gap-2">
<el-radio-group v-model="trendPeriod" size="small">
<el-radio-button label="月度" value="month" />