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} /> -
+ -
- +