feat(ui): streamline settings and community access
This commit is contained in:
1 parent
65dbd407a9
commit
a112205815
12 files changed
+412
-127
No files matched your search
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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)}
|
||||
|
||||
@@ -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' }
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user