Skip to content

Commit 0851c95

Browse files
authored
feat(ui): improve blog navigation and social links
Improve frontend and admin UI/UX, add extensible social links, compact category navigation, and move D1 schema setup out of request path.\n\nIncludes review fixes for accessibility, archive filtering/error states, social URL validation, Cloudflare deploy script output capture, and D1 schema readiness checks.
1 parent 31bfc8e commit 0851c95

30 files changed

Lines changed: 1215 additions & 330 deletions

.gitignore

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ node_modules/
88
# ────────────────────── 构建产出(不推送)──────────────────────
99
dist/
1010
.wrangler/
11+
.cache/
12+
.vite/
13+
coverage/
14+
playwright-report/
15+
test-results/
1116

1217
# ────────────────────── 环境变量与密钥(绝不推送)──────────────────
1318
# 所有 .env 文件都可能含密钥,禁止推送
@@ -17,6 +22,7 @@ dist/
1722

1823
# Wrangler 本地密钥(含 ADMIN_PASSWORD / JWT_SECRET 等)
1924
.dev.vars
25+
.dev.vars.*
2026

2127
# ────────────────────── 系统与编辑器临时文件(不推送)──────────────
2228
.DS_Store
@@ -31,15 +37,15 @@ Thumbs.db
3137
*.code-workspace
3238

3339
# ────────────────────── AI 工具私有数据(绝不推送)──────────────────
34-
# 通用记忆库(Aider / Cline / 其他)
40+
# 通用记忆库(Aider / Cline / Serena / 其他)
3541
.agents/
42+
.serena/
3643

37-
# Trae 项目私有数据:memory/ 含密码教训、PAT 用法、踩坑记录等敏感信息
38-
# rules/ skills/ 也仅供本机女仆使用,不入仓库
39-
.trae/
44+
# Codex 项目私有 AI 上下文与本机配置,不入仓库
45+
.codex/
46+
scripts/maintain-codex-context.mjs
4047

4148
# 各家 AI 编辑器的项目入口与全局配置(含个人偏好/上下文,不入仓库)
42-
CLAUDE.md
4349
GEMINI.md
4450
AGENTS.md
4551
opencode.json
@@ -51,6 +57,9 @@ opencode.json
5157
# ────────────────────── 日志与调试产物(不推送)──────────────────
5258
*.log
5359
npm-debug.log*
60+
yarn-debug.log*
61+
yarn-error.log*
62+
pnpm-debug.log*
5463

5564
# ────────────────────── 测试与临时输出(不推送)──────────────────
5665
tmp/
@@ -61,6 +70,8 @@ tests/output/
6170
# ✅ package-lock.json — 锁定依赖版本,CI 的 npm ci 依赖它
6271
# ✅ client/functions/ — Pages Functions 反向代理,漏掉则 /api/* 回退为首页 HTML
6372
# ✅ scripts/ — 部署脚本 (deploy-cloudflare.mjs)
73+
# ✅ scripts/reconcile-d1-schema.mjs — 部署前 D1 schema 兼容补齐
74+
# ✅ server/src/migrations/ — D1 正式迁移,生产 schema 入口
6475
# ✅ .github/ — CI/CD workflows、Issue/PR 模板、分支保护
6576
# ✅ .coderabbit.yaml — CodeRabbit 代码审查配置
6677
# ✅ SECURITY.md — 安全政策

client/src/components/admin-gate.tsx

Lines changed: 129 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState, useEffect, useRef, useCallback } from "react";
22
import { useLocation } from "wouter";
33
import { checkAuth, login } from "@/lib/api";
4+
import { ArrowRight, KeyRound, ShieldCheck, X } from "lucide-react";
45

