From b4a0e23c7edccf80385780cca69b647511847183 Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Sun, 19 Apr 2026 19:18:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=90=8E=E5=8F=B0=E4=B8=BA=E7=8E=B0=E4=BB=A3=E5=8C=96?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E9=A3=8E=20UI=20=E5=B9=B6=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E6=A8=A1=E6=80=81=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 参考收集端落地页的奶油纸质感 + 深海蓝侧栏 + Playfair Display + 金/陶/玉配色,重塑整体视觉 - 编辑、二维码从跳转路由改为模态弹窗,新增"复制链接"快捷操作 - 抽取 Modal / Toast / QRCodeModal / PageHeader / FormPrimitives 通用基建 - 合并三份 QRCode 页面为统一组件,精简路由配置 Co-Authored-By: Claude Opus 4.6 --- packages/web/src/App.tsx | 18 -- packages/web/src/admin/AdminLayout.tsx | 170 +++++++++++---- packages/web/src/admin/AdminLogin.tsx | 131 ++++++++++-- packages/web/src/admin/ArticleForm.tsx | 222 ++++++++++---------- packages/web/src/admin/ArticleList.tsx | 219 ++++++++++++------- packages/web/src/admin/ArticleQRCode.tsx | 120 ----------- packages/web/src/admin/FormPrimitives.tsx | 94 +++++++++ packages/web/src/admin/Modal.tsx | 100 +++++++++ packages/web/src/admin/MusicForm.tsx | 190 ++++++++--------- packages/web/src/admin/MusicList.tsx | 229 +++++++++++++------- packages/web/src/admin/MusicQRCode.tsx | 120 ----------- packages/web/src/admin/PageHeader.tsx | 161 ++++++++++++++ packages/web/src/admin/QRCodeModal.tsx | 143 +++++++++++++ packages/web/src/admin/RedemptionLog.tsx | 176 +++++++++++----- packages/web/src/admin/RuleForm.tsx | 134 ++++++------ packages/web/src/admin/RuleList.tsx | 172 +++++++++------ packages/web/src/admin/StampForm.tsx | 225 ++++++++++++-------- packages/web/src/admin/StampList.tsx | 244 +++++++++++++++------- packages/web/src/admin/StampQRCode.tsx | 128 ------------ packages/web/src/admin/Toast.tsx | 51 +++++ packages/web/src/admin/utils.ts | 36 ++++ packages/web/src/index.css | 12 ++ 22 files changed, 1948 insertions(+), 1147 deletions(-) delete mode 100644 packages/web/src/admin/ArticleQRCode.tsx create mode 100644 packages/web/src/admin/FormPrimitives.tsx create mode 100644 packages/web/src/admin/Modal.tsx delete mode 100644 packages/web/src/admin/MusicQRCode.tsx create mode 100644 packages/web/src/admin/PageHeader.tsx create mode 100644 packages/web/src/admin/QRCodeModal.tsx delete mode 100644 packages/web/src/admin/StampQRCode.tsx create mode 100644 packages/web/src/admin/Toast.tsx create mode 100644 packages/web/src/admin/utils.ts diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index 20e79aa..65bd7a0 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -8,16 +8,9 @@ import AdminLogin from "./admin/AdminLogin"; import AdminGuard from "./admin/AdminGuard"; import AdminLayout from "./admin/AdminLayout"; import StampList from "./admin/StampList"; -import StampForm from "./admin/StampForm"; -import StampQRCode from "./admin/StampQRCode"; import ArticleList from "./admin/ArticleList"; -import ArticleForm from "./admin/ArticleForm"; -import ArticleQRCode from "./admin/ArticleQRCode"; import MusicList from "./admin/MusicList"; -import MusicForm from "./admin/MusicForm"; -import MusicQRCode from "./admin/MusicQRCode"; import RuleList from "./admin/RuleList"; -import RuleForm from "./admin/RuleForm"; import RedemptionLog from "./admin/RedemptionLog"; function CollectRedirect() { @@ -41,20 +34,9 @@ export default function App() { }> }> } /> - } /> - } /> - } /> } /> - } /> - } /> - } /> } /> - } /> - } /> - } /> } /> - } /> - } /> } /> diff --git a/packages/web/src/admin/AdminLayout.tsx b/packages/web/src/admin/AdminLayout.tsx index c7f62b0..91a66ab 100644 --- a/packages/web/src/admin/AdminLayout.tsx +++ b/packages/web/src/admin/AdminLayout.tsx @@ -1,11 +1,12 @@ import { NavLink, Outlet, useNavigate } from "react-router-dom"; +import { ToastProvider } from "./Toast"; const navItems = [ - { path: "/admin/stamps", label: "图章管理" }, - { path: "/admin/articles", label: "文章管理" }, - { path: "/admin/music", label: "音乐管理" }, - { path: "/admin/rules", label: "兑换规则" }, - { path: "/admin/redemptions", label: "兑换记录" }, + { path: "/admin/stamps", label: "图章管理", eyebrow: "01", tag: "Stamps" }, + { path: "/admin/articles", label: "文章管理", eyebrow: "02", tag: "Articles" }, + { path: "/admin/music", label: "音乐管理", eyebrow: "03", tag: "Music" }, + { path: "/admin/rules", label: "兑换规则", eyebrow: "04", tag: "Rules" }, + { path: "/admin/redemptions", label: "兑换记录", eyebrow: "05", tag: "Log" }, ]; export default function AdminLayout() { @@ -17,40 +18,131 @@ export default function AdminLayout() { }; return ( -
- {/* Sidebar */} - + +
+ {/* ═══════════ Sidebar ═══════════ */} + + + {/* ═══════════ Main ═══════════ */} +
+
+ +
+
+
+
); } diff --git a/packages/web/src/admin/AdminLogin.tsx b/packages/web/src/admin/AdminLogin.tsx index 1c0cb96..73a79ff 100644 --- a/packages/web/src/admin/AdminLogin.tsx +++ b/packages/web/src/admin/AdminLogin.tsx @@ -27,28 +27,117 @@ export default function AdminLogin() { }; return ( -
-
-

管理后台

-
- setKey(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleLogin()} - placeholder="输入管理密钥" - className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm - focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" - /> - {error &&

{error}

} - + 图章后台 + +

