From cad190d83bbee70f9f618ac1e33ec5a947fb40fe Mon Sep 17 00:00:00 2001 From: Script Generator Date: Wed, 17 Jun 2026 11:11:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A1=B9=E7=9B=AE=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=8A=A0=E5=88=A0=E9=99=A4=E6=8C=89=E9=92=AE=EF=BC=88=E4=BB=85?= =?UTF-8?q?=E8=B6=85=E7=AE=A1=20+=20=E6=97=A0=E7=89=88=E6=9C=AC=E5=8F=AF?= =?UTF-8?q?=E5=88=A0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useProductStore 加 deleteProject(productId, projectId) action - ProjectRow 行右侧加三点菜单(MoreHorizontal)+ 下拉项「删除项目」 - 项目下有版本时菜单项 disabled + tooltip「项目下还有 N 个版本,不可删除」 - 仅有 project:delete 权限的角色(默认仅超管)能看到三点 icon - 点击删除 → confirm → 调 deleteProject Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/app/projects/page.tsx | 60 +++++++++-- apps/web/stores/useProductStore.ts | 14 +++ .../specs/2026-06-16-project-delete-design.md | 102 ++++++++++++++++++ 3 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 docs/superpowers/specs/2026-06-16-project-delete-design.md diff --git a/apps/web/app/projects/page.tsx b/apps/web/app/projects/page.tsx index 7d760eb..38ad358 100644 --- a/apps/web/app/projects/page.tsx +++ b/apps/web/app/projects/page.tsx @@ -3,7 +3,7 @@ import { RouteGuard } from '@/components/auth/Guard'; import { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { Search, FolderKanban, Package, ChevronDown, Plus, X } from 'lucide-react'; +import { Search, FolderKanban, Package, ChevronDown, Plus, X, MoreHorizontal, Trash2 } from 'lucide-react'; import { useProductStore } from '@/stores/useProductStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useVersionPlanStore } from '@/stores/useVersionPlanStore'; @@ -13,6 +13,7 @@ import { flattenProjects, flattenVersions, ProjectWithContext } from '@/lib/deri import { VersionChip } from '@/components/version/VersionChip'; import { Pagination, usePagination } from '@/components/Pagination'; import { buildVersionProgressMap } from '@/lib/version-progress'; +import { useHasPermission } from '@/components/auth/Guard'; export default function ProjectsPage() { return ( @@ -24,7 +25,7 @@ export default function ProjectsPage() { function ProjectsPageContent() { const router = useRouter(); - const { overview, fetchOverview, createProject } = useProductStore(); + const { overview, fetchOverview, createProject, deleteProject } = useProductStore(); const { requirements, fetchRequirements } = useRequirementStore(); const { plans, fetchPlans } = useVersionPlanStore(); const { tasks: devTasks, fetchTasks: fetchDevTasks } = useDevTaskStore(); @@ -121,7 +122,16 @@ function ProjectsPageContent() {
{paged.map((proj) => ( - router.push(`/projects/${proj.id}`)} /> + router.push(`/projects/${proj.id}`)} + onDelete={() => { + if (!confirm(`确认删除项目「${proj.name}」?此操作不可恢复。`)) return; + deleteProject(proj.productId, proj.id); + }} + /> ))}
@@ -150,19 +160,24 @@ function ProjectRow({ proj, versionProgressMap, onClick, + onDelete, }: { proj: ProjectWithContext; versionProgressMap: Record; onClick: () => void; + onDelete: () => void; }) { + const canDelete = useHasPermission('project:delete'); + const [menuOpen, setMenuOpen] = useState(false); const activeVersions = proj.versions.filter((v) => (versionProgressMap[v.id] ?? 0) < 100); const completedCount = proj.versions.filter((v) => (versionProgressMap[v.id] ?? 0) >= 100).length; + const versionCount = proj.versions.length; + const canActuallyDelete = versionCount === 0; return ( -
- + {canDelete && ( +
e.stopPropagation()}> + + {menuOpen && ( + <> +
setMenuOpen(false)} /> +
+ +
+ + )} +
+ )} +
); } diff --git a/apps/web/stores/useProductStore.ts b/apps/web/stores/useProductStore.ts index 8e26bb1..646fc58 100644 --- a/apps/web/stores/useProductStore.ts +++ b/apps/web/stores/useProductStore.ts @@ -79,6 +79,7 @@ interface ProductState { createVersion: (productId: string, data: { name: string; status: string }) => void; updateVersion: (productId: string, versionId: string, data: Partial) => void; deleteVersion: (productId: string, versionId: string) => void; + deleteProject: (productId: string, projectId: string) => void; } export const useProductStore = create((set, get) => ({ @@ -273,6 +274,19 @@ export const useProductStore = create((set, get) => ({ set({ overview: updated }); saveLocal(updated); }, + + deleteProject: (productId, projectId) => { + const updated = get().overview.map((p) => { + if (p.id !== productId) return p; + return { + ...p, + projects: p.projects.filter((proj) => proj.id !== projectId), + _count: { ...p._count, projects: Math.max(0, (p._count?.projects ?? 0) - 1) } as any, + }; + }); + set({ overview: updated }); + saveLocal(updated); + }, })); const STORAGE_KEY = 'ftb_products_overview_v4'; diff --git a/docs/superpowers/specs/2026-06-16-project-delete-design.md b/docs/superpowers/specs/2026-06-16-project-delete-design.md new file mode 100644 index 0000000..b5e37ec --- /dev/null +++ b/docs/superpowers/specs/2026-06-16-project-delete-design.md @@ -0,0 +1,102 @@ +# 项目删除功能设计 + +## Context + +项目模块当前没有删除入口。本期补齐"删除项目"功能,并对应权限点。 + +## 决策摘要 + +| 项 | 决策 | +|---|---| +| 删除条件 | 项目下**有任何版本**均不可删(最严格,避免误删带数据的项目) | +| 按钮位置 | 项目行右侧三点菜单(hover 显示),菜单项"删除项目" | +| 不可删提示 | 菜单项 disabled + tooltip "项目下还有 N 个版本,不可删除" | +| 权限点 | `project:delete`(已存在于 `lib/permissions.ts`) | +| 默认能删的角色 | 仅超管 `role-admin`(PM/dev/test/design 都不含此权限) | +| 二次确认 | 浏览器原生 `confirm('确认删除项目「{name}」?')` | +| 卡片整体可点 | 保留——三点 icon `e.stopPropagation()` 避免冒泡 | + +## 实施清单 + +**(1) `apps/web/stores/useProductStore.ts`** — 加 `deleteProject` action + +```ts +deleteProject: (productId: string, projectId: string) => void; + +// 实现:找到产品,从 projects 数组移除目标项目,写回 localStorage +deleteProject: (productId, projectId) => { + const overview = get().overview.map((p) => + p.id === productId + ? { ...p, projects: p.projects.filter((proj) => proj.id !== projectId) } + : p + ); + set({ overview }); + saveLocal(overview); +}, +``` + +**(2) `apps/web/app/projects/page.tsx`** — `ProjectRow` 加三点菜单 + +```tsx +const canDelete = useHasPermission('project:delete'); +const versionCount = proj.versions.length; +const canActuallyDelete = canDelete && versionCount === 0; + +// 行右侧 +{canDelete && ( + + 0 ? `项目下还有 ${versionCount} 个版本,不可删除` : ''} + onClick={() => handleDelete(proj)} + danger + > + 删除项目 + + +)} +``` + +下拉菜单沿用 versions/page.tsx 已有的 dropdown 写法(`MoreHorizontal` icon + 绝对定位 + `fixed inset-0` overlay 关闭)。 + +**handleDelete**: +```tsx +const handleDelete = (proj: ProjectWithContext) => { + if (!confirm(`确认删除项目「${proj.name}」?此操作不可恢复。`)) return; + deleteProject(proj.productId, proj.id); +}; +``` + +**(3) 权限默认值不动** + +- `lib/permissions.ts` 第 33 行 `std4('project')` 已包含 `project:delete` +- `DEFAULT_ROLE_PERMISSIONS`:超管 `['*']` 自动通过;PM 默认不含此权限(与"仅超管能删"一致) +- 无需修改 + +## 数据流 + +``` +ProjectRow + │ + ├─ useHasPermission('project:delete') ─→ 决定三点菜单是否显示 + │ + └─ proj.versions.length ─→ 决定菜单项是否可点 + │ + └─ disabled + tooltip + │ + 点击「删除」 ─→ confirm ─→ useProductStore.deleteProject() + │ + └─ 列表自动重渲(zustand 响应式) +``` + +## 验证 + +1. `pnpm type-check` 0 错误 +2. `pnpm build` 14+1 个页面成功 +3. 登录陈十(超管 `13200132008/Ftb@2024`): + - hover 项目行右侧出现三点 icon + - 点开菜单看到"删除项目" + - 项目下有版本:菜单项灰色 + 鼠标悬停 tooltip "项目下还有 N 个版本,不可删除" + - 项目下无版本:菜单项可点 → confirm → 项目消失 +4. 登录李四(开发 `13900139002/Ftb@2024`):项目行右侧无三点 icon(无 `project:delete` 权限) +5. 登录张三(PM `13800138001/Ftb@2024`):同上,无三点 icon