- 需求模块:完整 CRUD、状态流转(采纳/拒绝/关闭)、详情抽屉、产品→项目级联选择 - 加班记录:产品→项目→版本三级联动、月份筛选(MonthPicker)、CSV 导出 - 成员管理:左右布局(部门树+成员列表)、手机号脱敏、初始密码自动生成及规则设置 - 角色管理:卡片列表、系统角色保护、CRUD - 通用组件:FilterSelect 下拉、MonthPicker 月份选择器、Pagination 分页 - 样式统一:状态标签加 border、日期输入现代化、筛选组件风格一致 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
161 lines
6.7 KiB
TypeScript
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>
|
|
);
|
|
}
|