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:
+29
-6
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user