v0.14.0 设置页 Tabs 表单+登录安全(reCAPTCHA/锁定);TipTap 富文本编辑器(Markdown/媒体库);前台移动端自适应

- /admin/settings 弃用 JSON 编辑器改 antd Tabs 分组表单(site/woocommerce/mail/seo/security),未知键落「其他」JSON 兜底
- 登录安全:reCAPTCHA v2/v3 开关+密钥+阈值(siteverify fail closed),失败次数锁定;/admin 与 /my-account 登录接入
- 内容正文与产品详细描述换 TipTap v3:工具栏/Markdown 输入与导入/HTML 源码/图片上传/媒体库选图
- 根路由补 viewport meta(移动端根因);390px 实测 14 关键页 0 横向溢出
- 首页 Advance Tabs ≤767px 横滑、联系页内联 margin 块收缩、/shop 网格 4→3→2 列、移动菜单手风琴
This commit is contained in:
bigcode
2026-11-04 14:08:49 +08:00
parent 7fd9e75887
commit 62e6bcfcd2
18 changed files with 1530 additions and 81 deletions
+29 -6
View File
@@ -23,9 +23,11 @@ import {
SlidersHorizontal,
UserRound,
} from "lucide-react";
import { useState, type ReactNode } from "react";
import { useState, useEffect, type ReactNode } from "react";
import { StyleProvider } from "@ant-design/cssinjs";
import { getCurrentUser, loginFn, logoutFn } from "~/server/auth";
import { getLoginSecurityPublic } from "~/server/security";
import { RecaptchaField, useRecaptcha, type LoginSecurityPublic } from "~/components/admin/Recaptcha";
/** # 类型 后台菜单分组(分组标签不可点;key 即目标路由路径) **/
const MENU_GROUPS: { label: string; children: { key: string; icon: ReactNode; label: string }[] }[] = [
@@ -103,21 +105,40 @@ function AdminLayout() {
return <AdminShell user={user} />;
}
/** # 组件 后台登录页(全屏居中卡片,邮箱 + 密码) */
/** # 组件 后台登录页(全屏居中卡片,邮箱 + 密码;reCAPTCHA 按系统设置「登录安全」开关渲染) */
function AdminLoginPage(props: { noPermission: boolean; onOk: () => Promise<void> }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
/** # 成员变量 登录页公开配置(reCAPTCHA 启用状态/版本/站点密钥) **/
const [secCfg, setSecCfg] = useState<LoginSecurityPublic | null>(null);
const recaptcha = useRecaptcha(secCfg);
/** # 函数 登录提交:调用 loginFn,成功回调 onOk,失败展示错误 */
// 登录页挂载后拉取登录安全公开配置(未启用时返回 enabled=false,不注入脚本)
useEffect(() => {
void getLoginSecurityPublic()
.then(setSecCfg)
.catch(() => setSecCfg({ enabled: false, version: "v2", siteKey: "" }));
}, []);
/** # 函数 登录提交:调用 loginFn(附 reCAPTCHA 令牌),成功回调 onOk,失败展示错误并重置复选框 */
async function handleFinish(values: { email: string; password: string }) {
setLoading(true);
setError("");
try {
const res = await loginFn({ data: { email: values.email, password: values.password } });
if (!res.ok) setError(res.error);
else await props.onOk();
const token = await recaptcha.getToken();
if (secCfg?.enabled && token === null) {
setError("人机验证未完成,请完成勾选/等待验证脚本加载后重试");
recaptcha.reset();
return;
}
const res = await loginFn({ data: { email: values.email, password: values.password, recaptchaToken: token ?? undefined } });
if (!res.ok) {
setError(res.error);
recaptcha.reset();
} else await props.onOk();
} catch (e) {
setError(e instanceof Error ? e.message : "登录失败,请稍后重试");
recaptcha.reset();
} finally {
setLoading(false);
}
@@ -144,6 +165,8 @@ function AdminLoginPage(props: { noPermission: boolean; onOk: () => Promise<void
<Form.Item name="password" label="密码" rules={[{ required: true, message: "请输入密码" }]}>
<Input.Password placeholder="密码" autoComplete="current-password" />
</Form.Item>
{/* reCAPTCHA(系统设置 → 登录安全 开启后出现;v2 复选框 / v3 隐形) */}
<RecaptchaField cfg={secCfg} rec={recaptcha} />
{error ? (
<div role="alert" style={{ color: "#cf1322", background: "#fff1f0", border: "1px solid #ffa39e", borderRadius: 6, padding: "8px 12px", marginBottom: 16, fontSize: 13 }}>
{error}