"use client"; import { ArrowDown, ArrowUp } from "lucide-react"; import { useEffect, useState } from "react"; import { AdminSettingsActions, AdminSettingsGroup, AdminSwitch } from "@/components/admin"; import { apiUpdateSiteSettings, type PublicSettings } from "@/lib/api"; import { MOBILE_TABS, normalizeMobileTabs, type MobileTabKey } from "@/lib/mobileTabs"; import { toast } from "@/lib/toast"; /** 底栏入口说明 */ const TAB_META: Record = { home: { label: "首页", hint: "站点首页" }, library: { label: "书库", hint: "书库列表页入口" }, compose: { label: "发帖", hint: "发帖工作台入口(中央主按钮,无对应首页)" }, messages: { label: "消息", hint: "私聊/群聊列表;游客不显示" }, me: { label: "我的", hint: "个人主页;未登录指向登录页" }, }; type TabSwitches = Record; function switchesOf(tabs: MobileTabKey[]): TabSwitches { return { home: tabs.includes("home"), library: tabs.includes("library"), compose: tabs.includes("compose"), messages: tabs.includes("messages"), me: tabs.includes("me"), }; } /** 完整顺序:已启用项按配置顺序在前,未启用项按默认顺序追加在后 */ function orderOf(tabs: MobileTabKey[]): MobileTabKey[] { const enabled = normalizeMobileTabs(tabs); const rest = MOBILE_TABS.filter((key) => !enabled.includes(key)); return [...enabled, ...rest]; } /** 移动端底栏:开关 + 顺序调整,保存启用的 tab 子集(数组顺序即前台展示顺序) */ export default function MobileTabBarPanel({ initial, onSaved, onDirtyChange, }: { initial: PublicSettings; onSaved?: (next: PublicSettings) => void; onDirtyChange?: (dirty: boolean) => void; }) { const [order, setOrder] = useState(() => orderOf(initial.mobile_tabs)); const [on, setOn] = useState(() => switchesOf(normalizeMobileTabs(initial.mobile_tabs))); const [savedPayload, setSavedPayload] = useState(() => normalizeMobileTabs(initial.mobile_tabs)); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const payload = order.filter((key) => on[key]); const dirty = JSON.stringify(payload) !== JSON.stringify(savedPayload); useEffect(() => { onDirtyChange?.(dirty); }, [dirty, onDirtyChange]); const move = (i: number, dir: -1 | 1) => { setError(""); setOrder((prev) => { const j = i + dir; if (j < 0 || j >= prev.length) return prev; const next = [...prev]; const [row] = next.splice(i, 1); next.splice(j, 0, row); return next; }); }; const discard = () => { setOrder(orderOf(savedPayload)); setOn(switchesOf(savedPayload)); setError(""); }; const save = async () => { if (saving || !dirty) return; if (payload.length === 0) { setError("至少保留一个入口"); toast("至少保留一个入口"); return; } setSaving(true); setError(""); try { const res = await apiUpdateSiteSettings({ mobile_tabs: [...payload] }); const nextSaved = normalizeMobileTabs(res.mobile_tabs); setSavedPayload(nextSaved); setOrder(orderOf(nextSaved)); setOn(switchesOf(nextSaved)); onSaved?.(res); toast("已保存,移动端底栏已更新", "ok"); } catch (e) { const msg = e instanceof Error ? e.message : "保存失败"; setError(msg); toast(msg); } finally { setSaving(false); } }; return ( void save()} onDiscard={discard} /> } >
{order.map((key, i) => (
{TAB_META[key].label}
{TAB_META[key].hint}
{ setError(""); setOn((prev) => ({ ...prev, [key]: !prev[key] })); }} label={`底栏显示${TAB_META[key].label}`} />
))}
  • 顺序即前台底栏展示顺序;未启用的入口保留排序位置,重新启用后按当前顺序插入。
  • 至少保留一个入口;全部关闭将无法保存。
  • 排行榜不在底栏展示,移动端入口由首页抽屉承担。
); }