feat: 开发者可编辑项目、侧边栏项目列表优化、筛选器UI改进
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m1s
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:
parent
18e3ee18da
commit
512d3baca2
@ -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');
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user