Files
ftb-project-management/apps/web/components/layout/Sidebar.tsx
Script Generator 43d3f76e6a feat: 需求池重构 + 联动引擎 + 开发状态推导
架构:新建 lib/linkage-engine.ts 联动引擎
- deriveReqDevStatus(): 从 devTasks 推导需求开发状态
- canEditRequirement(): 开发中的需求不可编辑
- checkVersionChangeRisk(): 版本变更风险检查
- 所有跨模块推导逻辑集中在此文件

功能改动:
1. 导航改名"需求"→"需求池"
2. 新建需求:所属产品/项目必填,移除产品负责人字段
3. 需求表格新增"开发状态"列:
   未排期/待开发/开发中/已完成(从 devTasks 实时推导)
4. 开发中的需求:编辑按钮禁用
5. 需求设 versionId → 版本详情关联需求天然兼容
   (版本详情通过 filter(r.versionId === versionId) 读取)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-15 15:09:02 +08:00

95 lines
3.8 KiB
TypeScript

'use client';
import { usePathname, useRouter } from 'next/navigation';
import { Inbox, Package, FolderKanban, Tag, Users, LayoutGrid, Search, Lightbulb, Clock, Shield } from 'lucide-react';
const NAV_GROUPS = [
{
label: '工作区',
items: [
{ label: '与我相关', path: '/workspace', icon: Inbox },
{ label: '产品', path: '/products', icon: Package },
{ label: '项目', path: '/projects', icon: FolderKanban },
{ label: '版本', path: '/versions', icon: Tag },
{ label: '需求池', path: '/requirements', icon: Lightbulb },
{ label: '加班记录', path: '/overtime', icon: Clock },
],
},
{
label: '管理',
items: [
{ label: '成员', path: '/admin/members', icon: Users },
{ label: '角色', path: '/admin/roles', icon: Shield },
],
},
];
export function Sidebar() {
const pathname = usePathname();
const router = useRouter();
const isActive = (path: string) =>
pathname === path || (path !== '/' && pathname.startsWith(path));
return (
<aside className="flex h-screen w-60 flex-col border-r border-[var(--line)] bg-[var(--bg-card)]">
<div className="flex h-14 items-center gap-2 px-4">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-[var(--accent)]">
<LayoutGrid className="h-4 w-4 text-white" strokeWidth={2.25} />
</div>
<span className="text-[13px] font-semibold tracking-tight text-[var(--ink)]">FTB</span>
</div>
<div className="px-3 pb-2">
<div className="relative">
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-[var(--ink-muted)]" strokeWidth={2} />
<input
placeholder="搜索"
className="h-8 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-subtle)] pl-8 pr-2 text-[13px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:border-[var(--accent)] focus:bg-[var(--bg-card)] focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)]"
/>
</div>
</div>
<nav className="flex-1 px-2 pt-2">
{NAV_GROUPS.map((group) => (
<div key={group.label} className="mb-4">
<p className="mb-1 px-2 text-[11px] font-medium text-[var(--ink-muted)]">
{group.label}
</p>
{group.items.map((item) => {
const Icon = item.icon;
const active = isActive(item.path);
return (
<button
key={item.path}
onClick={() => router.push(item.path)}
className={`flex w-full items-center gap-2.5 rounded-lg px-2 py-1.5 text-left text-[13px] transition-colors ${
active
? 'bg-[var(--accent-soft)] font-medium text-[var(--accent-hover)]'
: 'text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)] hover:text-[var(--ink)]'
}`}
>
<Icon className="h-4 w-4 shrink-0" strokeWidth={1.75} />
<span className="truncate">{item.label}</span>
</button>
);
})}
</div>
))}
</nav>
<div className="border-t border-[var(--line)] p-2">
<button className="flex w-full items-center gap-2.5 rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-[var(--bg-subtle)]">
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[var(--accent)] text-[11px] font-semibold text-white">
A
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-[13px] font-medium text-[var(--ink)]">Admin</p>
<p className="truncate text-[11px] text-[var(--ink-muted)]"></p>
</div>
</button>
</div>
</aside>
);
}