All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m2s
- 新增项目编辑功能(修改名称、标识) - 团队成员页面增加编辑按钮(管理员可修改姓名、邮箱、角色) - 项目详情接口性能优化:批量查询替代N+1,Git数据按仓库名过滤(8s→0.2s) - 侧边栏和图表改为显示项目名称而非标识 - 同步日志按时间倒序排列 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
130 lines
3.3 KiB
Vue
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>
|