feat: 项目编辑、成员编辑、性能优化及UI改进
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m2s
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>
This commit is contained in:
parent
4283824533
commit
10ed4f090d
@ -154,6 +154,28 @@ adminRoutes.post('/admin/projects', zValidator('json', createProjectSchema), asy
|
|||||||
return c.json({ code: 0, data: { id }, message: 'success' }, 201);
|
return c.json({ code: 0, data: { id }, message: 'success' }, 201);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateProjectSchema = z.object({
|
||||||
|
name: z.string().min(1).max(200).optional(),
|
||||||
|
identifier: z.string().min(1).max(20).toUpperCase().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
adminRoutes.patch('/admin/projects/:id', zValidator('json', updateProjectSchema), async (c) => {
|
||||||
|
const id = c.req.param('id');
|
||||||
|
const data = c.req.valid('json');
|
||||||
|
|
||||||
|
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' });
|
||||||
|
});
|
||||||
|
|
||||||
adminRoutes.delete('/admin/projects/:id', async (c) => {
|
adminRoutes.delete('/admin/projects/:id', async (c) => {
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
await db.delete(projects).where(eq(projects.id, id));
|
await db.delete(projects).where(eq(projects.id, id));
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { z } from 'zod';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { db } from '../db/index';
|
import { db } from '../db/index';
|
||||||
import { projects, sprintSnapshots, milestones, taskSnapshots, gitCommits, gitPRs, users, objectives, keyResults, projectRepos, krLogs } from '../db/schema';
|
import { projects, sprintSnapshots, milestones, taskSnapshots, gitCommits, gitPRs, users, objectives, keyResults, projectRepos, krLogs } from '../db/schema';
|
||||||
import { eq, and, desc, gte } from 'drizzle-orm';
|
import { eq, and, desc, gte, inArray } from 'drizzle-orm';
|
||||||
import { requireRole } from '../middleware/role';
|
import { requireRole } from '../middleware/role';
|
||||||
import { AppError } from '../middleware/error-handler';
|
import { AppError } from '../middleware/error-handler';
|
||||||
import { getAllowedProjectIds } from '../services/permissions';
|
import { getAllowedProjectIds } from '../services/permissions';
|
||||||
@ -182,32 +182,47 @@ projectRoutes.get('/projects/:id', async (c) => {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
// OKR for this project
|
// OKR for this project (batch queries to avoid N+1)
|
||||||
const projectObjectives = await db.select().from(objectives)
|
const projectObjectives = await db.select().from(objectives)
|
||||||
.where(eq(objectives.projectId, projectId));
|
.where(eq(objectives.projectId, projectId));
|
||||||
|
|
||||||
|
const objIds = projectObjectives.map(o => o.id);
|
||||||
|
const allKRsForProject = objIds.length > 0
|
||||||
|
? await db.select().from(keyResults).where(inArray(keyResults.objectiveId, objIds))
|
||||||
|
: [];
|
||||||
|
const krIds = allKRsForProject.map(kr => kr.id);
|
||||||
|
const allLogsForProject = krIds.length > 0
|
||||||
|
? await db.select().from(krLogs).where(inArray(krLogs.krId, krIds)).orderBy(desc(krLogs.createdAt))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Group KRs by objective, logs by KR
|
||||||
|
const krsByObj = new Map<string, typeof allKRsForProject>();
|
||||||
|
for (const kr of allKRsForProject) {
|
||||||
|
if (!krsByObj.has(kr.objectiveId)) krsByObj.set(kr.objectiveId, []);
|
||||||
|
krsByObj.get(kr.objectiveId)!.push(kr);
|
||||||
|
}
|
||||||
|
const logsByKR = new Map<string, typeof allLogsForProject>();
|
||||||
|
for (const log of allLogsForProject) {
|
||||||
|
if (!logsByKR.has(log.krId)) logsByKR.set(log.krId, []);
|
||||||
|
const arr = logsByKR.get(log.krId)!;
|
||||||
|
if (arr.length < 5) arr.push(log);
|
||||||
|
}
|
||||||
|
|
||||||
const okrData = [];
|
const okrData = [];
|
||||||
let totalOKRProgress = 0;
|
let totalOKRProgress = 0;
|
||||||
for (const obj of projectObjectives) {
|
for (const obj of projectObjectives) {
|
||||||
const krs = await db.select().from(keyResults)
|
const krs = krsByObj.get(obj.id) || [];
|
||||||
.where(eq(keyResults.objectiveId, obj.id));
|
|
||||||
const owner = obj.ownerId
|
|
||||||
? await db.query.users.findFirst({ where: eq(users.id, obj.ownerId) })
|
|
||||||
: null;
|
|
||||||
okrData.push({
|
okrData.push({
|
||||||
id: obj.id,
|
id: obj.id,
|
||||||
title: obj.title,
|
title: obj.title,
|
||||||
ownerId: obj.ownerId || null,
|
ownerId: obj.ownerId || null,
|
||||||
ownerName: owner?.displayName || '未指定',
|
ownerName: obj.ownerId ? (userMap.get(obj.ownerId) || '未指定') : '未指定',
|
||||||
period: obj.period,
|
period: obj.period,
|
||||||
startDate: obj.startDate || null,
|
startDate: obj.startDate || null,
|
||||||
endDate: obj.endDate || null,
|
endDate: obj.endDate || null,
|
||||||
progress: obj.progress || 0,
|
progress: obj.progress || 0,
|
||||||
keyResults: await Promise.all(krs.map(async kr => {
|
keyResults: krs.map(kr => {
|
||||||
const logs = await db.select().from(krLogs)
|
const logs = logsByKR.get(kr.id) || [];
|
||||||
.where(eq(krLogs.krId, kr.id))
|
|
||||||
.orderBy(desc(krLogs.createdAt))
|
|
||||||
.limit(5);
|
|
||||||
const wasPostponed = logs.some(l => l.action === 'postponed');
|
const wasPostponed = logs.some(l => l.action === 'postponed');
|
||||||
const lastPostponeReason = logs.find(l => l.action === 'postponed')?.detail || null;
|
const lastPostponeReason = logs.find(l => l.action === 'postponed')?.detail || null;
|
||||||
return {
|
return {
|
||||||
@ -226,7 +241,7 @@ projectRoutes.get('/projects/:id', async (c) => {
|
|||||||
? Math.round(((kr.currentValue || 0) / kr.targetValue) * 100)
|
? Math.round(((kr.currentValue || 0) / kr.targetValue) * 100)
|
||||||
: 0,
|
: 0,
|
||||||
};
|
};
|
||||||
})),
|
}),
|
||||||
});
|
});
|
||||||
totalOKRProgress += obj.progress || 0;
|
totalOKRProgress += obj.progress || 0;
|
||||||
}
|
}
|
||||||
@ -247,15 +262,13 @@ projectRoutes.get('/projects/:id', async (c) => {
|
|||||||
return cleaned;
|
return cleaned;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 获取该项目所有 Git 数据(不限时间范围)
|
// 获取该项目绑定仓库的 Git 数据(按仓库名过滤,避免全表扫描)
|
||||||
const allCommits = await db.select().from(gitCommits);
|
const boundRepoNamesList = Array.from(boundRepoNames);
|
||||||
const allPRs = await db.select().from(gitPRs);
|
const recentCommits = boundRepoNamesList.length > 0
|
||||||
|
? await db.select().from(gitCommits).where(inArray(gitCommits.repoName, boundRepoNamesList))
|
||||||
const recentCommits = boundRepoNames.size > 0
|
|
||||||
? allCommits.filter(c => boundRepoNames.has(c.repoName))
|
|
||||||
: [];
|
: [];
|
||||||
const recentPRs = boundRepoNames.size > 0
|
const recentPRs = boundRepoNamesList.length > 0
|
||||||
? allPRs.filter(p => boundRepoNames.has(p.repoName))
|
? await db.select().from(gitPRs).where(inArray(gitPRs.repoName, boundRepoNamesList))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const weeklyTrend: { weekStart: string; commits: number; prs: number }[] = [];
|
const weeklyTrend: { weekStart: string; commits: number; prs: number }[] = [];
|
||||||
|
|||||||
@ -37,6 +37,10 @@ export function createProjectApi(data: { name: string; identifier: string }) {
|
|||||||
return request.post('/api/projects', data);
|
return request.post('/api/projects', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateProjectApi(id: string, data: { name?: string; identifier?: string }) {
|
||||||
|
return request.patch(`/api/admin/projects/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteProjectApi(id: string) {
|
export function deleteProjectApi(id: string) {
|
||||||
return request.delete(`/api/projects/${id}`);
|
return request.delete(`/api/projects/${id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ const chartOptions = computed(() => {
|
|||||||
const sorted = [...props.projects].sort(
|
const sorted = [...props.projects].sort(
|
||||||
(a, b) => b.currentCycleProgress - a.currentCycleProgress,
|
(a, b) => b.currentCycleProgress - a.currentCycleProgress,
|
||||||
);
|
);
|
||||||
const names = sorted.map((p) => `${p.identifier} ${p.name}`.trim());
|
const names = sorted.map((p) => p.name);
|
||||||
const values = sorted.map((p) => p.currentCycleProgress);
|
const values = sorted.map((p) => p.currentCycleProgress);
|
||||||
const bgValues = sorted.map(() => 100);
|
const bgValues = sorted.map(() => 100);
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ const chartOptions = computed(() => {
|
|||||||
const project = sorted[idx];
|
const project = sorted[idx];
|
||||||
if (!project) return '';
|
if (!project) return '';
|
||||||
return `
|
return `
|
||||||
<strong>${project.identifier} ${project.name}</strong><br/>
|
<strong>${project.name}</strong><br/>
|
||||||
进度: ${project.currentCycleProgress}%<br/>
|
进度: ${project.currentCycleProgress}%<br/>
|
||||||
${project.completedPoints}/${project.totalPoints} 点
|
${project.completedPoints}/${project.totalPoints} 点
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -170,7 +170,7 @@ const roleTagType = computed(() => {
|
|||||||
:class="{ active: route.path === `/projects/${proj.projectId}` }"
|
:class="{ active: route.path === `/projects/${proj.projectId}` }"
|
||||||
@click="handleProjectSelect(proj.projectId)"
|
@click="handleProjectSelect(proj.projectId)"
|
||||||
>
|
>
|
||||||
<span class="submenu-label">{{ proj.identifier || '' }} {{ proj.name }}</span>
|
<span class="submenu-label">{{ proj.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!projectList.length" class="submenu-item submenu-empty">
|
<div v-if="!projectList.length" class="submenu-item submenu-empty">
|
||||||
暂无项目
|
暂无项目
|
||||||
|
|||||||
@ -3,18 +3,27 @@
|
|||||||
* B-17 fix: New Member List page.
|
* B-17 fix: New Member List page.
|
||||||
* Displays all team members with role badges and links to their detail pages.
|
* Displays all team members with role badges and links to their detail pages.
|
||||||
*/
|
*/
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, h } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { NSpin, NDataTable, NTag } from 'naive-ui';
|
import { NSpin, NDataTable, NTag, NButton, NModal, NForm, NFormItem, NInput, NSelect, useMessage } from 'naive-ui';
|
||||||
import { getMemberListApi } from '@/api/members';
|
import { getMemberListApi } from '@/api/members';
|
||||||
|
import { updateUserApi } from '@/api/admin';
|
||||||
import DataCard from '@/components/shared/DataCard.vue';
|
import DataCard from '@/components/shared/DataCard.vue';
|
||||||
import EmptyState from '@/components/shared/EmptyState.vue';
|
import EmptyState from '@/components/shared/EmptyState.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const message = useMessage();
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const members = ref<any[]>([]);
|
const members = ref<any[]>([]);
|
||||||
|
|
||||||
onMounted(async () => {
|
const userRole = (() => {
|
||||||
|
try { return JSON.parse(localStorage.getItem('user') || '{}').role || 'viewer'; }
|
||||||
|
catch { return 'viewer'; }
|
||||||
|
})();
|
||||||
|
const isAdmin = userRole === 'admin';
|
||||||
|
|
||||||
|
async function loadMembers() {
|
||||||
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await getMemberListApi();
|
const res = await getMemberListApi();
|
||||||
members.value = res.data.data || [];
|
members.value = res.data.data || [];
|
||||||
@ -23,7 +32,47 @@ onMounted(async () => {
|
|||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
onMounted(loadMembers);
|
||||||
|
|
||||||
|
// ── 编辑成员 ──
|
||||||
|
const showEditModal = ref(false);
|
||||||
|
const editForm = ref({ id: '', displayName: '', email: '', role: '' });
|
||||||
|
|
||||||
|
function openEditModal(row: any, e: Event) {
|
||||||
|
e.stopPropagation();
|
||||||
|
editForm.value = { id: row.id, displayName: row.displayName, email: row.email, role: row.role };
|
||||||
|
showEditModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveEdit() {
|
||||||
|
if (!editForm.value.displayName) {
|
||||||
|
message.warning('姓名不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await updateUserApi(editForm.value.id, {
|
||||||
|
displayName: editForm.value.displayName,
|
||||||
|
email: editForm.value.email,
|
||||||
|
role: editForm.value.role,
|
||||||
|
});
|
||||||
|
message.success('成员信息已更新');
|
||||||
|
showEditModal.value = false;
|
||||||
|
loadMembers();
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error(err.response?.data?.message || '更新失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleOptions = [
|
||||||
|
{ value: 'admin', label: '管理员' },
|
||||||
|
{ value: 'manager', label: '经理' },
|
||||||
|
{ value: 'developer', label: '开发者' },
|
||||||
|
{ value: 'viewer', label: '观察者' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const roleLabels: Record<string, string> = { admin: '管理员', manager: '经理', developer: '开发者', viewer: '观察者' };
|
||||||
|
|
||||||
function roleTagType(role: string) {
|
function roleTagType(role: string) {
|
||||||
if (role === 'admin') return 'info';
|
if (role === 'admin') return 'info';
|
||||||
@ -51,9 +100,21 @@ const columns = [
|
|||||||
type: roleTagType(row.role),
|
type: roleTagType(row.role),
|
||||||
size: 'small',
|
size: 'small',
|
||||||
round: true,
|
round: true,
|
||||||
}, { default: () => row.role });
|
}, { default: () => roleLabels[row.role] || row.role });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...(isAdmin ? [{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 80,
|
||||||
|
render: (row: any) => {
|
||||||
|
return h(NButton, {
|
||||||
|
size: 'tiny',
|
||||||
|
type: 'warning',
|
||||||
|
onClick: (e: Event) => openEditModal(row, e),
|
||||||
|
}, { default: () => '编辑' });
|
||||||
|
},
|
||||||
|
}] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
function handleRowClick(row: any) {
|
function handleRowClick(row: any) {
|
||||||
@ -61,11 +122,6 @@ function handleRowClick(row: any) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { h } from 'vue';
|
|
||||||
export default {};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="member-list-page">
|
<div class="member-list-page">
|
||||||
<h2 style="margin-bottom: var(--space-5)">团队成员</h2>
|
<h2 style="margin-bottom: var(--space-5)">团队成员</h2>
|
||||||
@ -84,6 +140,21 @@ export default {};
|
|||||||
</DataCard>
|
</DataCard>
|
||||||
<EmptyState v-else-if="!loading" title="暂无成员" description="未找到团队成员。" />
|
<EmptyState v-else-if="!loading" title="暂无成员" description="未找到团队成员。" />
|
||||||
</NSpin>
|
</NSpin>
|
||||||
|
|
||||||
|
<!-- 编辑成员弹窗 -->
|
||||||
|
<NModal v-model:show="showEditModal" title="编辑成员" preset="dialog" positive-text="保存" @positive-click="handleSaveEdit">
|
||||||
|
<NForm>
|
||||||
|
<NFormItem label="姓名">
|
||||||
|
<NInput v-model:value="editForm.displayName" placeholder="请输入姓名" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="邮箱">
|
||||||
|
<NInput v-model:value="editForm.email" placeholder="请输入邮箱" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="角色">
|
||||||
|
<NSelect v-model:value="editForm.role" :options="roleOptions" />
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
</NModal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { ref, onMounted, h } from 'vue';
|
import { ref, onMounted, h } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { NSpin, NDataTable, NButton, NModal, NForm, NFormItem, NInput, NTag, NEmpty, useMessage } from 'naive-ui';
|
import { NSpin, NDataTable, NButton, NModal, NForm, NFormItem, NInput, NTag, NEmpty, useMessage } from 'naive-ui';
|
||||||
import { getAdminProjectsApi, createProjectApi, deleteProjectApi, bindRepoApi, getProjectReposApi, unbindRepoApi } from '@/api/admin';
|
import { getAdminProjectsApi, createProjectApi, updateProjectApi, deleteProjectApi, bindRepoApi, getProjectReposApi, unbindRepoApi } from '@/api/admin';
|
||||||
import DataCard from '@/components/shared/DataCard.vue';
|
import DataCard from '@/components/shared/DataCard.vue';
|
||||||
import EmptyState from '@/components/shared/EmptyState.vue';
|
import EmptyState from '@/components/shared/EmptyState.vue';
|
||||||
|
|
||||||
@ -115,6 +115,33 @@ async function handleUnbindRepo(bindingId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 编辑项目 ──
|
||||||
|
const showEditModal = ref(false);
|
||||||
|
const editProject = ref({ id: '', name: '', identifier: '' });
|
||||||
|
|
||||||
|
function openEditModal(project: any) {
|
||||||
|
editProject.value = { id: project.id, name: project.name, identifier: project.identifier || '' };
|
||||||
|
showEditModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEditProject() {
|
||||||
|
if (!editProject.value.name) {
|
||||||
|
message.warning('项目名称不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await updateProjectApi(editProject.value.id, {
|
||||||
|
name: editProject.value.name,
|
||||||
|
identifier: editProject.value.identifier.toUpperCase(),
|
||||||
|
});
|
||||||
|
message.success('项目已更新');
|
||||||
|
showEditModal.value = false;
|
||||||
|
loadProjects();
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error(err.response?.data?.message || '更新失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 删除项目 ──
|
// ── 删除项目 ──
|
||||||
async function handleDelete(id: string, name: string) {
|
async function handleDelete(id: string, name: string) {
|
||||||
if (!confirm(`确定删除项目「${name}」吗?`)) return;
|
if (!confirm(`确定删除项目「${name}」吗?`)) return;
|
||||||
@ -156,14 +183,17 @@ const columns = [
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: 200,
|
width: 250,
|
||||||
render: (row: any) => {
|
render: (row: any) => {
|
||||||
return h('div', { style: 'display:flex;gap:6px' }, [
|
return h('div', { style: 'display:flex;gap:6px' }, [
|
||||||
h(NButton, { size: 'tiny', type: 'info', onClick: () => router.push(`/projects/${row.id}`) }, { default: () => '查看' }),
|
h(NButton, { size: 'tiny', type: 'info', onClick: () => router.push(`/projects/${row.id}`) }, { default: () => '查看' }),
|
||||||
canCreate
|
canCreate
|
||||||
? h(NButton, { size: 'tiny', type: 'default', onClick: () => openRepoModal(row) }, { default: () => '仓库' })
|
? h(NButton, { size: 'tiny', type: 'warning', onClick: () => openEditModal(row) }, { default: () => '编辑' })
|
||||||
: null,
|
: null,
|
||||||
canCreate
|
canCreate
|
||||||
|
? h(NButton, { size: 'tiny', type: 'default', onClick: () => openRepoModal(row) }, { default: () => '仓库' })
|
||||||
|
: null,
|
||||||
|
userRole === 'admin'
|
||||||
? h(NButton, { size: 'tiny', type: 'error', onClick: () => handleDelete(row.id, row.name) }, { default: () => '删除' })
|
? h(NButton, { size: 'tiny', type: 'error', onClick: () => handleDelete(row.id, row.name) }, { default: () => '删除' })
|
||||||
: null,
|
: null,
|
||||||
]);
|
]);
|
||||||
@ -204,6 +234,18 @@ const columns = [
|
|||||||
</div>
|
</div>
|
||||||
</NModal>
|
</NModal>
|
||||||
|
|
||||||
|
<!-- 编辑项目弹窗 -->
|
||||||
|
<NModal v-model:show="showEditModal" title="编辑项目" preset="dialog" positive-text="保存" @positive-click="handleEditProject">
|
||||||
|
<NForm>
|
||||||
|
<NFormItem label="项目名称" required>
|
||||||
|
<NInput v-model:value="editProject.name" placeholder="项目名称" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem label="项目标识" required>
|
||||||
|
<NInput v-model:value="editProject.identifier" placeholder="大写英文缩写" />
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
</NModal>
|
||||||
|
|
||||||
<!-- 管理仓库弹窗 -->
|
<!-- 管理仓库弹窗 -->
|
||||||
<NModal v-model:show="showRepoModal" :title="`管理仓库 — ${editingProject?.name || ''}`" preset="dialog" :show-icon="false">
|
<NModal v-model:show="showRepoModal" :title="`管理仓库 — ${editingProject?.name || ''}`" preset="dialog" :show-icon="false">
|
||||||
<div v-if="editingProject">
|
<div v-if="editingProject">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user