diff --git a/frontend-web/.env.network b/frontend-web/.env.network index ae615eb8..d81d70e5 100644 --- a/frontend-web/.env.network +++ b/frontend-web/.env.network @@ -14,8 +14,8 @@ VITE_MOCK_SQ_REPORT=false VITE_MOCK_COP_TASK_DETAIL=false # Gateway / DeerFlow API -VITE_BACKEND_BASE_URL=http://47.88.25.99:7001 -VITE_LANGGRAPH_BASE_URL=http://47.88.25.99:7001/api +VITE_BACKEND_BASE_URL=http://10.19.200.5:2026 +VITE_LANGGRAPH_BASE_URL=http://10.19.200.5:2026/api # 轻应用「新建笔记」系列接口 VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000 diff --git a/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx b/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx index 65b504f0..d0bf5763 100644 --- a/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx +++ b/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx @@ -260,37 +260,68 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string }) key={agent.id} href={href} className={cn( - "group relative flex w-full items-start gap-2.5 rounded-xl border px-2.5 py-2 transition-colors", + "group relative flex w-full items-start gap-3 rounded-xl border px-3 py-2.5 transition-all duration-150", isActive - ? "border-primary/40 bg-primary/5" - : "border-transparent hover:border-border hover:bg-accent/50", + ? isDark + ? "border-[#2b8ffe] bg-[#0b4fae]/65 shadow-[0_0_0_1px_rgba(43,143,254,0.5),0_12px_28px_rgba(37,128,255,0.25)]" + : "border-primary/50 bg-primary/10 shadow-sm" + : isDark + ? "border-[#2b8ffe]/20 bg-[#042b59]/35 hover:border-[#2b8ffe]/60 hover:bg-[#063b77]/60" + : "border-transparent hover:border-border hover:bg-accent/50", )} > - + {desensitize(agent.name)} - + {agent.description ? desensitize(agent.description) : "智能体助手"} {isActive && ( - + )} ); diff --git a/frontend-web/src/components/workspace/agents/agent-module-sidebar.tsx b/frontend-web/src/components/workspace/agents/agent-module-sidebar.tsx index b9e48c2a..4cf1da8e 100644 --- a/frontend-web/src/components/workspace/agents/agent-module-sidebar.tsx +++ b/frontend-web/src/components/workspace/agents/agent-module-sidebar.tsx @@ -219,33 +219,64 @@ export function AgentModuleSidebar({ } }} className={cn( - "group relative flex w-full items-start gap-2.5 rounded-xl border px-2.5 py-2 transition-colors", + "group relative flex w-full items-start gap-3 rounded-xl border px-3 py-2.5 transition-all duration-150", isActive - ? "border-primary/40 bg-primary/5" - : "border-transparent hover:border-border hover:bg-accent/50", + ? isDark + ? "border-[#2b8ffe] bg-[#0b4fae]/65 shadow-[0_0_0_1px_rgba(43,143,254,0.5),0_12px_28px_rgba(37,128,255,0.25)]" + : "border-primary/50 bg-primary/10 shadow-sm" + : isDark + ? "border-[#2b8ffe]/20 bg-[#042b59]/35 hover:border-[#2b8ffe]/60 hover:bg-[#063b77]/60" + : "border-transparent hover:border-border hover:bg-accent/50", )} > - + {desensitize(agent.name)} - + {agent.description ? desensitize(agent.description) : "智能体助手"} {isActive && ( - + )} ); diff --git a/frontend-web/src/components/workspace/appearance-controls.tsx b/frontend-web/src/components/workspace/appearance-controls.tsx index 40c50efb..c11534b0 100644 --- a/frontend-web/src/components/workspace/appearance-controls.tsx +++ b/frontend-web/src/components/workspace/appearance-controls.tsx @@ -1,6 +1,6 @@ "use client"; -import { Check, LayoutIcon, PanelLeftIcon, Shirt } from "lucide-react"; +import { Check, ExternalLink, LayoutIcon, PanelLeftIcon, Shirt } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -14,8 +14,9 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useTheme, type ThemeType } from "@/contexts/ThemeContext"; -import { useLayoutMode } from "@/core/system-settings/hooks"; +import { useAppearanceSettings, useLayoutMode } from "@/core/system-settings/hooks"; import { cn } from "@/lib/utils"; +import { buildLightAppUrl } from "@/strategy-components/lib/light-app"; /** * 换肤 / 界面模式 顶栏控件。深色顶栏(Header)与简洁模式页面级顶栏(ChatPage 等)共用, @@ -39,6 +40,43 @@ const triggerToneClass = (tone: AppearanceControlTone) => ? "text-white hover:bg-[var(--brand-950)] hover:text-white active:bg-[#8a0618]" : "text-muted-foreground hover:bg-muted hover:text-foreground"; +export function CompactShortcutButtons({ tone }: { tone: AppearanceControlTone }) { + const { data: appearance } = useAppearanceSettings(); + const buttons = (appearance?.compact_shortcut_buttons ?? []) + .filter((button) => button.enabled && button.label.trim() && button.url.trim()) + .sort((a, b) => a.sort_order - b.sort_order); + + if (buttons.length === 0) return null; + + return ( + <> + {buttons.map((button) => ( + { + const url = buildLightAppUrl({ + url: button.url, + routeParams: button.route_params.map((param) => ({ ...param })), + }); + window.open(url, "_blank", "noopener,noreferrer"); + }} + > + + {button.label} + + ))} + > + ); +} + export function ThemeSwitcher({ tone }: { tone: AppearanceControlTone }) { const { theme, setTheme } = useTheme(); const [open, setOpen] = useState(false); diff --git a/frontend-web/src/components/workspace/general-qa-sidebar.tsx b/frontend-web/src/components/workspace/general-qa-sidebar.tsx index b06e4123..4937b8b1 100644 --- a/frontend-web/src/components/workspace/general-qa-sidebar.tsx +++ b/frontend-web/src/components/workspace/general-qa-sidebar.tsx @@ -349,33 +349,64 @@ export function GeneralQaSidebar() { type="button" onClick={() => handleSelectAgent(agent.id)} className={cn( - "group relative flex w-full items-start gap-2.5 rounded-xl border px-2.5 py-2 text-left transition-colors", + "group relative flex w-full items-start gap-3 rounded-xl border px-3 py-2.5 text-left transition-all duration-150", isActive - ? "border-primary/40 bg-primary/5" - : "border-transparent hover:border-border hover:bg-accent/50", + ? isDark + ? "border-[#2b8ffe] bg-[#0b4fae]/65 shadow-[0_0_0_1px_rgba(43,143,254,0.5),0_12px_28px_rgba(37,128,255,0.25)]" + : "border-primary/50 bg-primary/10 shadow-sm" + : isDark + ? "border-[#2b8ffe]/20 bg-[#042b59]/35 hover:border-[#2b8ffe]/60 hover:bg-[#063b77]/60" + : "border-transparent hover:border-border hover:bg-accent/50", )} > - + {desensitize(agent.name)} - + {agent.description ? desensitize(agent.description) : "智能体助手"} {isActive && ( - + )} ); diff --git a/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx b/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx index d94f8db1..162d710b 100644 --- a/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx @@ -1,16 +1,30 @@ "use client"; -import { DropletIcon, LayoutGridIcon, LayoutIcon, ListIcon, MoonIcon, PanelLeftIcon, SunIcon } from "lucide-react"; -import { useMemo, useState, type ComponentType, type SVGProps } from "react"; +import { + ArrowDown, + ArrowUp, + DropletIcon, + ExternalLink, + LayoutGridIcon, + LayoutIcon, + ListIcon, + MoonIcon, + PanelLeftIcon, + Plus, + SunIcon, + Trash2, +} from "lucide-react"; +import { useEffect, useMemo, useState, type ComponentType, type SVGProps } from "react"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; import { useTheme } from "@/contexts/ThemeContext"; -import { FONT_SCALE_OPTIONS } from "@/components/font-scale"; -import { Button } from "@/components/ui/button"; -import { - Select, +import { FONT_SCALE_OPTIONS } from "@/components/font-scale"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, SelectContent, SelectItem, SelectTrigger, @@ -25,14 +39,24 @@ import { isIsolatedSession, setIsolatedSession } from "@/core/embed/embed-sessio import { enUS, isLocale, zhCN, type Locale } from "@/core/i18n"; import { useI18n } from "@/core/i18n/hooks"; import { useLocalSettings } from "@/core/settings"; -import { - useAppearanceSettings, +import { + useAppearanceSettings, useCitationDisplaySettings, useLayoutMode, useUpdateAppearanceSettings, useUpdateCitationDisplaySettings, -} from "@/core/system-settings/hooks"; -import { cn } from "@/lib/utils"; +} from "@/core/system-settings/hooks"; +import type { + CompactShortcutButton, + CompactShortcutRouteParam, +} from "@/core/system-settings/api"; +import { cn } from "@/lib/utils"; +import { + APP_THEME_COLORS, + LOGIN_SOURCE_OPTIONS, + parseThemeMap, + stringifyThemeMap, +} from "@/strategy-components/lib/light-app"; import PrivateSquareSettingsPage from "@/pages/PrivateSquareSettingsPage"; import PublishSquaresSettingsPage from "@/pages/PublishSquaresSettingsPage"; @@ -41,10 +65,101 @@ import SkillCompressionSettingsPage from "@/pages/SkillCompressionSettingsPage"; import { SettingsSection } from "./settings-section"; -const languageOptions: { value: Locale; label: string }[] = [ - { value: "en-US", label: enUS.locale.localName }, - { value: "zh-CN", label: zhCN.locale.localName }, -]; +const languageOptions: { value: Locale; label: string }[] = [ + { value: "en-US", label: enUS.locale.localName }, + { value: "zh-CN", label: zhCN.locale.localName }, +]; + +const DEFAULT_SHORTCUT_THEME_MAP = "red=red;blue=blue;dark-blue=dark-blue"; + +function makeShortcutId() { + if (typeof crypto !== "undefined" && "randomUUID" in crypto) { + return crypto.randomUUID(); + } + return `shortcut-${Date.now()}-${Math.random().toString(16).slice(2)}`; +} + +function newShortcutParam(): CompactShortcutRouteParam { + return { + key: "access_token", + paramType: "login", + source: "access_token", + customValue: "", + themeValue: DEFAULT_SHORTCUT_THEME_MAP, + }; +} + +function newShortcutButton(): CompactShortcutButton { + return { + id: makeShortcutId(), + label: "快捷入口", + url: "", + route_params: [newShortcutParam()], + enabled: true, + sort_order: 0, + }; +} + +function normalizeShortcutDraft( + buttons: CompactShortcutButton[] | undefined, +): CompactShortcutButton[] { + const source = buttons?.length ? buttons : [newShortcutButton()]; + return [...source] + .sort((a, b) => a.sort_order - b.sort_order) + .map((button, index) => ({ + ...button, + id: button.id || makeShortcutId(), + label: button.label || "快捷入口", + url: button.url || "", + route_params: (button.route_params?.length + ? button.route_params + : [newShortcutParam()] + ).map((param) => ({ + key: param.key || "", + paramType: param.paramType || "login", + source: param.source || "access_token", + customValue: param.customValue ?? "", + themeValue: param.themeValue || DEFAULT_SHORTCUT_THEME_MAP, + })), + enabled: button.enabled ?? true, + sort_order: index, + })); +} + +function cleanShortcutParams( + params: CompactShortcutRouteParam[], +): CompactShortcutRouteParam[] { + return params + .filter((param) => param.key.trim()) + .map((param) => { + const base: CompactShortcutRouteParam = { + key: param.key.trim(), + paramType: param.paramType, + }; + if (param.paramType === "theme") { + base.themeValue = stringifyThemeMap(parseThemeMap(param.themeValue)); + return base; + } + base.source = param.source || "access_token"; + if (base.source === "other") { + base.customValue = (param.customValue ?? "").trim(); + } + return base; + }); +} + +function cleanShortcutButtons( + buttons: CompactShortcutButton[], +): CompactShortcutButton[] { + return buttons.map((button, index) => ({ + id: button.id || makeShortcutId(), + label: button.label.trim() || "快捷入口", + url: button.url.trim(), + route_params: cleanShortcutParams(button.route_params), + enabled: button.enabled, + sort_order: index, + })); +} export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { const { t, locale, changeLocale } = useI18n(); @@ -52,10 +167,11 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { const navigate = useNavigate(); const currentTheme = theme; const [settings, setSettings] = useLocalSettings(); - const isAdmin = getStoredAuth()?.system_role === "admin"; - const { data: appearance } = useAppearanceSettings(); - const updateAppearance = useUpdateAppearanceSettings(); - const { data: citationDisplay } = useCitationDisplaySettings(); + const isAdmin = getStoredAuth()?.system_role === "admin"; + const { data: appearance } = useAppearanceSettings(); + const updateAppearance = useUpdateAppearanceSettings(); + const [shortcutDraft, setShortcutDraft] = useState([]); + const { data: citationDisplay } = useCitationDisplaySettings(); const updateCitationDisplay = useUpdateCitationDisplaySettings(); const globalDefault = appearance?.default_theme ?? "light"; const globalSkillView = appearance?.default_skill_view ?? "list"; @@ -63,8 +179,14 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { const globalLayoutMode = appearance?.default_layout_mode ?? "full"; // 有效界面模式:用户自己选过则以其为准,否则跟随管理员系统默认(再否则会话级回退)。 const [effectiveLayoutMode, setLayoutMode] = useLayoutMode(); - const cleanupWords = - citationDisplay?.cleanup_words ?? settings.display.citationCleanupWords ?? []; + const cleanupWords = + citationDisplay?.cleanup_words ?? settings.display.citationCleanupWords ?? []; + + useEffect(() => { + setShortcutDraft( + normalizeShortcutDraft(appearance?.compact_shortcut_buttons), + ); + }, [appearance?.compact_shortcut_buttons]); // 独立登录会话(本标签页):开关切换鉴权存储 localStorage ↔ sessionStorage(见 scoped-storage)。 const [isolatedSession, setIsolatedSessionState] = useState(() => isIsolatedSession()); @@ -115,7 +237,7 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { } }; - const handleSetDefaultView = async ( + const handleSetDefaultView = async ( kind: "skill" | "agent", value: "card" | "list", ) => { @@ -134,7 +256,103 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { } catch (e) { toast.error(e instanceof Error ? e.message : "保存失败"); } - }; + }; + + const patchShortcut = ( + id: string, + patch: Partial, + ) => { + setShortcutDraft((items) => + items.map((item) => (item.id === id ? { ...item, ...patch } : item)), + ); + }; + + const patchShortcutParam = ( + id: string, + index: number, + patch: Partial, + ) => { + setShortcutDraft((items) => + items.map((item) => + item.id === id + ? { + ...item, + route_params: item.route_params.map((param, paramIndex) => + paramIndex === index ? { ...param, ...patch } : param, + ), + } + : item, + ), + ); + }; + + const moveShortcut = (id: string, direction: -1 | 1) => { + setShortcutDraft((items) => { + const index = items.findIndex((item) => item.id === id); + const target = index + direction; + if (index < 0 || target < 0 || target >= items.length) return items; + const next = [...items]; + [next[index], next[target]] = [next[target]!, next[index]!]; + return next.map((item, order) => ({ ...item, sort_order: order })); + }); + }; + + const addShortcutParam = (id: string) => { + setShortcutDraft((items) => + items.map((item) => + item.id === id + ? { + ...item, + route_params: [...item.route_params, newShortcutParam()], + } + : item, + ), + ); + }; + + const removeShortcutParam = (id: string, index: number) => { + setShortcutDraft((items) => + items.map((item) => + item.id === id + ? { + ...item, + route_params: item.route_params.filter((_, paramIndex) => paramIndex !== index), + } + : item, + ), + ); + }; + + const handleSaveShortcuts = async () => { + for (const item of shortcutDraft) { + if (item.enabled && !item.url.trim()) { + toast.error(`请填写「${item.label.trim() || "快捷入口"}」的跳转链接,或先关闭该按钮`); + return; + } + for (const param of item.route_params) { + if (!param.key.trim()) { + toast.error("路由参数键名不可为空"); + return; + } + if ( + param.paramType === "login" && + param.source === "other" && + !param.customValue?.trim() + ) { + toast.error("登录参数自定义值不可为空"); + return; + } + } + } + try { + await updateAppearance.mutateAsync({ + compact_shortcut_buttons: cleanShortcutButtons(shortcutDraft), + }); + toast.success("顶部快捷按钮已保存"); + } catch (e) { + toast.error(e instanceof Error ? e.message : "保存失败"); + } + }; const themeOptions = useMemo( () => [ @@ -224,11 +442,11 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { - {isAdmin && ( - <> - - + + @@ -257,13 +475,47 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { {globalLayoutMode === "full" ? "全量模式" : "简洁模式"} - - > - )} - - {isAdmin && ( - <> - + + > + )} + + {isAdmin && ( + <> + + + + setShortcutDraft((items) => [ + ...items, + { ...newShortcutButton(), sort_order: items.length }, + ]) + } + onPatch={patchShortcut} + onMove={moveShortcut} + onRemove={(id) => + setShortcutDraft((items) => + items + .filter((item) => item.id !== id) + .map((item, index) => ({ ...item, sort_order: index })), + ) + } + onAddParam={addShortcutParam} + onPatchParam={patchShortcutParam} + onRemoveParam={removeShortcutParam} + onSave={handleSaveShortcuts} + /> + + > + )} + + {isAdmin && ( + <> + void }) { > )} - ); -} - -function ThemePreviewCard({ - icon: Icon, - label, + ); +} + +function CompactShortcutSettings({ + buttons, + saving, + onAdd, + onPatch, + onMove, + onRemove, + onAddParam, + onPatchParam, + onRemoveParam, + onSave, +}: { + buttons: CompactShortcutButton[]; + saving: boolean; + onAdd: () => void; + onPatch: (id: string, patch: Partial) => void; + onMove: (id: string, direction: -1 | 1) => void; + onRemove: (id: string) => void; + onAddParam: (id: string) => void; + onPatchParam: ( + id: string, + index: number, + patch: Partial, + ) => void; + onRemoveParam: (id: string, index: number) => void; + onSave: () => void; +}) { + return ( + + + + 已配置 {buttons.length} 个按钮;只有启用且填写了跳转链接的按钮会显示。 + + + + + 添加按钮 + + + 保存按钮配置 + + + + + {buttons.length === 0 ? ( + + 暂无按钮,点击“添加按钮”创建一个。 + + ) : ( + + {buttons.map((button, index) => ( + + + + + onPatch(button.id, { enabled: checked }) + } + aria-label="启用快捷按钮" + /> + + {button.enabled ? "启用" : "停用"} + + + + + 按钮名称 + + onPatch(button.id, { label: event.target.value }) + } + placeholder="如:业务系统" + /> + + + 跳转链接 + + onPatch(button.id, { url: event.target.value }) + } + placeholder="https://example.com/page" + className="font-mono" + /> + + + + onMove(button.id, -1)} + disabled={index === 0} + title="上移" + > + + + onMove(button.id, 1)} + disabled={index === buttons.length - 1} + title="下移" + > + + + onRemove(button.id)} + title="删除" + > + + + + + + + + + + 路由参数 + + onAddParam(button.id)} + > + + 添加参数 + + + + {button.route_params.length === 0 ? ( + + 不追加额外参数。 + + ) : ( + + {button.route_params.map((param, paramIndex) => { + const themeMap = + param.paramType === "theme" + ? parseThemeMap(param.themeValue) + : {}; + return ( + + + + onPatchParam(button.id, paramIndex, { + key: event.target.value, + }) + } + placeholder="参数名 key" + className="font-mono text-xs" + /> + + onPatchParam(button.id, paramIndex, { + paramType: value as "login" | "theme", + source: + value === "login" + ? param.source || "access_token" + : undefined, + }) + } + > + + + + + 登录参数 + 主题参数 + + + {param.paramType === "login" ? ( + + + onPatchParam(button.id, paramIndex, { + source: value, + }) + } + > + + + + + {LOGIN_SOURCE_OPTIONS.map((option) => ( + + {option.label} + + ))} + + + {param.source === "other" && ( + + onPatchParam(button.id, paramIndex, { + customValue: event.target.value, + }) + } + placeholder="自定义值" + className="w-32 text-xs" + /> + )} + + ) : ( + + 按当前主题取值 + + )} + + onRemoveParam(button.id, paramIndex) + } + title="删除参数" + > + + + + + {param.paramType === "theme" && ( + + {APP_THEME_COLORS.map((color) => ( + + + + {color.label} + + + onPatchParam(button.id, paramIndex, { + themeValue: stringifyThemeMap({ + ...themeMap, + [color.key]: event.target.value, + }), + }) + } + placeholder={color.key} + className="h-8 min-w-0 font-mono text-xs" + /> + + ))} + + )} + + ); + })} + + )} + + + ))} + + )} + + ); +} + +function ThemePreviewCard({ + icon: Icon, + label, description, active, mode, diff --git a/frontend-web/src/core/system-settings/api.ts b/frontend-web/src/core/system-settings/api.ts index dacfa808..d2c0564e 100644 --- a/frontend-web/src/core/system-settings/api.ts +++ b/frontend-web/src/core/system-settings/api.ts @@ -279,12 +279,31 @@ export async function updatePublishSquares( export type ThemeMode = "light" | "dark" | "lightblue"; export type ViewMode = "card" | "list"; export type LayoutMode = "full" | "compact"; +export type CompactShortcutParamType = "login" | "theme"; + +export interface CompactShortcutRouteParam { + key: string; + paramType: CompactShortcutParamType; + source?: string | null; + customValue?: string | null; + themeValue?: string | null; +} + +export interface CompactShortcutButton { + id: string; + label: string; + url: string; + route_params: CompactShortcutRouteParam[]; + enabled: boolean; + sort_order: number; +} export interface AppearanceSettings { default_theme: ThemeMode; default_skill_view: ViewMode; default_agent_view: ViewMode; default_layout_mode: LayoutMode; + compact_shortcut_buttons: CompactShortcutButton[]; } export interface AppearanceUpdate { @@ -292,6 +311,7 @@ export interface AppearanceUpdate { default_skill_view?: ViewMode; default_agent_view?: ViewMode; default_layout_mode?: LayoutMode; + compact_shortcut_buttons?: CompactShortcutButton[]; } export async function loadAppearanceSettings(): Promise { diff --git a/frontend-web/src/open-canvas/pages/AIWritingPage.tsx b/frontend-web/src/open-canvas/pages/AIWritingPage.tsx index 2cd88cd4..eefd2bf0 100644 --- a/frontend-web/src/open-canvas/pages/AIWritingPage.tsx +++ b/frontend-web/src/open-canvas/pages/AIWritingPage.tsx @@ -450,7 +450,7 @@ function AIWritingBody({ {showSidebar && ( @@ -465,11 +465,11 @@ function AIWritingBody({ onClick={() => setSidebarCollapsed(false)} title="展开侧栏" aria-label="展开侧栏" - className="rounded-md p-1.5 text-slate-400 transition-colors hover:bg-slate-50 hover:text-slate-700" + className="rounded-md p-1.5 text-slate-400 transition-colors hover:bg-slate-50 hover:text-slate-700 dark:text-[#8aa0c4] dark:hover:bg-white/10 dark:hover:text-[#e8edf5]" > - +
+ 已配置 {buttons.length} 个按钮;只有启用且填写了跳转链接的按钮会显示。 +