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

@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from 'react';
import { Search, Plus, ChevronDown, Clock, Pencil, Trash2, X, Download } from 'lucide-react';
import { useOvertimeStore } from '@/stores/useOvertimeStore';
import { useProductStore } from '@/stores/useProductStore';
import { useRequirementStore } from '@/stores/useRequirementStore';
import { flattenProjects, flattenVersions } from '@/lib/derive';
import { calcDuration } from '@/lib/overtime';
import type { OvertimeRecord } from '@/lib/overtime';
@@ -15,6 +16,7 @@ import { FilterSelect } from '@/components/FilterSelect';
export default function OvertimePage() {
const { records, fetchRecords, createRecord, updateRecord, deleteRecord, reasons, addReason, updateReason, deleteReason } = useOvertimeStore();
const { overview, fetchOverview } = useProductStore();
const { requirements, fetchRequirements } = useRequirementStore();
const allProjects = useMemo(() => flattenProjects(overview), [overview]);
const allVersions = useMemo(() => flattenVersions(overview), [overview]);
@@ -27,6 +29,7 @@ export default function OvertimePage() {
const [showReasonDrawer, setShowReasonDrawer] = useState(false);
useEffect(() => { fetchOverview(); }, [fetchOverview]);
useEffect(() => { fetchRequirements(); }, [fetchRequirements]);
useEffect(() => { fetchRecords(); }, [fetchRecords]);
const projectName = (id: string) => allProjects.find((p) => p.id === id)?.name ?? '-';
@@ -124,9 +127,9 @@ export default function OvertimePage() {
</div>
) : (
<>
<div className="overflow-hidden rounded-2xl border border-[var(--line)] bg-[var(--bg-card)] shadow-[var(--shadow-sm)]">
<div className="rounded-2xl border border-[var(--line)] bg-[var(--bg-card)] shadow-[var(--shadow-sm)]">
<table className="w-full text-left text-[13px]">
<thead className="sticky top-0 z-10">
<thead className="sticky top-0 z-10 bg-[var(--bg-subtle)]">
<tr className="border-b border-[var(--line)] bg-[var(--bg-subtle)]">
<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>
@@ -182,6 +185,7 @@ export default function OvertimePage() {
projects={allProjects.map((p) => ({ id: p.id, name: p.name, productId: p.productId }))}
versions={allVersions}
reasons={reasons}
requirements={requirements.map((r) => ({ id: r.id, title: r.title, versionId: r.versionId }))}
onClose={() => setShowModal(false)}
onSubmit={(data) => {
if (editing) updateRecord(editing.id, data);
@@ -199,12 +203,13 @@ export default function OvertimePage() {
);
}
function OvertimeModal({ initial, products, projects, versions, reasons, onClose, onSubmit }: {
function OvertimeModal({ initial, products, projects, versions, reasons, requirements, onClose, onSubmit }: {
initial: OvertimeRecord | null;
products: { id: string; name: string }[];
projects: { id: string; name: string; productId: string }[];
versions: { id: string; name: string; projectId?: string }[];
reasons: { id: string; name: string }[];
requirements: { id: string; title: string; versionId?: string }[];
onClose: () => void;
onSubmit: (data: any) => void;
}) {
@@ -216,6 +221,7 @@ function OvertimeModal({ initial, products, projects, versions, reasons, onClose
const [endTime, setEndTime] = useState(initial?.endTime ?? '');
const [reasonId, setReasonId] = useState(initial?.reasonId ?? '');
const [remark, setRemark] = useState(initial?.remark ?? '');
const [requirementId, setRequirementId] = useState(initial?.requirementId ?? '');
// 初始编辑时反推 productId
useEffect(() => {
@@ -228,11 +234,12 @@ function OvertimeModal({ initial, products, projects, versions, reasons, onClose
const duration = startTime && endTime ? calcDuration(startTime, endTime) : 0;
const filteredProjects = productId ? projects.filter((p) => p.productId === productId) : projects;
const filteredVersions = projectId ? versions.filter((v) => (v as any).projectId === projectId) : [];
const filteredRequirements = versionId ? requirements.filter((r) => r.versionId === versionId) : [];
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!projectId || !person.trim() || !startTime || !endTime || !reasonId) return;
onSubmit({ projectId, versionId: versionId || undefined, person: person.trim(), startTime, endTime, reasonId, remark: remark.trim() || undefined });
onSubmit({ projectId, versionId: versionId || undefined, requirementId: requirementId || undefined, person: person.trim(), startTime, endTime, reasonId, remark: remark.trim() || undefined });
};
return (
@@ -292,6 +299,15 @@ function OvertimeModal({ initial, products, projects, versions, reasons, onClose
{reasons.map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}
</select>
</div>
{filteredRequirements.length > 0 && (
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"></label>
<select value={requirementId} onChange={(e) => setRequirementId(e.target.value)} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none">
<option value=""></option>
{filteredRequirements.map((r) => <option key={r.id} value={r.id}>{r.title}</option>)}
</select>
</div>
)}
<div>
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block"></label>
<textarea value={remark} onChange={(e) => setRemark(e.target.value)} rows={2} placeholder="可选" className="w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 py-2 text-[13px] focus:border-[var(--accent)] focus:outline-none resize-none" />