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 && ( +
+ + +
+ )}
项目 版本