feat: refine web auth form and admin layout spacing
This commit is contained in:
@@ -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`。
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user