feat: 开发者可编辑项目、侧边栏项目列表优化、筛选器UI改进
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m1s

- 新增 PATCH /api/projects/:id 开发者有权限可编辑项目
- 侧边栏项目列表改用项目API直接拉取,路由切换时自动刷新
- 项目筛选器和权限分配下拉框只显示项目名称,标签自动折叠

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
zyc 2026-04-14 15:02:42 +08:00
parent 18e3ee18da
commit 512d3baca2
6 changed files with 57 additions and 11 deletions

View File

@ -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<string, any> = { 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');

View File

@ -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) {

View File

@ -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<Array<{ projectId: string; name: string; identifier: string }>>([]);
// 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 {

View File

@ -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"
/>
</div>
</template>

View File

@ -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"
/>
</NModal>

View File

@ -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);