feat(ui): streamline settings and community access

This commit is contained in:
shiyu committed 2026-10-02 23:22:21 +08:00
1 parent 65dbd407a9
commit a112205815
12 files changed
+412 -127

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
import { Button, Modal, QRCode, theme } from 'antd';
import { SendOutlined, WechatOutlined } from '@ant-design/icons';
import { useI18n } from '../i18n';
export interface CommunityModalProps {
open: boolean;
onClose: () => void;
}
const TELEGRAM_URL = 'https://t.me/+thDsBfyqJxZkNTU1';
export default function CommunityModal({ open, onClose }: CommunityModalProps) {
const { token } = theme.useToken();
const { t } = useI18n();
return (
<Modal open={open} onCancel={onClose} title={t('Join Community')} footer={null} width={600}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 24, padding: '16px 0', textAlign: 'center' }}>
<section style={{ minWidth: 0 }}>
<h3 style={{ fontSize: 16, margin: '0 0 16px' }}><WechatOutlined style={{ marginRight: 8 }} />{t('WeChat')}</h3>
<div style={{ height: 200, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<img src="https://foxel.cc/image/wechat.png" width={200} height={200} style={{ objectFit: 'contain', maxWidth: '100%' }} alt={t('Scan to join WeChat group')} />
</div>
<div style={{ marginTop: 16, color: token.colorTextSecondary }}>
{t('Scan to join WeChat group')}
</div>
<div style={{ marginTop: 8, fontSize: 12, color: token.colorTextTertiary, overflowWrap: 'anywhere' }}>
{t('If QR expires, add drizzle2001 to join')}
</div>
</section>
<section style={{ minWidth: 0 }}>
<h3 style={{ fontSize: 16, margin: '0 0 16px' }}><SendOutlined style={{ marginRight: 8 }} />Telegram</h3>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<QRCode value={TELEGRAM_URL} size={200} color="#111111" bgColor="#ffffff" />
</div>
<Button style={{ marginTop: 16 }} icon={<SendOutlined />} href={TELEGRAM_URL} target="_blank" rel="noopener noreferrer">
{t('Join Telegram group')}
</Button>
</section>
</div>
</Modal>
);
}
+4 -8
View File
@@ -240,23 +240,19 @@ body { margin: 0; font-family: system-ui,-apple-system,BlinkMacSystemFont,'Segoe
min-height: 0;
padding: 5px;
}
.processors-tabs .ant-tabs-content-holder,
.processors-tabs .ant-tabs-content {
.processors-tabs > .ant-tabs-body-holder,
.processors-tabs .processors-tabs-body {
flex: 1;
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
.processors-tabs .ant-tabs-tabpane {
.processors-tabs .processors-tabs-pane {
flex: 1;
height: 100%;
min-height: 0;
display: none;
flex-direction: column;
}
.processors-tabs .ant-tabs-tabpane-active {
display: flex;
overflow: auto;
}
.plugins-tabs {
+8
View File
@@ -173,6 +173,10 @@
"Directory conflicts with existing file": "A file with the same name already exists at the target location",
"Join Community": "Join Community",
"Scan to join WeChat group": "Scan to join WeChat group",
"Community": "Community",
"Documentation": "Documentation",
"WeChat": "WeChat",
"Join Telegram group": "Join Telegram group",
"If QR expires, add drizzle2001 to join": "If QR expires, add drizzle2001 to join",
"Version Info": "Version Info",
"Current Version": "Current Version",
@@ -300,6 +304,10 @@
"Save failed": "Save failed",
"Loading...": "Loading...",
"Appearance Settings": "Appearance Settings",
"Appearance": "Appearance",
"System": "System",
"AI": "AI",
"Mappings": "Mappings",
"Theme": "Theme",
"Theme Mode": "Theme Mode",
"Light": "Light",
+7
View File
@@ -193,6 +193,10 @@
"Directory conflicts with existing file": "目标存在同名文件,无法创建目录",
"Join Community": "加入社区",
"Scan to join WeChat group": "微信扫码加入交流群",
"Community": "社区",
"Documentation": "文档",
"WeChat": "微信",
"Join Telegram group": "加入 Telegram 群组",
"If QR expires, add drizzle2001 to join": "如二维码失效,请添加 drizzle2001 拉群",
"Version Info": "版本信息",
"Current Version": "当前版本",
@@ -319,6 +323,9 @@
"Save failed": "保存失败",
"Loading...": "加载中...",
"Appearance Settings": "外观设置",
"Appearance": "外观",
"AI": "AI",
"Mappings": "映射",
"Theme": "主题",
"Theme Mode": "主题模式",
"Light": "亮色",
+24 -13
View File
@@ -8,8 +8,8 @@ import {
FileTextOutlined,
GithubOutlined,
MenuFoldOutlined,
SendOutlined,
WechatOutlined,
SettingOutlined,
TeamOutlined,
WarningOutlined,
} from '@ant-design/icons';
import '../styles/sider-menu.css';
@@ -18,7 +18,7 @@ import ReactMarkdown from 'react-markdown';
import { useTheme } from '../contexts/ThemeContext';
import { useI18n } from '../i18n';
import { useAppWindows } from '../contexts/AppWindowsContext';
import WeChatModal from '../components/WeChatModal';
import CommunityModal from '../components/CommunityModal';
import { useAuth } from '../contexts/AuthContext';
const { Sider } = Layout;
@@ -47,7 +47,7 @@ const SideNav = memo(function SideNav({
const { resolvedMode } = useTheme();
const { t } = useI18n();
const { user } = useAuth();
const [isModalOpen, setIsModalOpen] = useState(false);
const [communityOpen, setCommunityOpen] = useState(false);
const [isVersionModalOpen, setIsVersionModalOpen] = useState(false);
const [latestVersion, setLatestVersion] = useState<{
version: string;
@@ -255,14 +255,25 @@ const SideNav = memo(function SideNav({
)}
</div>
{!bodyCollapsed && (
<div style={{ display: 'flex', flexDirection: 'row', gap: 8 }}>
<Button shape="circle" icon={<GithubOutlined />} href="https://github.com/DrizzleTime/Foxel" target="_blank" />
<Button shape="circle" icon={<WechatOutlined />} onClick={() => setIsModalOpen(true)} />
<Button shape="circle" icon={<SendOutlined />} href="https://t.me/+thDsBfyqJxZkNTU1" target="_blank" />
<Button shape="circle" icon={<FileTextOutlined />} href="https://foxel.cc" target="_blank" />
</div>
)}
<div style={{ display: 'flex', flexDirection: bodyCollapsed ? 'column' : 'row', gap: 8 }}>
<Tooltip title="GitHub" placement={bodyCollapsed ? 'right' : 'top'}>
<Button shape="circle" aria-label="GitHub" icon={<GithubOutlined />} href="https://github.com/DrizzleTime/Foxel" target="_blank" rel="noopener noreferrer" />
</Tooltip>
<Tooltip title={t('Community')} placement={bodyCollapsed ? 'right' : 'top'}>
<Button shape="circle" aria-label={t('Community')} icon={<TeamOutlined />} onClick={() => {
if (mobile) onClose?.();
setCommunityOpen(true);
}} />
</Tooltip>
<Tooltip title={t('Documentation')} placement={bodyCollapsed ? 'right' : 'top'}>
<Button shape="circle" aria-label={t('Documentation')} icon={<FileTextOutlined />} href="https://foxel.cc" target="_blank" rel="noopener noreferrer" />
</Tooltip>
{user?.is_admin && (
<Tooltip title={t('System Settings')} placement={bodyCollapsed ? 'right' : 'top'}>
<Button shape="circle" aria-label={t('System Settings')} icon={<SettingOutlined />} onClick={() => handleChange('settings')} />
</Tooltip>
)}
</div>
</div>
</div>
);
@@ -296,7 +307,7 @@ const SideNav = memo(function SideNav({
</Sider>
)}
<WeChatModal open={isModalOpen} onClose={() => setIsModalOpen(false)} />
<CommunityModal open={communityOpen} onClose={() => setCommunityOpen(false)} />
<Modal
open={isVersionModalOpen}
onCancel={() => setIsVersionModalOpen(false)}
-2
View File
@@ -4,7 +4,6 @@ import {
ApiOutlined,
ShareAltOutlined,
CloudDownloadOutlined,
SettingOutlined,
RobotOutlined,
BugOutlined,
DatabaseOutlined,
@@ -44,7 +43,6 @@ export const navGroups: NavGroup[] = [
title: 'System',
children: [
{ key: 'users', icon: React.createElement(UserOutlined), label: 'User Management', adminOnly: true },
{ key: 'settings', icon: React.createElement(SettingOutlined), label: 'System Settings', adminOnly: true },
{ key: 'backup', icon: React.createElement(DatabaseOutlined), label: 'Backup & Restore', hideOnMobile: true },
{ key: 'audit', icon: React.createElement(BugOutlined), label: 'Audit Logs' }
]
+1
View File
@@ -585,6 +585,7 @@ const ProcessorsPage = memo(function ProcessorsPage() {
styles={{ body: { padding: 0, flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' } }}
>
<Tabs
classNames={{ body: 'processors-tabs-body', content: 'processors-tabs-pane' }}
activeKey={activeTab}
onChange={key => setActiveTab(key as TabKey)}
items={tabs as any}
@@ -1,8 +1,7 @@
import { Alert, message, Tabs, Space } from 'antd';
import { Alert, message, Tabs, Modal } from 'antd';
import { useEffect, useState } from 'react';
import PageCard from '../../components/PageCard';
import { getAllConfig, setConfig } from '../../api/config';
import { AppstoreOutlined, RobotOutlined, DatabaseOutlined, SkinOutlined, MailOutlined, CloudSyncOutlined } from '@ant-design/icons';
import { AppstoreOutlined, RobotOutlined, DatabaseOutlined, SkinOutlined, MailOutlined, CloudSyncOutlined, CloseOutlined } from '@ant-design/icons';
import { useTheme } from '../../contexts/ThemeContext';
import '../../styles/settings-tabs.css';
import { useI18n } from '../../i18n';
@@ -18,12 +17,21 @@ type TabKey = 'appearance' | 'app' | 'email' | 'ai' | 'vector-db' | 'mappings';
const TAB_KEYS: TabKey[] = ['appearance', 'app', 'email', 'ai', 'vector-db', 'mappings'];
const DEFAULT_TAB: TabKey = 'appearance';
const TAB_TITLES: Record<TabKey, string> = {
appearance: 'Appearance',
app: 'System',
email: 'Email',
ai: 'AI',
'vector-db': 'Database',
mappings: 'Mappings',
};
const isValidTab = (key?: string): key is TabKey => !!key && (TAB_KEYS as string[]).includes(key);
interface SystemSettingsPageProps {
tabKey?: string;
onTabNavigate?: (key: TabKey, options?: { replace?: boolean }) => void;
onClose?: () => void;
}
const APP_CONFIG_KEYS: { key: string, label: string, default?: string }[] = [
@@ -67,7 +75,7 @@ const CONFIG_DEFAULTS: Record<string, string> = {
const stringifyConfigValue = (value: unknown) => String(value ?? '');
export default function SystemSettingsPage({ tabKey, onTabNavigate }: SystemSettingsPageProps) {
export default function SystemSettingsPage({ tabKey, onTabNavigate, onClose }: SystemSettingsPageProps) {
const [loading, setLoading] = useState(false);
const [config, setConfigState] = useState<Record<string, string> | null>(null);
const [loadError, setLoadError] = useState<string | null>(null);
@@ -145,35 +153,47 @@ export default function SystemSettingsPage({ tabKey, onTabNavigate }: SystemSett
onTabNavigate?.(nextKey);
};
if (loadError) {
return (
<PageCard title={t('System Settings')}>
<Alert type="error" showIcon message={loadError} />
</PageCard>
);
}
if (!config) {
return <PageCard title={t('System Settings')}><div>{t('Loading...')}</div></PageCard>;
}
return (
<PageCard
title={t('System Settings')}
<Modal
open
onCancel={onClose}
closeIcon={<CloseOutlined />}
title={
<div className="fx-settings-modal-heading">
<span className="fx-settings-modal-title">{t('System Settings')}</span>
<span className="fx-settings-modal-section-title">{t(TAB_TITLES[activeTab])}</span>
</div>
}
footer={null}
width={isMobile ? 'calc(100vw - 24px)' : 1180}
style={{ maxWidth: 'calc(100vw - 24px)' }}
centered
destroyOnHidden={false}
className="fx-settings-modal"
styles={{
body: { padding: 0, overflow: 'hidden' },
container: { padding: 0, overflow: 'hidden' },
}}
>
<Space orientation="vertical" style={{ width: '100%' }} size={16}>
{loadError ? (
<div className="fx-settings-modal-message"><Alert type="error" showIcon message={loadError} /></div>
) : !config ? (
<div className="fx-settings-modal-message">{t('Loading...')}</div>
) : (
<Tabs
className="fx-settings-tabs"
classNames={{ body: 'fx-settings-body', content: 'fx-settings-pane' }}
tabPlacement={isMobile ? 'top' : 'start'}
activeKey={activeTab}
onChange={handleTabChange}
centered={!isMobile}
centered={false}
items={[
{
key: 'appearance',
label: (
<span>
<SkinOutlined style={{ marginRight: 8 }} />
{t('Appearance Settings')}
{t(TAB_TITLES.appearance)}
</span>
),
children: (
@@ -190,7 +210,7 @@ export default function SystemSettingsPage({ tabKey, onTabNavigate }: SystemSett
label: (
<span>
<AppstoreOutlined style={{ marginRight: 8 }} />
{t('App Settings')}
{t(TAB_TITLES.app)}
</span>
),
children: (
@@ -207,7 +227,7 @@ export default function SystemSettingsPage({ tabKey, onTabNavigate }: SystemSett
label: (
<span>
<MailOutlined style={{ marginRight: 8 }} />
{t('Email Settings')}
{t(TAB_TITLES.email)}
</span>
),
children: (
@@ -223,7 +243,7 @@ export default function SystemSettingsPage({ tabKey, onTabNavigate }: SystemSett
label: (
<span>
<RobotOutlined style={{ marginRight: 8 }} />
{t('AI Settings')}
{t(TAB_TITLES.ai)}
</span>
),
children: (
@@ -236,7 +256,7 @@ export default function SystemSettingsPage({ tabKey, onTabNavigate }: SystemSett
label: (
<span>
<DatabaseOutlined style={{ marginRight: 8 }} />
{t('Vector Database')}
{t(TAB_TITLES['vector-db'])}
</span>
),
children: (
@@ -248,7 +268,7 @@ export default function SystemSettingsPage({ tabKey, onTabNavigate }: SystemSett
label: (
<span>
<CloudSyncOutlined style={{ marginRight: 8 }} />
{t('Protocol Mappings')}
{t(TAB_TITLES.mappings)}
</span>
),
children: (
@@ -261,7 +281,7 @@ export default function SystemSettingsPage({ tabKey, onTabNavigate }: SystemSett
},
]}
/>
</Space>
</PageCard>
)}
</Modal>
);
}
@@ -1,4 +1,6 @@
import { Form, Input, Button, InputNumber, Card, Radio, message } from 'antd';
import { Form, Input, Button, InputNumber, ColorPicker, Radio, message } from 'antd';
import { SaveOutlined } from '@ant-design/icons';
import { useEffect } from 'react';
import { useTheme } from '../../../contexts/ThemeContext';
import { useI18n } from '../../../i18n';
@@ -23,9 +25,11 @@ export default function AppearanceSettingsTab({
onSave,
themeKeys,
}: AppearanceSettingsTabProps) {
const { previewTheme } = useTheme();
const { previewTheme, refreshTheme } = useTheme();
const { t } = useI18n();
useEffect(() => () => { void refreshTheme(); }, [refreshTheme]);
return (
<Form
layout="vertical"
@@ -66,10 +70,11 @@ export default function AppearanceSettingsTab({
}
await onSave(vals);
}}
style={{ marginTop: 24 }}
className="fx-appearance-form"
key={'appearance-' + JSON.stringify(config)}
>
<Card title={t('Theme')}>
<section className="fx-settings-section">
<h3>{t('Theme')}</h3>
<Form.Item name={themeKeys.MODE} label={t('Theme Mode')}>
<Radio.Group buttonStyle="solid">
<Radio.Button value="light">{t('Light')}</Radio.Button>
@@ -77,23 +82,28 @@ export default function AppearanceSettingsTab({
<Radio.Button value="system">{t('Follow System')}</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item name={themeKeys.PRIMARY} label={t('Primary Color')}>
<Input type="color" size="large" />
<Form.Item
name={themeKeys.PRIMARY}
label={t('Primary Color')}
getValueFromEvent={(_, hex: string) => hex}
>
<ColorPicker showText disabledAlpha />
</Form.Item>
<Form.Item name={themeKeys.RADIUS} label={t('Border Radius')}>
<InputNumber min={0} max={24} style={{ width: '100%' }} />
<InputNumber min={0} max={24} style={{ width: 120 }} />
</Form.Item>
</Card>
<Card title={t('Advanced')} style={{ marginTop: 24 }}>
</section>
<section className="fx-settings-section">
<h3>{t('Advanced')}</h3>
<Form.Item name={themeKeys.TOKENS} label={t('Override AntD Tokens (JSON)')} tooltip={t('e.g. {"colorText": "#222"}')}>
<Input.TextArea autoSize={{ minRows: 4 }} placeholder='{ "colorText": "#222" }' />
<Input.TextArea autoSize={{ minRows: 3, maxRows: 8 }} placeholder='{ "colorText": "#222" }' />
</Form.Item>
<Form.Item name={themeKeys.CSS} label={t('Custom CSS')}>
<Input.TextArea autoSize={{ minRows: 6 }} placeholder={":root{ }\n/* CSS */"} />
<Input.TextArea autoSize={{ minRows: 4, maxRows: 10 }} placeholder={":root{ }\n/* CSS */"} />
</Form.Item>
</Card>
<Form.Item style={{ marginTop: 24 }}>
<Button type="primary" htmlType="submit" loading={loading} block>
</section>
<Form.Item className="fx-settings-save">
<Button type="primary" htmlType="submit" loading={loading} icon={<SaveOutlined />}>
{t('Save')}
</Button>
</Form.Item>
+39 -23
View File
@@ -1,6 +1,7 @@
import { Layout, Flex } from 'antd';
import { memo, useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router';
import { memo, useCallback, useEffect, useState } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router';
import type { Location } from 'react-router';
import SideNav from '../layout/SideNav.tsx';
import TopHeader from '../layout/TopHeader.tsx';
import FileExplorerPage from '../pages/FileExplorerPage/FileExplorerPage.tsx';
@@ -20,11 +21,14 @@ import { AppWindowsLayer } from '../apps/AppWindowsLayer';
import AiAgentWidget from '../components/AiAgentWidget';
import useResponsive from '../hooks/useResponsive';
const ShellBody = memo(function ShellBody() {
const ShellBody = memo(function ShellBody({ navigationLocation }: { navigationLocation?: Location }) {
const params = useParams<{ navKey?: string; '*': string }>();
const navKey = params.navKey ?? 'files';
const subPath = params['*'] ?? '';
const navigate = useNavigate();
const pageLocation = useLocation();
const location = navigationLocation ?? pageLocation;
const settingsOpen = /^\/settings(?:\/|$)/.test(location.pathname);
const { isMobile } = useResponsive();
const COLLAPSED_KEY = 'layout.siderCollapsed';
const [collapsed, setCollapsed] = useState(() => localStorage.getItem(COLLAPSED_KEY) === '1');
@@ -40,7 +44,27 @@ const ShellBody = memo(function ShellBody() {
}, [isMobile, navKey, subPath]);
const { windows, closeWindow, toggleMax, bringToFront, updateWindow } = useAppWindows();
const settingsTab = navKey === 'settings' ? (subPath.split('/')[0] || undefined) : undefined;
const settingsTab = settingsOpen ? location.pathname.split('/')[2] : undefined;
const handleNavChange = useCallback((key: string) => {
setMobileNavOpen(false);
if (key === 'settings') {
navigate('/settings/appearance', {
state: { settingsBackground: location },
});
} else {
navigate(`/${key}`);
}
}, [location, navigate]);
const handleSettingsTabNavigate = useCallback((key: string) => {
navigate(`/settings/${key}`, { replace: true, state: location.state });
}, [location.state, navigate]);
const handleCloseSettings = () => {
const background = location.state?.settingsBackground;
const target = background && !/^\/settings(?:\/|$)/.test(background.pathname)
? background
: { pathname: '/files' };
navigate(target, { replace: true, state: background?.state });
};
const agentCurrentPath = navKey === 'files' ? ('/' + subPath).replace(/\/+/g, '/').replace(/\/+$/, '') || '/' : null;
const handleToggleNav = () => {
if (isMobile) {
@@ -57,13 +81,7 @@ const ShellBody = memo(function ShellBody() {
collapsed={collapsed}
onToggle={handleToggleNav}
activeKey={navKey}
onChange={(key) => {
if (key === 'settings') {
navigate('/settings/appearance', { replace: true });
} else {
navigate(`/${key}`);
}
}}
onChange={handleNavChange}
/>
)}
@@ -75,13 +93,7 @@ const ShellBody = memo(function ShellBody() {
collapsed={false}
onToggle={handleToggleNav}
activeKey={navKey}
onChange={(key) => {
if (key === 'settings') {
navigate('/settings/appearance', { replace: true });
} else {
navigate(`/${key}`);
}
}}
onChange={handleNavChange}
/>
)}
@@ -113,9 +125,6 @@ const ShellBody = memo(function ShellBody() {
{navKey === 'processors' && <ProcessorsPage />}
{navKey === 'offline' && <OfflineDownloadPage />}
{navKey === 'plugins' && <PluginsPage />}
{navKey === 'settings' && (
<SystemSettingsPage tabKey={settingsTab} onTabNavigate={(key, options) => navigate(`/settings/${key}`, options)} />
)}
{navKey === 'audit' && <AuditLogsPage />}
{navKey === 'backup' && <BackupPage />}
{navKey === 'users' && <UsersPage />}
@@ -124,6 +133,13 @@ const ShellBody = memo(function ShellBody() {
</Layout.Content>
</Layout>
{settingsOpen && (
<SystemSettingsPage
tabKey={settingsTab}
onTabNavigate={handleSettingsTabNavigate}
onClose={handleCloseSettings}
/>
)}
<AppWindowsLayer
windows={windows}
onClose={closeWindow}
@@ -136,10 +152,10 @@ const ShellBody = memo(function ShellBody() {
);
});
const LayoutShell = memo(function LayoutShell() {
const LayoutShell = memo(function LayoutShell({ navigationLocation }: { navigationLocation?: Location }) {
return (
<AppWindowsProvider>
<ShellBody />
<ShellBody navigationLocation={navigationLocation} />
</AppWindowsProvider>
);
});
+15 -2
View File
@@ -35,11 +35,24 @@ function RequireAuth({ children }: { children: JSX.Element }) {
}
export function AppRouter() {
const location = useLocation();
const settingsOpen = /^\/settings(?:\/|$)/.test(location.pathname);
const background = location.state?.settingsBackground;
const pageLocation = settingsOpen
? background && !/^\/settings(?:\/|$)/.test(background.pathname)
? background
: { ...location, pathname: '/files', search: '', hash: '' }
: location;
return (
<RequireAuth>
<Routes>
<Routes location={pageLocation}>
{routes.map(r => (
<Route key={r.path} path={r.path} element={r.element} />
<Route
key={r.path}
path={r.path}
element={r.path === '/:navKey/*' ? <LayoutShell navigationLocation={location} /> : r.element}
/>
))}
</Routes>
</RequireAuth>
+199 -37
View File
@@ -1,67 +1,229 @@
.fx-settings-tabs .ant-tabs-nav {
margin-bottom: 12px;
.fx-settings-modal {
max-width: calc(100vw - 64px);
padding-bottom: 0;
}
.fx-settings-tabs .ant-tabs-nav-list {
padding: 4px 8px;
width: 100%;
gap: 8px;
.fx-settings-modal .ant-modal-container {
padding: 0;
overflow: hidden;
border-radius: 20px;
box-shadow: 0 20px 64px rgba(0, 0, 0, 0.18);
}
.fx-settings-tabs .ant-tabs-tab {
margin: 0 !important;
.fx-settings-modal .ant-modal-header {
margin: 0;
padding: 0;
}
.fx-settings-modal .ant-modal-close {
top: 16px;
right: 18px;
}
.fx-settings-modal-heading {
display: grid;
grid-template-columns: 180px minmax(0, 1fr);
min-height: 68px;
font-size: 18px;
letter-spacing: 0;
}
.fx-settings-modal-title,
.fx-settings-modal-section-title {
display: flex;
align-items: center;
padding: 18px 24px;
}
.fx-settings-modal-title {
background: var(--ant-color-fill-quaternary);
border-right: 1px solid var(--ant-color-border-secondary);
}
.fx-settings-modal-section-title {
padding-right: 64px;
border-bottom: 1px solid var(--ant-color-border-secondary);
}
.fx-settings-tabs {
height: min(730px, calc(100dvh - 164px));
min-height: 0;
}
.fx-settings-tabs > .ant-tabs-nav {
width: 180px;
min-width: 180px;
padding: 12px 10px;
margin: 0;
background: var(--ant-color-fill-quaternary);
border-right: 1px solid var(--ant-color-border-secondary);
}
.fx-settings-tabs > .ant-tabs-nav::before,
.fx-settings-tabs > .ant-tabs-nav .ant-tabs-ink-bar {
display: none;
}
.fx-settings-tabs > .ant-tabs-nav .ant-tabs-tab {
margin: 0 0 4px;
padding: 11px 12px;
border-radius: 8px;
padding: 8px 12px !important;
flex: 1 1 auto;
justify-content: center;
text-align: left;
white-space: normal;
}
.fx-settings-tabs .ant-tabs-tab .ant-tabs-tab-btn {
color: var(--ant-color-text-secondary) !important;
.fx-settings-tabs > .ant-tabs-nav .ant-tabs-tab-btn {
width: 100%;
color: var(--ant-color-text-secondary);
text-shadow: none;
}
.fx-settings-tabs .ant-tabs-tab:hover {
background: var(--ant-color-fill-tertiary) !important;
.fx-settings-tabs > .ant-tabs-nav .ant-tabs-tab-btn > span {
display: flex;
align-items: center;
}
/* 选中态:按主题细分 */
html[data-theme='dark'] .fx-settings-tabs .ant-tabs-tab-active {
background: var(--ant-color-primary-bg) !important;
.fx-settings-tabs > .ant-tabs-nav .ant-tabs-tab:hover {
background: var(--ant-color-fill-tertiary);
}
html[data-theme='dark'] .fx-settings-tabs .ant-tabs-tab-active .ant-tabs-tab-btn,
html[data-theme='dark'] .fx-settings-tabs .ant-tabs-tab-active .ant-tabs-tab-btn .anticon {
color: var(--ant-color-text) !important;
.fx-settings-tabs > .ant-tabs-nav .ant-tabs-tab-active {
background: var(--ant-color-fill-secondary);
}
.fx-settings-tabs > .ant-tabs-nav .ant-tabs-tab-active .ant-tabs-tab-btn {
color: var(--ant-color-text);
font-weight: 600;
}
html[data-theme='light'] .fx-settings-tabs .ant-tabs-tab-active {
background: var(--ant-color-primary) !important;
.fx-settings-tabs > .ant-tabs-body-holder {
min-width: 0;
min-height: 0;
border: 0;
overflow: hidden;
}
html[data-theme='light'] .fx-settings-tabs .ant-tabs-tab-active .ant-tabs-tab-btn,
html[data-theme='light'] .fx-settings-tabs .ant-tabs-tab-active .ant-tabs-tab-btn .anticon {
color: var(--ant-color-text-light-solid) !important;
.fx-settings-tabs .fx-settings-body {
height: 100%;
}
.fx-settings-modal .fx-settings-tabs > .ant-tabs-body-holder > .fx-settings-body > .fx-settings-pane {
height: 100%;
padding: 24px 32px 32px;
overflow: auto;
scrollbar-width: thin;
scrollbar-color: var(--ant-color-border) transparent;
overscroll-behavior: contain;
}
.fx-settings-pane > .ant-form,
.fx-settings-pane > .ant-space,
.fx-settings-pane > .ant-card {
margin-top: 0 !important;
}
.fx-settings-section {
padding-bottom: 8px;
}
.fx-settings-section + .fx-settings-section {
padding-top: 24px;
border-top: 1px solid var(--ant-color-border-secondary);
}
.fx-settings-section h3 {
margin: 0 0 20px;
color: var(--ant-color-text-secondary);
font-size: 14px;
font-weight: 600;
}
.fx-settings-tabs .ant-tabs-ink-bar {
background: var(--ant-color-primary) !important;
.fx-settings-save {
position: sticky;
bottom: -32px;
margin: 0;
padding: 16px 0;
background: var(--ant-color-bg-container);
border-top: 1px solid var(--ant-color-border-secondary);
}
.fx-settings-modal-message {
min-height: 240px;
padding: 32px;
}
.fx-settings-tabs .fx-ai-top-bar h3 {
font-size: 18px;
}
@media (max-width: 767px) {
.fx-settings-tabs .ant-tabs-nav {
overflow-x: auto;
.fx-settings-modal {
width: calc(100vw - 24px) !important;
max-width: calc(100vw - 24px);
margin: 12px auto;
}
.fx-settings-tabs .ant-tabs-nav-list {
width: max-content;
min-width: 100%;
flex-wrap: nowrap;
padding: 2px 4px;
.fx-settings-modal .ant-modal-container {
border-radius: 16px;
}
.fx-settings-tabs .ant-tabs-tab {
.fx-settings-modal-heading {
grid-template-columns: minmax(0, 1fr);
min-height: 60px;
}
.fx-settings-modal-title {
padding: 18px 20px;
padding-right: 60px;
border: 0;
background: var(--ant-color-bg-container);
}
.fx-settings-modal-section-title {
display: none;
}
.fx-settings-tabs {
height: calc(100dvh - 108px);
flex-direction: column;
}
.fx-settings-tabs > .ant-tabs-nav {
width: 100%;
min-width: 0;
padding: 6px 12px;
border: 0;
border-bottom: 1px solid var(--ant-color-border-secondary);
}
.fx-settings-tabs > .ant-tabs-nav .ant-tabs-tab {
flex: 0 0 auto;
justify-content: flex-start;
padding: 9px 12px;
margin: 0 4px 0 0;
white-space: nowrap;
}
.fx-settings-tabs > .ant-tabs-body-holder {
flex: 1;
}
.fx-settings-modal .fx-settings-tabs > .ant-tabs-body-holder > .fx-settings-body > .fx-settings-pane {
padding: 20px 18px 24px;
}
.fx-settings-save {
bottom: -24px;
}
}
.fx-settings-modal .ant-color-picker-trigger,
.fx-settings-modal .ant-input-outlined,
.fx-settings-modal .ant-input-number-outlined {
background: var(--ant-color-bg-container);
color: var(--ant-color-text);
}
.fx-settings-modal .ant-radio-button-wrapper:not(.ant-radio-button-wrapper-checked) {
background: var(--ant-color-bg-container);
color: var(--ant-color-text);
}