+ Stamp · Admin Console +

+
+ + {/* Card */} +
+ {/* Corner flourishes */} + + + + + +
+
+ + + Access + +
+ + + setKey(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && key && handleLogin()} + placeholder="••••••••" + className="w-full px-4 py-3 bg-white border border-[var(--border-default)] rounded-lg text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)]/50 focus:outline-none focus:border-[var(--gold)] focus:ring-2 focus:ring-[var(--gold)]/15 transition-all" + autoFocus + /> + + {error && ( +
+ + + + + {error} +
+ )} + + + +
+

+ 仅限授权访问 +

+
+
diff --git a/packages/web/src/admin/ArticleForm.tsx b/packages/web/src/admin/ArticleForm.tsx index 434363b..4d69a43 100644 --- a/packages/web/src/admin/ArticleForm.tsx +++ b/packages/web/src/admin/ArticleForm.tsx @@ -1,6 +1,8 @@ import { useState, useEffect } from "react"; -import { useNavigate, useParams } from "react-router-dom"; +import Modal from "./Modal"; import { adminFetch } from "./adminApi"; +import { useToast } from "./Toast"; +import { Field, ErrorRow, FormFooter, HintRow, fieldCls } from "./FormPrimitives"; type Article = { id: string; @@ -13,11 +15,16 @@ type Article = { enabled: boolean; }; -export default function ArticleForm() { - const { id } = useParams(); - const navigate = useNavigate(); - const isEdit = !!id; +type Props = { + open: boolean; + id: string | null; + onClose: () => void; + onSaved: () => void; +}; +export default function ArticleForm({ open, id, onClose, onSaved }: Props) { + const toast = useToast(); + const [currentId, setCurrentId] = useState(null); const [title, setTitle] = useState(""); const [subtitle, setSubtitle] = useState(""); const [body, setBody] = useState(""); @@ -28,8 +35,17 @@ export default function ArticleForm() { const [saving, setSaving] = useState(false); const [error, setError] = useState(""); + const isEdit = !!currentId; + useEffect(() => { - if (!id) return; + if (!open) return; + setCurrentId(id); + setError(""); + if (!id) { + setTitle(""); setSubtitle(""); setBody(""); setCaption(""); + setCoverImage(""); setSortOrder(0); setEnabled(true); + return; + } adminFetch("/articles").then((articles) => { const article = articles.find((a) => a.id === id); if (article) { @@ -42,32 +58,33 @@ export default function ArticleForm() { setEnabled(article.enabled); } }); - }, [id]); + }, [open, id]); const handleUpload = async (file: File) => { - if (!id) { + if (!currentId) { setError("请先保存文章后再上传封面"); return; } + setError(""); const formData = new FormData(); formData.append("image", file); - const data = await adminFetch<{ path: string }>(`/articles/${id}/upload`, { - method: "POST", - body: formData, - }); - setCoverImage(data.path); + try { + const data = await adminFetch<{ path: string }>(`/articles/${currentId}/upload`, { + method: "POST", + body: formData, + }); + setCoverImage(data.path); + toast.show("封面已上传"); + onSaved(); + } catch (e) { + setError(e instanceof Error ? e.message : "上传失败"); + } }; const handleSave = async () => { setError(""); - if (!title.trim()) { - setError("请输入标题"); - return; - } - if (!body.trim()) { - setError("请输入正文"); - return; - } + if (!title.trim()) return setError("请输入标题"); + if (!body.trim()) return setError("请输入正文"); setSaving(true); try { const payload = { @@ -79,19 +96,22 @@ export default function ArticleForm() { enabled, }; if (isEdit) { - await adminFetch(`/articles/${id}`, { + await adminFetch(`/articles/${currentId}`, { method: "PUT", body: JSON.stringify(payload), }); + toast.show("已保存"); + onSaved(); + onClose(); } else { const article = await adminFetch
("/articles", { method: "POST", body: JSON.stringify(payload), }); - navigate(`/admin/articles/${article.id}/edit`, { replace: true }); - return; + setCurrentId(article.id); + toast.show("已创建,现在可以上传封面"); + onSaved(); } - navigate("/admin/articles"); } catch (e) { setError(e instanceof Error ? e.message : "保存失败"); } finally { @@ -100,122 +120,104 @@ export default function ArticleForm() { }; return ( -
-

- {isEdit ? "编辑文章" : "添加文章"} -

- -
-
- + +
+ setTitle(e.target.value)} placeholder="如:朝天宫" - className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm - focus:outline-none focus:ring-1 focus:ring-blue-500" + className={fieldCls} /> -
+ -
- + setSubtitle(e.target.value)} placeholder="如:千年冶山,文脉绵延" - className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm - focus:outline-none focus:ring-1 focus:ring-blue-500" + className={fieldCls} /> -
+ -
- +