56
/**
67
* 管理后台暗门组件
@@ -21,6 +22,7 @@ export function AdminGate({
2122
const [checking, setChecking] = useState(true);
2223
const [, setLocation] = useLocation();
2324
const inputRef = useRef<HTMLInputElement>(null);
25+
const modalRef = useRef<HTMLDivElement>(null);
2426

2527
// 打开时立即检查是否已登录
2628
useEffect(() => {
@@ -64,11 +66,35 @@ export function AdminGate({
6466
[password, onClose, setLocation]
6567
);
6668

67-
// ESC 关闭
69+
// ESC 关闭,并将键盘焦点限制在弹窗内
6870
useEffect(() => {
6971
if (!open) return;
7072
const handler = (e: KeyboardEvent) => {
71-
if (e.key === "Escape") onClose();
73+
if (e.key === "Escape") {
74+
onClose();
75+
return;
76+
}
77+
78+
if (e.key !== "Tab") return;
79+
80+
const focusableElements = Array.from(
81+
modalRef.current?.querySelectorAll<HTMLElement>(
82+
'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
83+
) || []
84+
).filter((element) => !element.hasAttribute("disabled") && element.offsetParent !== null);
85+
86+
if (focusableElements.length === 0) return;
87+
88+
const first = focusableElements[0];
89+
const last = focusableElements[focusableElements.length - 1];
90+
91+
if (e.shiftKey && document.activeElement === first) {
92+
e.preventDefault();
93+
last.focus();
94+
} else if (!e.shiftKey && document.activeElement === last) {
95+
e.preventDefault();
96+
first.focus();
97+
}
7298
};
7399
window.addEventListener("keydown", handler);
74100
return () => window.removeEventListener("keydown", handler);
@@ -80,75 +106,115 @@ export function AdminGate({
80106
<>
81107
{/* 背景遮罩 */}
82108
<div
83-
className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm animate-in fade-in duration-200"
109+
className="fixed inset-0 z-50 bg-background/55 backdrop-blur-[10px] animate-in fade-in duration-200"
84110
onClick={onClose}
85111
/>
86112

