feat: 版本详情完整功能 + 数据串联 + 登录模块

- 版本详情:关联需求Tab(从需求池添加已采纳需求/移除释放)
- 版本详情:调研/产品方案/UI设计Tab(计划CRUD、完成提交成果、耗时统计)
- 版本详情:超期校验(结束日期超版本截止需填写原因)
- 版本详情:概览统计卡片(加班时长/排名/原因占比)
- 数据串联:产品→项目→版本→需求→加班全链路贯通
- 版本管理:规划中版本可删除,删除释放关联需求
- 数据清理:仅保留翻台宝/值班,需求池10条值班待评审需求
- 修复:列表页overflow裁剪菜单问题(4个页面统一修复)
- 新增:登录模块、AuthGuard、VersionPlan store

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Script Generator
2026-06-10 17:07:36 +08:00
parent 2f69bc74cd
commit 2acc9aeaa9
21 changed files with 1278 additions and 323 deletions

View File

@@ -6,6 +6,7 @@ import { ChevronLeft, Pencil, Trash2 } from 'lucide-react';
import type { RequirementStatus } from '@/lib/requirement';
import { useProductStore } from '@/stores/useProductStore';
import { useRequirementStore } from '@/stores/useRequirementStore';
import { useOvertimeStore } from '@/stores/useOvertimeStore';
import { RequirementTable } from '@/components/product/RequirementTable';
import { RequirementForm } from '@/components/product/RequirementForm';
import { ProductForm } from '@/components/product/ProductForm';
@@ -27,6 +28,7 @@ export default function ProductDetailPage() {
fetchRequirements,
deleteRequirement,
} = useRequirementStore();
const { records, fetchRecords } = useOvertimeStore();
const [showReqForm, setShowReqForm] = useState(false);
const [editingReq, setEditingReq] = useState<any>(null);
@@ -37,7 +39,8 @@ export default function ProductDetailPage() {
useEffect(() => {
fetchProduct(productId);
fetchRequirements();
}, [productId, fetchProduct, fetchRequirements]);
fetchRecords();
}, [productId, fetchProduct, fetchRequirements, fetchRecords]);
const productRequirements = useMemo(() => {
let list = requirements.filter((r) => r.productId === productId);
@@ -112,6 +115,27 @@ export default function ProductDetailPage() {
{currentProduct.description && (
<p className="line-clamp-2 py-3 text-[13px] text-[var(--ink-soft)]">{currentProduct.description}</p>
)}
{/* 汇总统计 */}
<div className="flex gap-4 py-3 border-b border-[var(--line-soft)]">
{(() => {
const allReqs = requirements.filter((r) => r.productId === productId);
const adopted = allReqs.filter((r) => r.status === 'adopted' || r.status === 'planned').length;
const developing = allReqs.filter((r) => r.status === 'developing' || r.status === 'testing').length;
const released = allReqs.filter((r) => r.status === 'released').length;
const projectReqs = allReqs.filter((r) => r.projectId);
const projectIds = [...new Set(projectReqs.map((r) => r.projectId!))];
const overtimeHours = Math.round(records.filter((r) => projectIds.includes(r.projectId)).reduce((sum, r) => sum + r.duration, 0) * 10) / 10;
return (
<>
<div className="text-center"><div className="text-[16px] font-semibold text-[var(--ink)]">{allReqs.length}</div><div className="text-[11px] text-[var(--ink-muted)]"></div></div>
<div className="text-center"><div className="text-[16px] font-semibold text-blue-600">{adopted}</div><div className="text-[11px] text-[var(--ink-muted)]"></div></div>
<div className="text-center"><div className="text-[16px] font-semibold text-amber-600">{developing}</div><div className="text-[11px] text-[var(--ink-muted)]"></div></div>
<div className="text-center"><div className="text-[16px] font-semibold text-green-600">{released}</div><div className="text-[11px] text-[var(--ink-muted)]"></div></div>
<div className="text-center"><div className="text-[16px] font-semibold text-red-500">{overtimeHours}h</div><div className="text-[11px] text-[var(--ink-muted)]"></div></div>
</>
);
})()}
</div>
<div className="flex gap-1">
{TABS.map((tab) => (
<button