diff --git a/app/(admin)/settings/_components/access-security-dashboard.tsx b/app/(admin)/settings/_components/access-security-dashboard.tsx index 2662cfeb7..3037596e6 100644 --- a/app/(admin)/settings/_components/access-security-dashboard.tsx +++ b/app/(admin)/settings/_components/access-security-dashboard.tsx @@ -117,10 +117,14 @@ const severityLabels: Record = { }; const severityClassNames: Record = { - info: "border-emerald-200 bg-emerald-50 text-emerald-700", - watch: "border-amber-200 bg-amber-50 text-amber-700", - high: "border-orange-200 bg-orange-50 text-orange-700", - critical: "border-red-200 bg-red-50 text-red-700", + info: + "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800/70 dark:bg-emerald-950/45 dark:text-emerald-300", + watch: + "border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800/70 dark:bg-amber-950/45 dark:text-amber-300", + high: + "border-orange-200 bg-orange-50 text-orange-700 dark:border-orange-800/70 dark:bg-orange-950/45 dark:text-orange-300", + critical: + "border-red-200 bg-red-50 text-red-700 dark:border-red-800/70 dark:bg-red-950/45 dark:text-red-300", }; function formatNumber(value: number | null | undefined) { @@ -145,10 +149,10 @@ function formatTime(value: string | null | undefined) { function getStatusClassName(statusCode: number | null) { if (!statusCode) return "text-muted-foreground"; - if (statusCode >= 500) return "text-red-700"; - if (statusCode >= 400) return "text-orange-700"; - if (statusCode >= 300) return "text-sky-700"; - return "text-emerald-700"; + if (statusCode >= 500) return "text-red-700 dark:text-red-300"; + if (statusCode >= 400) return "text-orange-700 dark:text-orange-300"; + if (statusCode >= 300) return "text-sky-700 dark:text-sky-300"; + return "text-emerald-700 dark:text-emerald-300"; } function AccessSeverityBadge({ severity }: { severity: Severity }) { @@ -260,7 +264,8 @@ export function AccessSecurityDashboard() { value: formatNumber(data?.overview.totalEvents), note: `${hours} 小时窗口`, icon: Activity, - className: "border-slate-200 bg-slate-50 text-slate-900", + className: + "border-slate-200 bg-slate-50 text-slate-900 dark:border-slate-700/70 dark:bg-slate-900/70 dark:text-slate-100", }, { key: "ips", @@ -268,7 +273,8 @@ export function AccessSecurityDashboard() { value: formatNumber(data?.overview.uniqueIps), note: `${formatNumber(data?.overview.abnormalIps)} 个异常标注`, icon: Globe2, - className: "border-sky-200 bg-sky-50 text-sky-900", + className: + "border-sky-200 bg-sky-50 text-sky-900 dark:border-sky-800/70 dark:bg-sky-950/45 dark:text-sky-100", }, { key: "abnormal", @@ -276,7 +282,8 @@ export function AccessSecurityDashboard() { value: formatNumber(data?.overview.abnormalEvents), note: `${formatNumber(data?.overview.manualBlockedEvents)} 次手动阻断`, icon: ShieldAlert, - className: "border-orange-200 bg-orange-50 text-orange-900", + className: + "border-orange-200 bg-orange-50 text-orange-900 dark:border-orange-800/70 dark:bg-orange-950/45 dark:text-orange-100", }, { key: "login", @@ -284,7 +291,8 @@ export function AccessSecurityDashboard() { value: formatNumber(data?.overview.loginSuccessEvents), note: `${formatNumber(data?.overview.unauthorizedEvents)} 次未授权`, icon: LogIn, - className: "border-emerald-200 bg-emerald-50 text-emerald-900", + className: + "border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-800/70 dark:bg-emerald-950/45 dark:text-emerald-100", }, ]; @@ -395,7 +403,7 @@ export function AccessSecurityDashboard() { : 0; return (
@@ -455,26 +463,26 @@ export function AccessSecurityDashboard() {
{activeBlocklistEntries.map((entry) => (
- - + + {entry.ip}
-
+
{entry.reason || "未填写原因"}
-
+
{formatTime(entry.updatedAt)}
-
+
{(data?.rules || []).map((rule) => ( @@ -634,7 +642,7 @@ export function AccessSecurityDashboard() {
-
+
时间
IP
状态
diff --git a/app/(admin)/settings/_components/admin-settings-navigation.tsx b/app/(admin)/settings/_components/admin-settings-navigation.tsx index f449fa28f..feca5a567 100644 --- a/app/(admin)/settings/_components/admin-settings-navigation.tsx +++ b/app/(admin)/settings/_components/admin-settings-navigation.tsx @@ -11,11 +11,9 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { ADMIN_SETTINGS_NAV_GROUPS } from "../_lib/settings-navigation"; +import { ADMIN_SETTINGS_NAV_ITEMS } from "../_lib/settings-navigation"; import type { AdminSettingsNavItem } from "../_lib/types"; -const ALL_SETTINGS_ITEMS = ADMIN_SETTINGS_NAV_GROUPS.flatMap((group) => group.items); - function SettingsQuickAction({ activeTab, item, @@ -73,9 +71,13 @@ export function AdminSettingsNavigation({ onTabChange: (value: string) => void; }) { const router = useRouter(); - const activeItem = ALL_SETTINGS_ITEMS.find((item) => item.value === activeTab); + const activeItem = ADMIN_SETTINGS_NAV_ITEMS.find( + (item) => item.value === activeTab + ); const handleSelectChange = (value: string) => { - const nextItem = ALL_SETTINGS_ITEMS.find((item) => item.value === value); + const nextItem = ADMIN_SETTINGS_NAV_ITEMS.find( + (item) => item.value === value + ); if (nextItem?.href) { router.push(nextItem.href); return; @@ -95,7 +97,7 @@ export function AdminSettingsNavigation({ - {ALL_SETTINGS_ITEMS.map((item) => ( + {ADMIN_SETTINGS_NAV_ITEMS.map((item) => ( {item.label} @@ -119,7 +121,7 @@ export function AdminSettingsNavigation({
- {ALL_SETTINGS_ITEMS.map((item) => ( + {ADMIN_SETTINGS_NAV_ITEMS.map((item) => ( group.items +); diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 31fe53276..f246b43cf 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -318,6 +318,7 @@ import { DEFAULT_LOCAL_AI_EXPANDED_SECTIONS, LOCAL_AI_EXPANDED_STORAGE_KEY, } from "./_lib/local-ai-sections"; +import { ADMIN_SETTINGS_NAV_ITEMS } from "./_lib/settings-navigation"; import type { LocalAiOverviewItem, LocalAiPlanningSectionsProps, @@ -1710,6 +1711,25 @@ function isMoonshotOfficialBaseUrl(value?: string | null) { export function AdminSettingsClient() { const [activeTab, setActiveTab] = useState("users"); + useEffect(() => { + const requestedTab = new URLSearchParams(window.location.search).get("tab"); + if ( + requestedTab && + ADMIN_SETTINGS_NAV_ITEMS.some( + (item) => item.value === requestedTab && !item.href + ) + ) { + setActiveTab(requestedTab); + } + }, []); + + const handleActiveTabChange = (value: string) => { + setActiveTab(value); + const url = new URL(window.location.href); + url.searchParams.set("tab", value); + window.history.replaceState(null, "", url); + }; + // 用户管理状态 const [userSearch, setUserSearch] = useState(""); const [userPage, setUserPage] = useState(1); @@ -8657,10 +8677,10 @@ export function AdminSettingsClient() { return (
- + {/* 用户管理 */} diff --git a/app/(admin)/settings/style-pro/style-pro-settings-client.tsx b/app/(admin)/settings/style-pro/style-pro-settings-client.tsx index ef8c81a89..dce5d9ff9 100644 --- a/app/(admin)/settings/style-pro/style-pro-settings-client.tsx +++ b/app/(admin)/settings/style-pro/style-pro-settings-client.tsx @@ -1,7 +1,7 @@ "use client"; -import Link from "next/link"; -import { ArrowLeft, Plus, Save, Trash2, Wand2 } from "lucide-react"; +import { Plus, Save, Trash2, Wand2 } from "lucide-react"; +import { useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import useSWR from "swr"; import { toast } from "sonner"; @@ -25,6 +25,7 @@ import { import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { fetcher } from "@/lib/utils"; +import { AdminSettingsNavigation } from "../_components/admin-settings-navigation"; import { STYLE_PRO_CONFIG_KEY, defaultStyleProConfig, @@ -322,6 +323,7 @@ function CustomFieldEditor({ } export function StyleProSettingsClient() { + const router = useRouter(); const { data, mutate } = useSWR( `/api/admin/settings?key=${STYLE_PRO_CONFIG_KEY}`, fetcher @@ -561,17 +563,15 @@ export function StyleProSettingsClient() { }; return ( -
-
+
+ router.push(`/settings?tab=${value}`)} + /> + +
+
-
- -

风格化 PRO 设置

配置无限画布里的风格化 PRO 子页面。用户只看到分类和按钮名称,隐藏提示词和模型只在服务端生成时使用。 @@ -592,7 +592,7 @@ export function StyleProSettingsClient() { {saving ? "保存中" : "保存配置"}

-
+
@@ -1193,6 +1193,7 @@ export function StyleProSettingsClient() { ))} -
+ +
); } diff --git a/playwright.helpers.config.ts b/playwright.helpers.config.ts index 061b6225d..1ac030919 100644 --- a/playwright.helpers.config.ts +++ b/playwright.helpers.config.ts @@ -9,6 +9,7 @@ export default defineConfig({ /routes\/image-message-contracts\.test\.ts/, /routes\/production-local-fallbacks\.test\.ts/, /routes\/canvas-entry-performance\.test\.ts/, + /routes\/settings-visual-consistency\.test\.ts/, ], fullyParallel: true, forbidOnly: !!process.env.CI, diff --git a/tests/routes/settings-visual-consistency.test.ts b/tests/routes/settings-visual-consistency.test.ts new file mode 100644 index 000000000..80832a586 --- /dev/null +++ b/tests/routes/settings-visual-consistency.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from "@playwright/test"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +function readSource(path: string) { + return readFileSync(resolve(process.cwd(), path), "utf8"); +} + +test.describe("settings visual consistency", () => { + test("keeps access security surfaces readable in dark mode", () => { + const source = readSource( + "app/(admin)/settings/_components/access-security-dashboard.tsx" + ); + + expect(source).toContain("dark:bg-slate-900/70"); + expect(source).toContain("dark:bg-sky-950/45"); + expect(source).toContain("dark:bg-orange-950/45"); + expect(source).toContain("dark:bg-emerald-950/45"); + expect(source).toContain("dark:bg-slate-800"); + expect(source).toContain("dark:bg-zinc-900/70"); + expect(source).toContain("dark:bg-red-950/30"); + }); + + test("keeps Style Pro inside the shared settings shell", () => { + const mainSource = readSource( + "app/(admin)/settings/admin-settings-client.tsx" + ); + const styleProSource = readSource( + "app/(admin)/settings/style-pro/style-pro-settings-client.tsx" + ); + + const sharedShell = + "settings-workspace w-full px-3 py-3 sm:px-4 xl:px-5"; + expect(mainSource).toContain(sharedShell); + expect(styleProSource).toContain(sharedShell); + expect(styleProSource).toContain(" { + const source = readSource( + "app/(admin)/settings/admin-settings-client.tsx" + ); + + expect(source).toContain( + 'new URLSearchParams(window.location.search).get("tab")' + ); + expect(source).toContain("ADMIN_SETTINGS_NAV_ITEMS.some"); + expect(source).toContain('url.searchParams.set("tab", value)'); + }); +});