diff --git a/memory/2026-04-12.md b/memory/2026-04-12.md index 46b819c..df5e2e5 100644 --- a/memory/2026-04-12.md +++ b/memory/2026-04-12.md @@ -168,3 +168,28 @@ - 展开结果确认: - `db.ports.published` 为 `5433`。 - `db.image` 为 `docker.m.daocloud.io/pgvector/pgvector:pg16`。 + +## 追加优化(后台页面左右留白收敛) + +- 背景: + - 用户反馈后台页面左右两侧 margin 偏大,宽屏下可用内容空间浪费。 +- 处理: + - `web/src/app/admin/layout.tsx` 将主容器最大宽度由 `max-w-[1360px]` 提升到 `max-w-[1760px]`。 + - 同时增加轻量响应式外层横向内边距(`px-3 sm:px-4 xl:px-6`),避免贴边。 + - 主内容区内边距从 `p-6 md:p-8` 收敛为 `p-4 md:p-6`,进一步释放表格可视宽度。 +- 验证: + - `cd web && npx eslint src/app/admin/layout.tsx` 通过。 + +## 追加修复(Web 镜像构建 TypeScript 阻塞) + +- 触发问题: + - `docker compose build web` 在 `web/Dockerfile` 的 `RUN npm run build` 失败。 + - 报错定位在 `web/src/app/page.tsx:190` 附近(`placeholder="Email"` 上下文),实际错误为 `Cannot find name 'title'`。 +- 根因: + - 登录/注册表单标题渲染使用了未定义变量 `title`,导致 TypeScript 检查失败并中断 Next.js 构建。 +- 处理: + - `web/src/app/page.tsx` 将 `{title}` 改为基于 `mode` 的内联表达式: + - 登录显示 `登录` + - 注册显示 `注册` +- 验证: + - 复跑 `docker compose build web`,构建成功,路由静态生成完成,最终输出 `fquiz-web Built`。 diff --git a/web/src/app/admin/layout.tsx b/web/src/app/admin/layout.tsx index 682dd93..25adb92 100644 --- a/web/src/app/admin/layout.tsx +++ b/web/src/app/admin/layout.tsx @@ -91,7 +91,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
-
+
-
+

后台管理

diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 8b8940d..9213e93 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { FormEvent, useMemo, useState } from "react"; +import { FormEvent, useEffect, useState } from "react"; import { useAuth } from "@/components/auth-provider"; import { API_BASE_URL, readApiError } from "@/lib/api"; @@ -9,6 +9,13 @@ import { API_BASE_URL, readApiError } from "@/lib/api"; type Mode = "login" | "register"; type PingResponse = { message: string }; +type RememberedCredentials = { + email: string; + password: string; +}; + +const REMEMBER_CREDENTIALS_KEY = "fquiz.remembered_credentials"; + export default function Home() { const { user, initializing, login, register, logout, hasPermission } = useAuth(); @@ -16,14 +23,29 @@ export default function Home() { const [email, setEmail] = useState(""); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); + const [rememberPassword, setRememberPassword] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [pingResult, setPingResult] = useState(null); - const title = useMemo( - () => (mode === "login" ? "登录账号" : "注册新账号"), - [mode], - ); + useEffect(() => { + try { + const raw = window.localStorage.getItem(REMEMBER_CREDENTIALS_KEY); + if (!raw) { + return; + } + const saved = JSON.parse(raw) as Partial; + if (typeof saved.email === "string") { + setEmail(saved.email); + } + if (typeof saved.password === "string") { + setPassword(saved.password); + setRememberPassword(true); + } + } catch { + window.localStorage.removeItem(REMEMBER_CREDENTIALS_KEY); + } + }, []); const handleSubmit = async (event: FormEvent) => { event.preventDefault(); @@ -32,6 +54,15 @@ export default function Home() { try { if (mode === "login") { await login(email, password); + if (rememberPassword) { + const credentials: RememberedCredentials = { email, password }; + window.localStorage.setItem( + REMEMBER_CREDENTIALS_KEY, + JSON.stringify(credentials), + ); + } else { + window.localStorage.removeItem(REMEMBER_CREDENTIALS_KEY); + } } else { await register(email, username, password); } @@ -156,11 +187,14 @@ export default function Home() {
-

{title}

+

+ {mode === "login" ? "登录" : "注册"} +

setEmail(event.target.value)} required @@ -181,12 +215,23 @@ export default function Home() { className="control w-full" placeholder="Password (>= 8 chars)" type="password" + autoComplete={mode === "login" ? "current-password" : "new-password"} value={password} onChange={(event) => setPassword(event.target.value)} minLength={8} maxLength={128} required /> + {mode === "login" && ( + + )}