Files
ftb-project-management/apps/web/components/requirement/RequirementDetail.tsx
Script Generator 9a0b16a8f1 feat: 实现需求管理、加班记录、成员/角色管理模块
- 需求模块:完整 CRUD、状态流转(采纳/拒绝/关闭)、详情抽屉、产品→项目级联选择
- 加班记录:产品→项目→版本三级联动、月份筛选(MonthPicker)、CSV 导出
- 成员管理:左右布局(部门树+成员列表)、手机号脱敏、初始密码自动生成及规则设置
- 角色管理:卡片列表、系统角色保护、CRUD
- 通用组件:FilterSelect 下拉、MonthPicker 月份选择器、Pagination 分页
- 样式统一:状态标签加 border、日期输入现代化、筛选组件风格一致

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

161 lines
6.7 KiB
TypeScript

'use client';
import { X } from 'lucide-react';
import type { Requirement, DictItem, SourceTarget } from '@/lib/requirement';
import { REQ_STATUS_LABEL, REQ_STATUS_COLOR, EFFORT_SHORT, EFFORT_COLOR, SOURCE_TYPE_LABEL } from '@/lib/requirement';
interface RequirementDetailProps {
requirement: Requirement;
projects: { id: string; name: string }[];
types: DictItem[];
platforms: DictItem[];
sourceTargets: SourceTarget[];
resolveVersionName: (id?: string) => string;
onClose: () => void;
onEdit: () => void;
onAdopt?: () => void;
onReject?: () => void;
onCloseReq?: () => void;
onDelete?: () => void;
}
const PRIORITY_COLORS: Record<string, string> = {
P0: 'bg-red-500/10 text-red-600',
P1: 'bg-orange-500/10 text-orange-600',
P2: 'bg-blue-500/10 text-blue-600',
P3: 'bg-zinc-100 text-zinc-600',
P4: 'bg-zinc-100 text-zinc-500',
};
export function RequirementDetail({
requirement: req,
projects,
types,
platforms,
sourceTargets,
resolveVersionName,
onClose,
onEdit,
onAdopt,
onReject,
onCloseReq,
onDelete,
}: RequirementDetailProps) {
const canEdit = ['pending_review', 'adopted', 'planned'].includes(req.status);
const projectName = projects.find((p) => p.id === req.projectId)?.name ?? '-';
const typeName = types.find((t) => t.id === req.typeId)?.name ?? '-';
const platformNames = req.platforms.map((pid) => platforms.find((p) => p.id === pid)?.name ?? pid).join('、') || '-';
return (
<div className="fixed inset-0 z-50 flex justify-end bg-black/30" onClick={onClose}>
<div className="w-[480px] h-full bg-[var(--bg-card)] border-l border-[var(--line)] flex flex-col shadow-xl" onClick={(e) => e.stopPropagation()}>
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--line)]">
<div className="flex items-center gap-2.5">
<span className="text-[13px] font-semibold text-[var(--ink)]">{req.code}</span>
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-medium ${REQ_STATUS_COLOR[req.status]}`}>
{REQ_STATUS_LABEL[req.status]}
</span>
</div>
<button onClick={onClose} className="p-1.5 rounded-md hover:bg-[var(--bg-subtle)] text-[var(--ink-muted)]">
<X className="h-4 w-4" />
</button>
</div>
{/* Actions */}
<div className="flex items-center gap-2 px-5 py-3 border-b border-[var(--line)]">
{canEdit && (
<button onClick={onEdit} className="h-7 px-3 rounded-md text-[12px] font-medium border border-[var(--line)] text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)] transition-colors">
</button>
)}
{onAdopt && req.status === 'pending_review' && (
<button onClick={onAdopt} className="h-7 px-3 rounded-md text-[12px] font-medium text-emerald-600 border border-emerald-200 hover:bg-emerald-50 transition-colors">
</button>
)}
{onReject && req.status === 'pending_review' && (
<button onClick={onReject} className="h-7 px-3 rounded-md text-[12px] font-medium text-red-600 border border-red-200 hover:bg-red-50 transition-colors">
</button>
)}
{onCloseReq && (req.status === 'adopted' || req.status === 'planned') && (
<button onClick={onCloseReq} className="h-7 px-3 rounded-md text-[12px] font-medium text-orange-600 border border-orange-200 hover:bg-orange-50 transition-colors">
</button>
)}
{onDelete && ['pending_review', 'adopted', 'rejected', 'closed'].includes(req.status) && (
<button onClick={onDelete} className="h-7 px-3 rounded-md text-[12px] font-medium text-red-500 border border-red-200 hover:bg-red-50 transition-colors">
</button>
)}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-4">
{/* 需求概述 */}
<div>
<h2 className="text-[15px] font-semibold text-[var(--ink)] leading-snug">{req.title}</h2>
</div>
{/* 需求描述 */}
{req.description && (
<div>
<Label></Label>
<p className="text-[13px] text-[var(--ink-soft)] whitespace-pre-wrap leading-relaxed">{req.description}</p>
</div>
)}
{/* 基本信息 */}
<div className="rounded-lg border border-[var(--line)] divide-y divide-[var(--line)]">
<FieldRow label="需求来源" value={`${SOURCE_TYPE_LABEL[req.sourceType]} · ${req.sourceTarget || '-'}`} />
<FieldRow label="所属项目" value={projectName} />
<FieldRow label="需求类型" value={typeName} />
<FieldRow label="支持端" value={platformNames} />
<FieldRow label="优先级">
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-medium ${PRIORITY_COLORS[req.priority] ?? ''}`}>
{req.priority}
</span>
</FieldRow>
{req.effort && (
<FieldRow label="工作量">
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-medium ${EFFORT_COLOR[req.effort]}`}>
{EFFORT_SHORT[req.effort]}
</span>
</FieldRow>
)}
<FieldRow label="所属版本" value={resolveVersionName(req.versionId)} />
</div>
{/* 人员 & 日期 */}
<div className="rounded-lg border border-[var(--line)] divide-y divide-[var(--line)]">
<FieldRow label="产品负责人" value={req.productOwner || '-'} />
<FieldRow label="录入人员" value={req.creator} />
<FieldRow label="录入日期" value={req.createdAt.slice(0, 10)} />
</div>
{/* 关联父需求 */}
{req.parentId && (
<div className="rounded-lg border border-[var(--line)]">
<FieldRow label="父需求" value={req.parentId} />
</div>
)}
</div>
</div>
</div>
);
}
function Label({ children }: { children: React.ReactNode }) {
return <div className="text-[11px] font-medium text-[var(--ink-muted)] mb-1">{children}</div>;
}
function FieldRow({ label, value, children }: { label: string; value?: string; children?: React.ReactNode }) {
return (
<div className="flex items-center justify-between px-4 py-2.5">
<span className="text-[12px] text-[var(--ink-muted)]">{label}</span>
{children ?? <span className="text-[13px] text-[var(--ink)]">{value}</span>}
</div>
);
}