- 后端:Bun + Hono + Drizzle ORM + SQLite - 前端:Vue 3 + Naive UI + ECharts - 项目管理:创建项目 + 绑定 Git 仓库 - OKR 系统:目标/关键结果 CRUD + 进度追踪 - Git 同步:Gitea API 自动同步 commit/PR + 作者关联 - 数据看板:项目 OKR 进度 + KR 状态分布 + 代码活动 - 权限体系:admin/manager/developer/viewer 四级 - Docker 部署:docker-compose + nginx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
200 lines
6.9 KiB
Vue
200 lines
6.9 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, computed } from 'vue';
|
|
import { useRoute } from 'vue-router';
|
|
import { NSpin, NDataTable, NTag } from 'naive-ui';
|
|
import { getMemberDetailApi } from '@/api/members';
|
|
import DataCard from '@/components/shared/DataCard.vue';
|
|
import ContributionHeatmap from '@/components/charts/ContributionHeatmap.vue';
|
|
import { useECharts, CHART_COLORS } from '@/composables/useECharts';
|
|
import type { HeatmapDay } from '@/types';
|
|
|
|
const route = useRoute();
|
|
const loading = ref(true);
|
|
const data = ref<any>(null);
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const res = await getMemberDetailApi(route.params.id as string);
|
|
data.value = res.data.data;
|
|
} catch (err) {
|
|
console.error('Failed to load member:', err);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* B-14 fix: Map member API heatmap data to ContributionHeatmap's expected HeatmapDay format.
|
|
* Member Detail API returns contributionHeatmap.days which already has the correct fields.
|
|
*/
|
|
const heatmapDays = computed<HeatmapDay[]>(() => {
|
|
return data.value?.contributionHeatmap?.days || [];
|
|
});
|
|
|
|
// B-19 fix: Delivery trend line - ensure all sprint data points are shown
|
|
const trendOptions = computed(() => {
|
|
const cycles = data.value?.deliveryTrend?.cycles || [];
|
|
return {
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
formatter(params: any) {
|
|
const item = params[0];
|
|
const cycle = cycles[item.dataIndex];
|
|
if (!cycle) return '';
|
|
return `<strong>${cycle.name}</strong><br/>` +
|
|
`分配: ${cycle.assignedPoints} 点<br/>` +
|
|
`完成: ${cycle.completedPoints} 点<br/>` +
|
|
`交付率: ${cycle.rate}%`;
|
|
},
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
data: cycles.map((c: any) => c.name),
|
|
axisLabel: { fontSize: 11, interval: 0, rotate: cycles.length > 6 ? 30 : 0 },
|
|
},
|
|
yAxis: { type: 'value', max: 100, axisLabel: { formatter: '{value}%' } },
|
|
series: [{
|
|
type: 'line',
|
|
data: cycles.map((c: any) => c.rate),
|
|
smooth: true,
|
|
itemStyle: { color: CHART_COLORS[0] },
|
|
areaStyle: { color: 'rgba(59,89,152,0.1)' },
|
|
symbol: 'circle',
|
|
symbolSize: 8,
|
|
label: {
|
|
show: cycles.length <= 8,
|
|
formatter: '{c}%',
|
|
fontSize: 11,
|
|
},
|
|
}],
|
|
};
|
|
});
|
|
|
|
// KPI Radar
|
|
const radarOptions = computed(() => {
|
|
const kpi = data.value?.kpiScorecard;
|
|
if (!kpi) return {};
|
|
return {
|
|
radar: {
|
|
indicator: [
|
|
{ name: '交付率', max: 100 },
|
|
{ name: '交付速度', max: 100 },
|
|
{ name: '代码质量', max: 100 },
|
|
{ name: 'PR 效率', max: 100 },
|
|
{ name: '评审参与', max: 100 },
|
|
],
|
|
},
|
|
series: [{
|
|
type: 'radar',
|
|
data: [{
|
|
value: [
|
|
kpi.sprintDeliveryRate,
|
|
Math.max(0, 100 - kpi.avgDeliveryDays * 5),
|
|
Math.max(0, 100 - kpi.bugDensity * 100),
|
|
Math.max(0, 100 - kpi.prMergeTimeAvg),
|
|
kpi.reviewParticipation,
|
|
],
|
|
itemStyle: { color: CHART_COLORS[0] },
|
|
areaStyle: { color: 'rgba(59,89,152,0.2)' },
|
|
}],
|
|
}],
|
|
};
|
|
});
|
|
|
|
const { chartRef: trendRef } = useECharts(trendOptions);
|
|
const { chartRef: radarRef } = useECharts(radarOptions);
|
|
|
|
const taskColumns = [
|
|
{ title: '标题', key: 'title', ellipsis: { tooltip: true } },
|
|
{ title: '状态', key: 'status', width: 100, render: (row: any) => row.status },
|
|
{ title: '优先级', key: 'priority', width: 90 },
|
|
{ title: '点数', key: 'storyPoints', width: 70, align: 'center' as const },
|
|
{ title: '截止日期', key: 'dueDate', width: 110 },
|
|
];
|
|
|
|
/**
|
|
* B-20 fix: Add pagination to Current Tasks table.
|
|
* Default page size of 10 with option to expand.
|
|
*/
|
|
const taskPagination = {
|
|
pageSize: 10,
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="member-detail-page">
|
|
<NSpin :show="loading">
|
|
<template v-if="data">
|
|
<h2 style="margin-bottom: var(--space-4)">{{ data.member?.displayName }}</h2>
|
|
<p style="color: var(--color-text-secondary); margin-bottom: var(--space-5)">{{ data.member?.email }} - {{ data.member?.role }}</p>
|
|
|
|
<div class="grid-2">
|
|
<DataCard title="Sprint 交付率趋势" subtitle="最近 6 个 Sprint">
|
|
<div ref="trendRef" style="height: 240px" />
|
|
</DataCard>
|
|
|
|
<DataCard title="KPI 评分卡">
|
|
<div ref="radarRef" style="height: 240px" />
|
|
</DataCard>
|
|
</div>
|
|
|
|
<!-- B-14 fix: replaced placeholder with real ContributionHeatmap component -->
|
|
<DataCard title="贡献热力图" subtitle="最近 6 个月" style="margin-top: var(--space-5)">
|
|
<div style="height: 160px">
|
|
<ContributionHeatmap :days="heatmapDays" />
|
|
</div>
|
|
</DataCard>
|
|
|
|
<!-- B-20 fix: added pagination prop to NDataTable -->
|
|
<DataCard title="当前任务" style="margin-top: var(--space-5)">
|
|
<NDataTable
|
|
:columns="taskColumns"
|
|
:data="data.currentTasks || []"
|
|
:bordered="false"
|
|
:pagination="taskPagination"
|
|
size="small"
|
|
/>
|
|
</DataCard>
|
|
|
|
<!-- KPI Details -->
|
|
<div class="kpi-cards" style="margin-top: var(--space-5)">
|
|
<div class="kpi-card">
|
|
<span class="kpi-label">交付率</span>
|
|
<span class="kpi-value tabular-nums">{{ data.kpiScorecard?.sprintDeliveryRate || 0 }}%</span>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<span class="kpi-label">平均交付天数</span>
|
|
<span class="kpi-value tabular-nums">{{ data.kpiScorecard?.avgDeliveryDays || 0 }}d</span>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<span class="kpi-label">Bug 密度</span>
|
|
<span class="kpi-value tabular-nums">{{ data.kpiScorecard?.bugDensity || 0 }}</span>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<span class="kpi-label">PR 合入时间</span>
|
|
<span class="kpi-value tabular-nums">{{ data.kpiScorecard?.prMergeTimeAvg || 0 }}h</span>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<span class="kpi-label">连续活跃</span>
|
|
<span class="kpi-value tabular-nums">{{ data.kpiScorecard?.activityStreak || 0 }}d</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</NSpin>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-5); }
|
|
@media (max-width: 768px) { .grid-2 { grid-template-columns: 1fr; } }
|
|
|
|
.kpi-cards { display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--space-3); }
|
|
.kpi-card {
|
|
background: var(--color-bg-card); border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-card); padding: var(--space-4); text-align: center;
|
|
}
|
|
.kpi-label { display: block; font-size: 12px; color: var(--color-text-secondary); margin-bottom: var(--space-2); }
|
|
.kpi-value { font-size: 24px; font-weight: 700; color: var(--color-primary-hex); }
|
|
@media (max-width: 768px) { .kpi-cards { grid-template-columns: repeat(2, 1fr); } }
|
|
</style>
|