87113
{/* 密码框 */}
88-
<div className="fixed z-50 left-1/2 top-[30%] -translate-x-1/2 w-[320px] animate-in fade-in slide-in-from-top-2 duration-200">
89-
<div className="rounded-xl border border-border/40 bg-card/95 backdrop-blur-xl shadow-2xl p-[24px]">
90-
{checking ? (
91-
<div className="py-[16px] text-center text-[13px] text-muted-foreground/60">
92-
验证中...
114+
<div
115+
ref={modalRef}
116+
role="dialog"
117+
aria-modal="true"
118+
aria-labelledby="admin-gate-title"
119+
className="fixed left-1/2 top-1/2 z-50 w-[min(92vw,420px)] -translate-x-1/2 -translate-y-1/2 animate-in fade-in slide-in-from-top-2 duration-200"
120+
>
121+
<div className="overflow-hidden rounded-md border border-border/35 bg-card/95 shadow-[0_28px_90px_oklch(0_0_0_/_38%)] backdrop-blur-xl">
122+
<div className="flex items-center justify-between border-b border-border/20 px-[18px] py-[14px]">
123+
<div className="flex items-center gap-[10px]">
124+
<div className="flex h-[32px] w-[32px] items-center justify-center rounded-md border border-cyan-400/20 bg-cyan-400/10 text-cyan-300">
125+
<ShieldCheck className="h-[16px] w-[16px]" />
126+
</div>
127+
<div>
128+
<p id="admin-gate-title" className="text-[13px] font-semibold text-foreground">后台安全验证</p>
129+
<p className="text-[11px] text-muted-foreground/55">Monolith Admin Gate</p>
130+
</div>
93131
</div>
94-
) : (
95-
<form onSubmit={handleSubmit} aria-label="管理员登录">
96-
<div className="mb-[16px] text-center">
97-
<div className="mx-auto mb-[10px] flex h-[36px] w-[36px] items-center justify-center rounded-lg bg-gradient-to-b from-foreground/8 to-foreground/4 border border-border/20">
98-
<span className="text-[16px]">🔐</span>
99-
</div>
100-
<p className="text-[12px] text-muted-foreground/50">
101-
管理员验证
102-
</p>
132+
<button
133+
type="button"
134+
onClick={onClose}
135+
className="flex h-[36px] w-[36px] items-center justify-center rounded-md text-muted-foreground/55 transition-colors hover:bg-accent/40 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
136+
aria-label="关闭管理验证"
137+
>
138+
<X className="h-[15px] w-[15px]" />
139+
</button>
140+
</div>
141+
142+
<div className="p-[22px]">
143+
{checking ? (
144+
<div className="flex min-h-[180px] flex-col items-center justify-center gap-[12px] text-center text-[13px] text-muted-foreground/65">
145+
<div className="h-[28px] w-[28px] rounded-full border-2 border-foreground/10 border-t-cyan-400 animate-spin" />
146+
正在检查登录状态
103147
</div>
148+
) : (
149+
<form onSubmit={handleSubmit} aria-label="管理员登录">
150+
<div className="mb-[18px]">
151+
<div className="mb-[12px] flex items-start gap-[12px]">
152+
<div className="mt-[2px] flex h-[44px] w-[44px] shrink-0 items-center justify-center rounded-md border border-border/30 bg-background/45 text-foreground">
153+
<KeyRound className="h-[18px] w-[18px]" />
154+
</div>
155+
<div>
156+
<h2 className="text-[18px] font-semibold leading-tight tracking-[-0.01em] text-foreground">进入管理后台</h2>
157+
<p className="mt-[6px] text-[13px] leading-[1.7] text-muted-foreground/70">
158+
输入本地或生产环境配置的管理密码,验证通过后进入内容控制台。
159+
</p>
160+
</div>
161+
</div>
162+
<div className="grid grid-cols-3 gap-[6px] text-[10px] text-muted-foreground/50">
163+
<span className="rounded-md border border-border/20 bg-background/25 px-[8px] py-[6px] text-center">内容</span>
164+
<span className="rounded-md border border-border/20 bg-background/25 px-[8px] py-[6px] text-center">媒体</span>
165+
<span className="rounded-md border border-border/20 bg-background/25 px-[8px] py-[6px] text-center">SEO</span>
166+
</div>
167+
</div>
168+
169+
{/* 隐藏 username 字段:让 Bitwarden / 1Password / Chrome 等密码管理器识别为登录表单 */}
170+
<input
171+
type="text"
172+
name="username"
173+
value="admin"
174+
autoComplete="username"
175+
readOnly
176+
hidden
177+
tabIndex={-1}
178+
aria-hidden="true"
179+
/>
180+
181+
<input
182+
ref={inputRef}
183+
id="admin-gate-password"
184+
name="password"
185+
type="password"
186+
value={password}
187+
onChange={(e) => {
188+
setPassword(e.target.value);
189+
setError("");
190+
}}
191+
placeholder="输入密码"
192+
autoComplete="current-password"
193+
aria-label="管理员密码"
194+
className="h-[46px] w-full rounded-md border border-border/45 bg-background/55 px-[14px] pr-[48px] text-[15px] text-foreground outline-none transition-all placeholder:text-muted-foreground/35 focus:border-cyan-400/45 focus:ring-1 focus:ring-cyan-400/20"
195+
/>
196+
197+
{error && (
198+
<p className="mt-[10px] rounded-md border border-red-400/20 bg-red-400/8 px-[10px] py-[8px] text-center text-[12px] text-red-400/90">
199+
{error}
200+
</p>
201+
)}
202+
203+
<button
204+
type="submit"
205+
disabled={loading || !password.trim()}
206+
className="mt-[14px] flex h-[46px] w-full items-center justify-center gap-[8px] rounded-md bg-foreground text-[14px] font-medium text-background transition-all hover:-translate-y-[2px] hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:translate-y-0 disabled:opacity-40"
207+
>
208+
{loading ? "验证中..." : "进入控制台"}
209+
{!loading && <ArrowRight className="h-[15px] w-[15px]" />}
210+
</button>
104211

105-
{/* 隐藏 username 字段:让 Bitwarden / 1Password / Chrome 等密码管理器识别为登录表单 */}
106-
<input
107-
type="text"
108-
name="username"
109-
value="admin"
110-
autoComplete="username"
111-
readOnly
112-
hidden
113-
tabIndex={-1}
114-
aria-hidden="true"
115-
/>
116-
117-
<input
118-
ref={inputRef}
119-
id="admin-gate-password"
120-
name="password"
121-
type="password"
122-
value={password}
123-
onChange={(e) => {
124-
setPassword(e.target.value);
125-
setError("");
126-
}}
127-
placeholder="输入密码"
128-
autoComplete="current-password"
129-
aria-label="管理员密码"
130-
className="h-[40px] w-full rounded-lg border border-border/40 bg-background/50 px-[14px] text-[14px] text-foreground placeholder:text-muted-foreground/30 outline-none focus:border-foreground/25 focus:ring-1 focus:ring-foreground/10 transition-all"
131-
/>
132-
133-
{error && (
134-
<p className="mt-[8px] text-[12px] text-red-400/80 text-center">
135-
{error}
212+
<p className="mt-[14px] text-center text-[11px] text-muted-foreground/35">
213+
ESC 关闭 · Ctrl Shift A 呼出
136214
</p>
137-
)}
138-
139-
<button
140-
type="submit"
141-
disabled={loading || !password.trim()}
142-
className="mt-[12px] h-[36px] w-full rounded-lg bg-foreground text-background text-[13px] font-medium hover:opacity-90 disabled:opacity-40 transition-opacity"
143-
>
144-
{loading ? "验证中..." : "进入"}
145-
</button>
146-
147-
<p className="mt-[12px] text-center text-[11px] text-muted-foreground/25">
148-
按 ESC 关闭
149-
</p>
150-
</form>
151-
)}
215+
</form>
216+
)}
217+
</div>
152218
</div>
153219
</div>
154220
</>

0 commit comments

Comments
 (0)