devperf/frontend/src/components/charts/ProjectProgressBars.vue
zyc 10ed4f090d
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m2s
feat: 项目编辑、成员编辑、性能优化及UI改进
- 新增项目编辑功能(修改名称、标识)
- 团队成员页面增加编辑按钮(管理员可修改姓名、邮箱、角色)
- 项目详情接口性能优化:批量查询替代N+1,Git数据按仓库名过滤(8s→0.2s)
- 侧边栏和图表改为显示项目名称而非标识
- 同步日志按时间倒序排列

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:21:48 +08:00

130 lines
3.3 KiB
Vue

<script setup lang="ts">
import { computed, type PropType } from 'vue';
import { useECharts, CHART_COLORS } from '@/composables/useECharts';
import type { ProjectProgressItem } from '@/types';
const props = defineProps({
projects: {
type: Array as PropType<ProjectProgressItem[]>,
default: () => [],
},
});
const emit = defineEmits<{
(e: 'project-click', projectId: string): void;
}>();
const chartOptions = computed(() => {
const sorted = [...props.projects].sort(
(a, b) => b.currentCycleProgress - a.currentCycleProgress,
);
const names = sorted.map((p) => p.name);
const values = sorted.map((p) => p.currentCycleProgress);
const bgValues = sorted.map(() => 100);
return {
tooltip: {
trigger: 'axis' as const,
axisPointer: { type: 'shadow' as const },
formatter(params: any) {
const idx = params[0]?.dataIndex ?? 0;
const project = sorted[idx];
if (!project) return '';
return `
<strong>${project.name}</strong><br/>
进度: ${project.currentCycleProgress}%<br/>
${project.completedPoints}/${project.totalPoints}
`;
},
},
grid: { top: 8, right: 60, bottom: 8, left: 8, containLabel: true },
xAxis: {
type: 'value' as const,
max: 100,
show: false,
},
yAxis: {
type: 'category' as const,
data: names,
inverse: true,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
fontSize: 12,
color: '#1A1F2E',
width: 120,
overflow: 'truncate' as const,
},
},
series: [
{
type: 'bar' as const,
data: bgValues,
barWidth: 14,
barGap: '-100%',
itemStyle: { color: '#F3F4F6', borderRadius: 7 },
silent: true,
z: 1,
},
{
type: 'bar' as const,
data: values.map((v) => ({
value: v,
itemStyle: {
color: v >= 80 ? CHART_COLORS[1] : v >= 50 ? CHART_COLORS[0] : CHART_COLORS[2],
borderRadius: 7,
},
})),
barWidth: 14,
z: 2,
label: {
show: true,
position: 'right' as const,
formatter: '{c}%',
fontSize: 12,
fontWeight: 600,
color: '#1A1F2E',
fontFamily: 'Plus Jakarta Sans, sans-serif',
},
},
],
animationDuration: 500,
animationEasing: 'cubicOut',
};
});
const { chartRef, chart } = useECharts(chartOptions);
// Handle click to navigate to project detail
function handleClick() {
if (!chart.value) return;
chart.value.on('click', (params: any) => {
if (params.componentType === 'series' && params.seriesIndex === 1) {
const sorted = [...props.projects].sort(
(a, b) => b.currentCycleProgress - a.currentCycleProgress,
);
const project = sorted[params.dataIndex];
if (project) {
emit('project-click', project.projectId);
}
}
});
}
// Set up click handler after chart is ready
import { onMounted, watch } from 'vue';
watch(chart, (c) => { if (c) handleClick(); });
</script>
<template>
<div ref="chartRef" class="project-progress-bars" />
</template>
<style scoped>
.project-progress-bars {
width: 100%;
height: 100%;
min-height: 260px;
}
</style>