From 512d3baca283832e1c1d03c44fd84351912be029 Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Tue, 14 Apr 2026 15:02:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BC=80=E5=8F=91=E8=80=85=E5=8F=AF?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E9=A1=B9=E7=9B=AE=E3=80=81=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E9=A1=B9=E7=9B=AE=E5=88=97=E8=A1=A8=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E3=80=81=E7=AD=9B=E9=80=89=E5=99=A8UI=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 PATCH /api/projects/:id 开发者有权限可编辑项目 - 侧边栏项目列表改用项目API直接拉取,路由切换时自动刷新 - 项目筛选器和权限分配下拉框只显示项目名称,标签自动折叠 Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/src/routes/projects.ts | 36 +++++++++++++++++++ frontend/src/api/admin.ts | 2 +- frontend/src/components/layout/AppSidebar.vue | 21 +++++++---- frontend/src/components/shared/FilterBar.vue | 4 ++- frontend/src/views/Admin.vue | 3 +- frontend/src/views/Overview.vue | 2 +- 6 files changed, 57 insertions(+), 11 deletions(-) diff --git a/backend/src/routes/projects.ts b/backend/src/routes/projects.ts index 6a164d7..63cfe25 100644 --- a/backend/src/routes/projects.ts +++ b/backend/src/routes/projects.ts @@ -94,6 +94,42 @@ projectRoutes.delete('/projects/:id', } ); +// PATCH /api/projects/:id — 开发者(有权限)或管理员可编辑 +const updateProjectSchema = z.object({ + name: z.string().min(1).max(200).optional(), + identifier: z.string().min(1).max(20).toUpperCase().optional(), +}); + +projectRoutes.patch('/projects/:id', + requireRole('admin', 'manager', 'developer'), + zValidator('json', updateProjectSchema), + async (c) => { + const id = c.req.param('id'); + const user = c.get('user'); + const data = c.req.valid('json'); + + // 开发者需要有该项目的权限 + if (user.role === 'developer') { + const allowedIds = await getAllowedProjectIds(user); + if (allowedIds !== null && !allowedIds.includes(id)) { + throw new AppError(40103, 'Insufficient permissions', 403); + } + } + + const project = await db.query.projects.findFirst({ where: eq(projects.id, id) }); + if (!project) { + throw new AppError(40402, 'Project not found', 404); + } + + const updateData: Record = { updatedAt: new Date() }; + if (data.name) updateData.name = data.name; + if (data.identifier) updateData.identifier = data.identifier; + + await db.update(projects).set(updateData).where(eq(projects.id, id)); + return c.json({ code: 0, data: { id }, message: 'success' }); + } +); + // GET /api/projects/:id/repos — 所有登录用户 projectRoutes.get('/projects/:id/repos', async (c) => { const projectId = c.req.param('id'); diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts index b7d895d..2e06ac6 100644 --- a/frontend/src/api/admin.ts +++ b/frontend/src/api/admin.ts @@ -38,7 +38,7 @@ export function createProjectApi(data: { name: string; identifier: string }) { } export function updateProjectApi(id: string, data: { name?: string; identifier?: string }) { - return request.patch(`/api/admin/projects/${id}`, data); + return request.patch(`/api/projects/${id}`, data); } export function deleteProjectApi(id: string) { diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index cd975e8..06c47a6 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -4,12 +4,12 @@ * Projects shows a collapsible sub-menu listing projects from the API. * Members links to the member list page (admin/manager only). */ -import { computed, ref, onMounted } from 'vue'; +import { computed, ref, onMounted, watch } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { NTag, NTooltip } from 'naive-ui'; import { useAuthStore } from '@/stores/auth'; import { useDashboardStore } from '@/stores/dashboard'; -import { getOverviewApi } from '@/api/overview'; +import { getAdminProjectsApi } from '@/api/admin'; const route = useRoute(); const router = useRouter(); @@ -20,14 +20,21 @@ const dashStore = useDashboardStore(); const projectsExpanded = ref(false); const projectList = ref>([]); -// Load project list for sidebar sub-menu -onMounted(async () => { +async function loadProjectList() { try { - const res = await getOverviewApi(); - projectList.value = res.data.data.projectProgress || []; + const res = await getAdminProjectsApi(); + const list = res.data.data || []; + projectList.value = list.map((p: any) => ({ projectId: p.id, name: p.name, identifier: p.identifier || '' })); } catch { - // Silently fail - sidebar still works without project sub-menu + // Silently fail } +} + +onMounted(loadProjectList); + +// 路由变化时刷新项目列表(如从项目列表页返回时) +watch(() => route.path, (newPath) => { + if (newPath === '/projects') loadProjectList(); }); interface NavItem { diff --git a/frontend/src/components/shared/FilterBar.vue b/frontend/src/components/shared/FilterBar.vue index 7ba2f5d..8b0ee24 100644 --- a/frontend/src/components/shared/FilterBar.vue +++ b/frontend/src/components/shared/FilterBar.vue @@ -46,8 +46,10 @@ watch([selectedPeriod, selectedProjects], () => { :options="projects || []" placeholder="筛选项目" multiple + filterable clearable - style="width: 300px" + max-tag-count="responsive" + style="min-width: 200px; max-width: 400px" /> diff --git a/frontend/src/views/Admin.vue b/frontend/src/views/Admin.vue index 25192e8..bd92621 100644 --- a/frontend/src/views/Admin.vue +++ b/frontend/src/views/Admin.vue @@ -117,7 +117,7 @@ async function loadProjects() { } const projectOptions = computed(() => - allProjects.value.map(p => ({ value: p.id, label: `${p.name} (${p.identifier || p.id.slice(0, 8)})` })) + allProjects.value.map(p => ({ value: p.id, label: p.name })) ); function openProjectPermModal(user: any) { @@ -348,6 +348,7 @@ const roleOptions = [ multiple filterable placeholder="选择可查看的项目" + max-tag-count="responsive" /> diff --git a/frontend/src/views/Overview.vue b/frontend/src/views/Overview.vue index 1acf378..fba9db3 100644 --- a/frontend/src/views/Overview.vue +++ b/frontend/src/views/Overview.vue @@ -42,7 +42,7 @@ async function loadData(filters?: { period?: string; projectIds?: string[] }) { overviewData.value = overviewRes.data.data; projectOptions.value = projectRes.data.data.map((p: any) => ({ value: p.id, - label: `${p.identifier || ''} ${p.name}`.trim(), + label: p.name, })); } catch (err) { console.error('Failed to load overview:', err);