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:
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user