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 && (
+
+
+
+)}
+```
+
+下拉菜单沿用 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