Files
ftb-project-management/apps/web/components/version/VersionRequirementsTab.tsx
Script Generator 0a83ab8816 fix: 关联需求添加人显示"系统添加" + 开发状态列 + 开发中不可移除
1. 添加人字段:无值时显示"系统添加"而非"-"
2. 新增"开发状态"列:从 linkage-engine 推导
3. 开发中的需求:移除按钮替换为"开发中"文字提示
4. 去掉"描述"列(表格更紧凑)

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

154 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState } from 'react';
import { Plus, X, Search } from 'lucide-react';
import type { Requirement } from '@/lib/requirement';
import type { DevTask } from '@/lib/dev-task';
import { REQ_STATUS_LABEL, REQ_STATUS_COLOR } from '@/lib/requirement';
import { deriveReqDevStatus, canEditRequirement, REQ_DEV_STATUS_LABEL, REQ_DEV_STATUS_COLOR } from '@/lib/linkage-engine';
interface Props {
versionId: string;
projectId: string;
requirements: Requirement[];
devTasks: DevTask[];
onLink: (reqIds: string[], addedBy: string) => void;
onUnlink: (reqId: string) => void;
currentUserName: string;
}
export function VersionRequirementsTab({ versionId, projectId, requirements, devTasks, onLink, onUnlink, currentUserName }: Props) {
const [showAddModal, setShowAddModal] = useState(false);
const linkedReqs = requirements.filter((r) => r.versionId === versionId);
const availableReqs = requirements.filter((r) => r.projectId === projectId && !r.versionId && r.status === 'adopted');
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-[12px] text-[var(--ink-muted)]">{linkedReqs.length} </span>
<button onClick={() => setShowAddModal(true)} className="flex h-8 items-center gap-1.5 rounded-lg bg-[var(--accent)] px-3 text-[13px] font-medium text-white shadow-[var(--shadow-sm)] hover:bg-[var(--accent-hover)] transition-colors">
<Plus className="h-3.5 w-3.5" strokeWidth={2} />
</button>
</div>
{linkedReqs.length === 0 ? (
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-12 text-center text-[13px] text-[var(--ink-muted)]">
</div>
) : (
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] overflow-hidden">
<table className="w-full text-left text-[13px]">
<thead className="sticky top-0 z-10 bg-[var(--bg-subtle)]">
<tr className="border-b border-[var(--line)]">
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)] text-right"></th>
</tr>
</thead>
<tbody>
{linkedReqs.map((req) => {
const devStatus = deriveReqDevStatus(req.id, devTasks);
const isDeveloping = !canEditRequirement(req.id, devTasks);
return (
<tr key={req.id} className="border-b border-[var(--line-soft)] last:border-0 hover:bg-[var(--bg-subtle)] transition-colors">
<td className="px-4 py-3 font-mono text-[12px] text-[var(--ink-muted)]">{req.code}</td>
<td className="px-4 py-3 font-medium text-[var(--ink)]">{req.title}</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-[10px] font-medium border ${REQ_STATUS_COLOR[req.status]}`}>
{REQ_STATUS_LABEL[req.status]}
</span>
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-[10px] font-medium ${REQ_DEV_STATUS_COLOR[devStatus]}`}>
{REQ_DEV_STATUS_LABEL[devStatus]}
</span>
</td>
<td className="px-4 py-3 text-[12px] text-[var(--ink-soft)]">{req.addedToVersionBy || '系统添加'}</td>
<td className="px-4 py-3 text-right">
{isDeveloping ? (
<span className="text-[11px] text-[var(--ink-muted)]"></span>
) : (
<button onClick={() => onUnlink(req.id)} className="h-6 px-2 rounded text-[11px] font-medium text-red-500 hover:bg-red-50 transition-colors"></button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{showAddModal && (
<AddRequirementModal
available={availableReqs}
onClose={() => setShowAddModal(false)}
onConfirm={(ids) => { onLink(ids, currentUserName); setShowAddModal(false); }}
/>
)}
</div>
);
}
function AddRequirementModal({ available, onClose, onConfirm }: {
available: Requirement[];
onClose: () => void;
onConfirm: (ids: string[]) => void;
}) {
const [selected, setSelected] = useState<Set<string>>(new Set());
const [search, setSearch] = useState('');
const filtered = available.filter((r) =>
r.title.includes(search) || r.code.includes(search)
);
const toggle = (id: string) => {
const next = new Set(selected);
if (next.has(id)) next.delete(id); else next.add(id);
setSelected(next);
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
<div className="w-full max-w-lg rounded-2xl bg-[var(--bg-card)] border border-[var(--line)] shadow-[var(--shadow-md)] flex flex-col max-h-[70vh]" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--line)]">
<h3 className="text-[13px] font-semibold text-[var(--ink)]"></h3>
<button onClick={onClose} className="p-1 rounded hover:bg-[var(--bg-subtle)] text-[var(--ink-muted)]"><X className="h-4 w-4" /></button>
</div>
<div className="px-5 py-3 border-b border-[var(--line)]">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-[var(--ink-muted)]" />
<input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="搜索需求编号或标题" className="h-8 w-full rounded-lg border border-[var(--line)] bg-[var(--bg)] pl-9 pr-3 text-[12px] focus:border-[var(--accent)] focus:outline-none" />
</div>
</div>
<div className="flex-1 overflow-y-auto px-5 py-3">
{filtered.length === 0 ? (
<p className="text-center text-[12px] text-[var(--ink-muted)] py-8"></p>
) : (
<div className="space-y-1.5">
{filtered.map((req) => (
<label key={req.id} className="flex items-center gap-3 rounded-lg px-3 py-2 hover:bg-[var(--bg-subtle)] cursor-pointer">
<input type="checkbox" checked={selected.has(req.id)} onChange={() => toggle(req.id)} className="h-4 w-4 rounded border-[var(--line)] text-[var(--accent)]" />
<span className="text-[11px] font-mono text-[var(--ink-muted)] shrink-0">{req.code}</span>
<span className="text-[12px] text-[var(--ink)] flex-1 truncate">{req.title}</span>
</label>
))}
</div>
)}
</div>
<div className="flex items-center justify-between px-5 py-4 border-t border-[var(--line)]">
<span className="text-[11px] text-[var(--ink-muted)]"> {selected.size} </span>
<div className="flex gap-2">
<button onClick={onClose} className="h-8 px-3 rounded-lg text-[12px] font-medium border border-[var(--line)] text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]"></button>
<button onClick={() => onConfirm(Array.from(selected))} disabled={selected.size === 0} className="h-8 px-4 rounded-lg text-[12px] font-medium bg-[var(--accent)] text-white hover:bg-[var(--accent-hover)] disabled:opacity-50"></button>
</div>
</div>
</div>
</div>
);
}