feat: refine web auth form and admin layout spacing

This commit is contained in:
chengkai3
2026-04-12 22:45:38 +08:00
parent 4001276c5d
commit fcec3a4c31
3 changed files with 78 additions and 8 deletions
+25
View File
@@ -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`
+2 -2
View File
@@ -91,7 +91,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
<div className="absolute right-[-6rem] top-20 h-96 w-96 rounded-full bg-sky-300/30 blur-3xl" />
</div>
<div className="relative mx-auto grid min-h-screen w-full max-w-[1360px] grid-cols-1 gap-0 md:grid-cols-[280px_minmax(0,1fr)]">
<div className="relative mx-auto grid min-h-screen w-full max-w-[1760px] grid-cols-1 gap-0 px-3 sm:px-4 xl:px-6 md:grid-cols-[280px_minmax(0,1fr)]">
<aside className="border-r border-[var(--border)] bg-white/70 p-6 backdrop-blur-xl md:sticky md:top-0 md:h-screen md:overflow-y-auto">
<div className="mb-8">
<Link href="/" className="text-xl font-bold tracking-tight text-slate-900">fquiz admin</Link>
@@ -140,7 +140,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
</div>
</aside>
<main className="p-6 md:p-8">
<main className="p-4 md:p-6">
<div className="surface-card mb-6 flex items-center justify-between gap-4 bg-gradient-to-br from-white/95 via-cyan-50/65 to-sky-50/80">
<div>
<p className="text-sm text-muted"></p>
+51 -6
View File
@@ -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<PingResponse | null>(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<RememberedCredentials>;
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<HTMLFormElement>) => {
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() {
</div>
<form className="space-y-3" onSubmit={handleSubmit}>
<h2 className="text-base font-medium">{title}</h2>
<h2 className="text-base font-medium">
{mode === "login" ? "登录" : "注册"}
</h2>
<input
className="control w-full"
placeholder="Email"
type="email"
autoComplete="username"
value={email}
onChange={(event) => 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" && (
<label className="flex items-center gap-2 text-sm text-muted">
<input
type="checkbox"
checked={rememberPassword}
onChange={(event) => setRememberPassword(event.target.checked)}
/>
</label>
)}
<button
className="btn-primary"
disabled={busy}