From 2ed9eae390b67e3e0ebfbba09106961c47d8d926 Mon Sep 17 00:00:00 2001 From: ddcat666 <26564566+ddcat666@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:44:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=8A=80=E8=83=BD=E5=88=86?= =?UTF-8?q?=E7=B1=BB=20-=20=E9=87=8D=E6=9E=84=E5=88=86=E7=B1=BB=E7=BB=B4?= =?UTF-8?q?=E6=8A=A4=E4=B8=8E=E6=8A=80=E8=83=BD=E5=BD=92=E7=B1=BB=E4=BA=A4?= =?UTF-8?q?=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/app/appearance.go | 2 +- .../settings/appearance-settings-page.tsx | 6 +- web/src/pages/admin/skill-curation-page.css | 54 +++ web/src/pages/admin/skill-curation-page.tsx | 409 +++++++++++++++--- 4 files changed, 415 insertions(+), 56 deletions(-) create mode 100644 web/src/pages/admin/skill-curation-page.css diff --git a/backend/internal/app/appearance.go b/backend/internal/app/appearance.go index 03f0415f4..2c30167dd 100644 --- a/backend/internal/app/appearance.go +++ b/backend/internal/app/appearance.go @@ -48,7 +48,7 @@ const ( defaultAppearanceVideoURL = "https://boss-shjd.biliapi.net/updream/aniforge/video/video_bbcb00bd-650d-4249-9346-5cd21fd2484c_m1hc-u0-1pu13x-3v1s.mp4" defaultAppearancePosterURL = "https://i0.hdslb.com/bfs/aitool/aniforge/image/02933f26-5f1b-49ff-a811-b7f95ee5e5b8_m1hc-u0-sau.jpg" defaultAppearanceHeroTitle = "让一个故事,\n从文字走向银幕。" - defaultRedeemPurchaseURL = "https://wzyp.cn/shop/JPDK4SRD" + defaultRedeemPurchaseURL = "暂未配置" ) type AppearanceSetting struct { diff --git a/web/src/pages/admin/settings/appearance-settings-page.tsx b/web/src/pages/admin/settings/appearance-settings-page.tsx index a81bbc615..e8a34987e 100644 --- a/web/src/pages/admin/settings/appearance-settings-page.tsx +++ b/web/src/pages/admin/settings/appearance-settings-page.tsx @@ -530,6 +530,9 @@ export default function AppearanceSettingsPage() { onChange={(event) => setBrandSlug(event.target.value.toLocaleLowerCase().replace(/[^a-z0-9-]/g, ""))} /> + + setRedeemPurchaseUrl(event.target.value)} /> +
setIcpFilingNumber(event.target.value)} /> - - setRedeemPurchaseUrl(event.target.value)} /> -
diff --git a/web/src/pages/admin/skill-curation-page.css b/web/src/pages/admin/skill-curation-page.css new file mode 100644 index 000000000..a768c1038 --- /dev/null +++ b/web/src/pages/admin/skill-curation-page.css @@ -0,0 +1,54 @@ +.admin-skill-curation-section-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + border-bottom: 1px solid var(--admin-divider); + padding-bottom: 10px; +} + +.admin-skill-curation-section-heading h3 { + margin: 0; + color: var(--admin-text-primary); + font-size: 13px; + font-weight: 650; + line-height: 20px; +} + +.admin-skill-curation-section-heading p { + margin: 2px 0 0; + color: var(--admin-text-muted); + font-size: 11px; + line-height: 17px; +} + +.admin-skill-curation-fields-root { + grid-template-columns: minmax(180px, 1.4fr) minmax(180px, 1fr) 96px 110px; +} + +.admin-skill-curation-icon-option { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.admin-skill-curation-subsection { + display: flex; + flex-direction: column; + gap: 14px; + margin-top: 8px; + border-top: 1px solid var(--admin-divider); + padding-top: 18px; +} + +@media (max-width: 1199px) { + .admin-skill-curation-fields-root { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 639px) { + .admin-skill-curation-fields-root { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/web/src/pages/admin/skill-curation-page.tsx b/web/src/pages/admin/skill-curation-page.tsx index 2a8c6f061..714580f9e 100644 --- a/web/src/pages/admin/skill-curation-page.tsx +++ b/web/src/pages/admin/skill-curation-page.tsx @@ -1,75 +1,380 @@ import { useEffect, useState } from "react"; -import { Alert, App, Button, Input, InputNumber, Select, Space } from "antd"; +import { Alert, App, Button, Input, InputNumber, Select } from "antd"; +import { Check, Edit3, Plus, RefreshCw, Shapes, SlidersHorizontal } from "lucide-react"; import { AdminPageFrame } from "./components/admin-shell"; -import { AdminDataTable } from "./components/admin-ui"; +import { AdminDataTable, AdminStatusBadge, AdminTableEmpty, SettingsSectionCard } from "./components/admin-ui"; import { AdminSwitch } from "./ui/controls"; -import { getSkillCuration, updateSkillCuration, type CurationCategory, type CurationChange, type SkillCuration, type CurationRoot } from "@/services/api/skill-curation"; +import { getSkillCuration, updateSkillCuration, type CurationCategory, type CurationChange, type CurationRoot, type SkillCuration } from "@/services/api/skill-curation"; import { curationIcon, curationIconKeys, curationIconLabels } from "@/components/skills/skill-curation-browser"; import { listSkills, type Skill } from "@/services/api/skills"; +import "./skill-curation-page.css"; + +const emptyRoot = (): CurationRoot => ({ id: "", name: "", iconKey: "shapes", sortOrder: 0, enabled: true }); +const emptyCategory = (rootTag = "drama"): CurationCategory => ({ id: "", rootTag, name: "", sortOrder: 0, enabled: true }); export default function SkillCurationPage() { const { message } = App.useApp(); const [data, setData] = useState(null); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); - const [category, setCategory] = useState({ id: "", rootTag: "drama", name: "", sortOrder: 0, enabled: true }); + const [root, setRoot] = useState(emptyRoot); + const [category, setCategory] = useState(emptyCategory); const [search, setSearch] = useState(""); const [skills, setSkills] = useState([]); const [skillId, setSkillId] = useState(""); - const [categoryIds, setCategoryIds] = useState([]); - const [root, setRoot] = useState({ id: "", name: "", iconKey: "shapes", sortOrder: 0, enabled: true }); const [assignedRoot, setAssignedRoot] = useState(""); - const reload = async () => { try { setData(await getSkillCuration(true)); setError(""); } catch (e) { setError(String(e)); } }; - useEffect(() => { void reload(); }, []); + const [categoryIds, setCategoryIds] = useState([]); + + const selectedSkill = skills.find((skill) => skill.skillId === skillId); + const selectedRoot = data?.roots?.find((item) => item.id === assignedRoot); + const availableCategories = data?.categories.filter((item) => item.enabled && item.rootTag === (assignedRoot || selectedSkill?.tag)) ?? []; + + const reload = async () => { + try { + setData(await getSkillCuration(true)); + setError(""); + } catch (e) { + setError(String(e)); + } + }; + + useEffect(() => { + void reload(); + }, []); + useEffect(() => { let cancelled = false; - const timer = setTimeout(() => { listSkills({ scope: "public", search, pageSize: 50 }).then((result) => { if (!cancelled) setSkills(result.skills); }).catch((e) => { if (!cancelled) setError(String(e)); }); }, 250); - return () => { cancelled = true; clearTimeout(timer); }; + const timer = setTimeout(() => { + listSkills({ scope: "public", search, pageSize: 50 }) + .then((result) => { + if (!cancelled) setSkills(result.skills); + }) + .catch((e) => { + if (!cancelled) setError(String(e)); + }); + }, 250); + return () => { + cancelled = true; + clearTimeout(timer); + }; }, [search]); + const save = async (change: CurationChange) => { if (!data) return; setBusy(true); - try { setData(await updateSkillCuration(data.revision, change)); setError(""); message.success("已保存"); window.dispatchEvent(new Event("canvas-skills-changed")); } - catch (e) { setError(`${String(e)};请重新加载后核对修改`); } - finally { setBusy(false); } + try { + setData(await updateSkillCuration(data.revision, change)); + setError(""); + message.success("已保存"); + window.dispatchEvent(new Event("canvas-skills-changed")); + } catch (e) { + setError(`${String(e)};请重新加载后核对修改`); + } finally { + setBusy(false); + } }; - const selectedSkill = skills.find((skill) => skill.skillId === skillId); - return void reload()}>重新加载}> - - {error && } - void save({ enabled })} />平台策展模式 - table={{ rowKey: "id", dataSource: data?.roots || [], pagination: false, loading: !data, columns: [ - { title: "一级分类", dataIndex: "name" }, { title: "图示", render: (_, item) => { const Icon = curationIcon(item.iconKey); return ; } }, - { title: "排序", dataIndex: "sortOrder" }, { title: "状态", render: (_, item) => item.enabled ? "启用" : "停用" }, - { title: "操作", render: (_, item) => }, - ] }} /> - - setRoot({ ...root, name: e.target.value })} /> - ({ value: item.id, label: item.name, disabled: !item.enabled }))} onChange={(rootTag) => setCategory({ ...category, rootTag })} /> - setCategory({ ...category, name: e.target.value })} /> - setCategory({ ...category, sortOrder: sortOrder ?? 0 })} /> - setCategory({ ...category, enabled })} /> - - - - - setSearch(e.target.value)} /> - ({ value: item.id, label: item.name, disabled: !item.enabled }))]} onChange={(id) => { setAssignedRoot(id); setCategoryIds([]); }} /> - setRoot({ ...root, name: e.target.value })} /> + + + + +
+ 状态 +
+ setCategory({ ...category, enabled })} /> + {category.enabled ? "启用" : "停用"} +
+
+
+
+ {category.id ? `正在编辑「${category.name}」` : "新增后即可在技能归类中使用"} +
+ + +
+
+
+ +
+ + className="admin-skill-curation-table" + table={{ + rowKey: "id", + dataSource: data?.categories || [], + pagination: false, + loading: !data, + columns: [ + { title: "一级分类", render: (_, row) => data?.roots?.find((item) => item.id === row.rootTag)?.name || "未归入启用分类" }, + { title: "子分类", dataIndex: "name" }, + { title: "排序", dataIndex: "sortOrder", width: 78, align: "center" }, + { title: "状态", width: 90, render: (_, row) => }, + { + title: "操作", + width: 90, + render: (_, row) => ( + + ), + }, + ], + }} + empty={ + } onClick={() => document.querySelector("[aria-label=分类名称]")?.focus()}> + 创建子分类 + + } + /> + } + /> +
+ + +