简化登录页并新增记住密码

This commit is contained in:
chengkai3
2026-05-01 19:39:16 +08:00
parent 0728cf7edf
commit 1aa23508b1
2 changed files with 102 additions and 111 deletions
+23
View File
@@ -478,3 +478,26 @@
- 风险与影响:
- 影响范围限定在前端管理页交互层;后端接口仍保持兼容,可继续返回权限相关字段但前端不再暴露配置入口。
- 若后续需彻底下线该能力(含后端字段/持久化),需单独评估接口契约与历史数据兼容。
## Work Log - 登录页面还原最简洁样式并保留记住密码(2026-05-01)
- 背景:
- Issue `FL-144` 要求将登录页还原为最简洁样式,保留“登录、记住密码”功能,并将标题改为“防雷计算”。
- 本次改动(最小闭环):
- 文件:`web/src/app/page.tsx`
- 去除登录页注册模式相关状态与 UI`mode/register/username/切换按钮`),仅保留登录流程。
- 页面主标题改为 `防雷计算`
- 视觉样式收敛为简洁白底 + 居中卡片布局,移除装饰性图标块、渐变、复杂文案。
- 新增“记住密码”复选框:
- 勾选后登录成功时将用户 ID 与密码写入 `localStorage`
- 未勾选时清理本地缓存;
- 页面加载时若已记住则自动回填账号密码并默认勾选。
- 验证:
- 代码路径自检:登录仍走 `useAuth().login` 既有链路,未改动鉴权接口与路由跳转逻辑。
- 按任务约束未执行编译/安装依赖。
- 风险与影响:
- 影响范围:仅前端登录页 `web/src/app/page.tsx`
- 风险:记住密码当前使用浏览器 `localStorage` 明文存储,存在本机安全暴露风险(符合需求但需知悉)。
+79 -111
View File
@@ -1,23 +1,24 @@
"use client";
import { IdcardOutlined, LockOutlined, UserOutlined } from "@ant-design/icons";
import { IdcardOutlined, LockOutlined } from "@ant-design/icons";
import { ChangeEvent, FormEvent, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Alert, Button, Input, Space, Typography } from "antd";
import { Alert, Button, Checkbox, Input, Space, Typography } from "antd";
import { useAuth } from "@/components/auth-provider";
import { Card } from "@/components/ui-antd";
type Mode = "login" | "register";
const LOGIN_REMEMBER_KEY = "login.remember";
const LOGIN_USER_ID_KEY = "login.user_id";
const LOGIN_PASSWORD_KEY = "login.password";
export default function Home() {
const router = useRouter();
const { user, initializing, login, register } = useAuth();
const { user, initializing, login } = useAuth();
const [mode, setMode] = useState<Mode>("login");
const [userId, setUserId] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [rememberPassword, setRememberPassword] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
@@ -27,8 +28,20 @@ export default function Home() {
}
}, [initializing, router, user]);
const formTitle = mode === "login" ? "登录你的工作台" : "创建你的工作台";
const submitLabel = mode === "login" ? "登录" : "创建账号";
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const remembered = window.localStorage.getItem(LOGIN_REMEMBER_KEY) === "1";
if (!remembered) {
return;
}
setRememberPassword(true);
setUserId(window.localStorage.getItem(LOGIN_USER_ID_KEY) ?? "");
setPassword(window.localStorage.getItem(LOGIN_PASSWORD_KEY) ?? "");
}, []);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -36,11 +49,21 @@ export default function Home() {
setBusy(true);
try {
if (mode === "login") {
await login(userId.trim(), password);
} else {
await register(`${username.trim() || userId.trim()}@example.local`, username.trim(), password);
const normalizedUserId = userId.trim();
await login(normalizedUserId, password);
if (typeof window !== "undefined") {
if (rememberPassword) {
window.localStorage.setItem(LOGIN_REMEMBER_KEY, "1");
window.localStorage.setItem(LOGIN_USER_ID_KEY, normalizedUserId);
window.localStorage.setItem(LOGIN_PASSWORD_KEY, password);
} else {
window.localStorage.removeItem(LOGIN_REMEMBER_KEY);
window.localStorage.removeItem(LOGIN_USER_ID_KEY);
window.localStorage.removeItem(LOGIN_PASSWORD_KEY);
}
}
setPassword("");
} catch (submitError) {
const message = submitError instanceof Error ? submitError.message : "未知错误";
@@ -67,108 +90,53 @@ export default function Home() {
}
return (
<main className="min-h-screen bg-[#eef2f8] px-4 py-6 sm:px-8 sm:py-10">
<section className="mx-auto flex w-full max-w-[760px] items-center justify-center rounded-[24px] bg-[#f3f4f6] p-4 shadow-[0_20px_48px_rgba(15,23,42,0.12)] sm:p-8">
<Card className="w-full border-0 shadow-none" styles={{ body: { padding: "24px 16px" } }}>
<Space direction="vertical" size={22} className="w-full">
<div className="flex items-center justify-center gap-4">
<div className="grid h-[60px] w-[60px] place-items-center rounded-[16px] bg-[linear-gradient(160deg,#0ccbf0_0%,#3571ff_54%,#7156f8_100%)] text-[32px] font-bold text-white shadow-[0_10px_20px_rgba(53,113,255,0.32)]">
D
</div>
<div>
<Typography.Title level={2} className="!mb-0 !text-[#0f1b36]">
</Typography.Title>
<Typography.Text type="secondary">Development Intelligence Platform</Typography.Text>
</div>
<main className="flex min-h-screen items-center justify-center bg-white px-4 py-8">
<Card className="w-full max-w-[360px]">
<Space direction="vertical" size={20} className="w-full">
<Typography.Title level={3} className="!mb-0 !text-center">
</Typography.Title>
<form className="space-y-4" onSubmit={handleSubmit}>
<Input
size="large"
value={userId}
prefix={<IdcardOutlined />}
placeholder="用户 ID"
onChange={(event: ChangeEvent<HTMLInputElement>) => setUserId(event.currentTarget.value)}
autoComplete="username"
required
/>
<Input.Password
size="large"
value={password}
prefix={<LockOutlined />}
placeholder="密码"
onChange={(event: ChangeEvent<HTMLInputElement>) => setPassword(event.currentTarget.value)}
autoComplete="current-password"
minLength={1}
maxLength={128}
required
/>
<div className="flex items-center justify-between">
<Checkbox
checked={rememberPassword}
onChange={(event) => setRememberPassword(event.target.checked)}
>
</Checkbox>
</div>
<Typography.Title level={2} className="!mb-0 !text-center !text-[#0f1b36]">
{formTitle}
</Typography.Title>
<Button block size="large" type="primary" htmlType="submit" loading={busy}>
</Button>
</form>
<form className="space-y-4" onSubmit={handleSubmit}>
<div>
<Typography.Text strong className="mb-2 block tracking-wide text-[#1e293b]">
ID
</Typography.Text>
<Input
size="large"
value={userId}
prefix={<IdcardOutlined className="text-slate-400" />}
placeholder="请输入用户ID"
onChange={(event: ChangeEvent<HTMLInputElement>) => setUserId(event.currentTarget.value)}
className="h-[48px] rounded-[10px]"
autoComplete="username"
required
/>
</div>
{mode === "register" && (
<div>
<Typography.Text strong className="mb-2 block tracking-wide text-[#1e293b]">
</Typography.Text>
<Input
size="large"
value={username}
prefix={<UserOutlined className="text-slate-400" />}
placeholder="请输入用户名"
onChange={(event: ChangeEvent<HTMLInputElement>) => setUsername(event.currentTarget.value)}
className="h-[48px] rounded-[10px]"
minLength={3}
maxLength={64}
required
/>
</div>
)}
<div>
<Typography.Text strong className="mb-2 block tracking-wide text-[#1e293b]">
</Typography.Text>
<Input.Password
size="large"
value={password}
prefix={<LockOutlined className="text-slate-400" />}
placeholder={mode === "login" ? "请输入密码" : "请输入密码(至少 8 位)"}
onChange={(event: ChangeEvent<HTMLInputElement>) => setPassword(event.currentTarget.value)}
className="h-[48px] rounded-[10px]"
autoComplete={mode === "login" ? "current-password" : "new-password"}
minLength={mode === "login" ? 1 : 8}
maxLength={128}
required
/>
</div>
<Button block size="large" type="primary" htmlType="submit" loading={busy}>
{submitLabel}
</Button>
{mode === "login" && (
<div className="flex justify-end">
<Button type="link" onClick={() => setError("请联系管理员重置密码。")}>
</Button>
</div>
)}
<Button
block
type="link"
onClick={() => {
setError("");
setMode((current) => (current === "login" ? "register" : "login"));
}}
>
{mode === "login" ? "创建新项目?" : "返回登录"}
</Button>
</form>
{error && <Alert showIcon type="error" message={error} />}
</Space>
</Card>
</section>
{error && <Alert showIcon type="error" message={error} />}
</Space>
</Card>
</main>
);
}