From 2acc9aeaa9bb6dd713f996035052e6435bf1ea49 Mon Sep 17 00:00:00 2001 From: Script Generator Date: Wed, 10 Jun 2026 17:07:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=89=88=E6=9C=AC=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E5=8A=9F=E8=83=BD=20+=20=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=B8=B2=E8=81=94=20+=20=E7=99=BB=E5=BD=95=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 版本详情:关联需求Tab(从需求池添加已采纳需求/移除释放) - 版本详情:调研/产品方案/UI设计Tab(计划CRUD、完成提交成果、耗时统计) - 版本详情:超期校验(结束日期超版本截止需填写原因) - 版本详情:概览统计卡片(加班时长/排名/原因占比) - 数据串联:产品→项目→版本→需求→加班全链路贯通 - 版本管理:规划中版本可删除,删除释放关联需求 - 数据清理:仅保留翻台宝/值班,需求池10条值班待评审需求 - 修复:列表页overflow裁剪菜单问题(4个页面统一修复) - 新增:登录模块、AuthGuard、VersionPlan store Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/app/admin/members/page.tsx | 4 +- apps/web/app/layout.tsx | 7 +- apps/web/app/login/page.tsx | 124 ++++++ apps/web/app/overtime/page.tsx | 24 +- apps/web/app/products/[id]/page.tsx | 26 +- apps/web/app/projects/[id]/page.tsx | 28 +- apps/web/app/requirements/page.tsx | 23 +- apps/web/app/versions/[id]/page.tsx | 365 ++++++++++++------ apps/web/app/versions/page.tsx | 31 +- apps/web/components/AuthGuard.tsx | 33 ++ apps/web/components/layout/LayoutShell.tsx | 23 ++ .../requirement/RequirementModal.tsx | 34 +- apps/web/components/version/PlanTab.tsx | 311 +++++++++++++++ .../version/VersionRequirementsTab.tsx | 138 +++++++ apps/web/lib/overtime.ts | 3 +- apps/web/lib/requirement.ts | 1 + apps/web/lib/version-plan.ts | 65 ++++ apps/web/stores/useAuthStore.ts | 91 +++++ apps/web/stores/useProductStore.ts | 56 +-- apps/web/stores/useRequirementStore.ts | 152 +------- apps/web/stores/useVersionPlanStore.ts | 62 +++ 21 files changed, 1278 insertions(+), 323 deletions(-) create mode 100644 apps/web/app/login/page.tsx create mode 100644 apps/web/components/AuthGuard.tsx create mode 100644 apps/web/components/layout/LayoutShell.tsx create mode 100644 apps/web/components/version/PlanTab.tsx create mode 100644 apps/web/components/version/VersionRequirementsTab.tsx create mode 100644 apps/web/lib/version-plan.ts create mode 100644 apps/web/stores/useAuthStore.ts create mode 100644 apps/web/stores/useVersionPlanStore.ts diff --git a/apps/web/app/admin/members/page.tsx b/apps/web/app/admin/members/page.tsx index 6077bac..e178a1a 100644 --- a/apps/web/app/admin/members/page.tsx +++ b/apps/web/app/admin/members/page.tsx @@ -135,9 +135,9 @@ export default function MembersPage() {

暂无成员

) : ( -
+
- + diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 64d3b6f..a0158fc 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,6 +1,6 @@ import './globals.css'; import type { Metadata } from 'next'; -import { Sidebar } from '@/components/layout/Sidebar'; +import { LayoutShell } from '@/components/layout/LayoutShell'; export const metadata: Metadata = { title: 'FTB 项目管理', @@ -14,9 +14,8 @@ export default function RootLayout({ }) { return ( - - -
{children}
+ + {children} ); diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx new file mode 100644 index 0000000..b73480f --- /dev/null +++ b/apps/web/app/login/page.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuthStore } from '@/stores/useAuthStore'; +import { LayoutGrid, Phone, Lock, Eye, EyeOff } from 'lucide-react'; + +export default function LoginPage() { + const router = useRouter(); + const { login } = useAuthStore(); + const [phone, setPhone] = useState(''); + const [password, setPassword] = useState(''); + const [remember, setRemember] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + + if (!phone.trim() || !password.trim()) { + setError('请输入手机号和密码'); + return; + } + + setLoading(true); + setTimeout(() => { + const success = login(phone.trim(), password, remember); + if (success) { + router.push('/products'); + } else { + setError('手机号或密码错误'); + } + setLoading(false); + }, 300); + }; + + return ( +
+
+ {/* Logo */} +
+
+ +
+

FTB 项目管理

+

登录以继续使用系统

+
+ + {/* Form */} +
+
+
+ +
+ + setPhone(e.target.value)} + placeholder="请输入手机号" + maxLength={11} + className="h-10 w-full rounded-lg border border-[var(--line)] bg-[var(--bg)] pl-10 pr-3 text-[14px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:border-[var(--accent)] focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)] transition-all" + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + placeholder="请输入密码" + className="h-10 w-full rounded-lg border border-[var(--line)] bg-[var(--bg)] pl-10 pr-10 text-[14px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:border-[var(--accent)] focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)] transition-all" + /> + +
+
+ +
+ +
+ + {error && ( +
+ {error} +
+ )} + + + + +

+ 默认账号:13200132008 / Ftb@2024(超级管理员) +

+
+
+
+ ); +} diff --git a/apps/web/app/overtime/page.tsx b/apps/web/app/overtime/page.tsx index ec4d8c4..ca1db5f 100644 --- a/apps/web/app/overtime/page.tsx +++ b/apps/web/app/overtime/page.tsx @@ -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() { ) : ( <> -
+
姓名 角色
- + @@ -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) => )} + {filteredRequirements.length > 0 && ( +
+ + +
+ )}
项目 版本