mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-05 07:36:53 +08:00
feat(web): add first-pass mobile responsive support
This commit is contained in:
+242
-212
@@ -1,4 +1,4 @@
|
||||
import { Layout, Menu, theme, Button, Modal, Tag, Tooltip, Descriptions, Alert, Divider, Spin } from 'antd';
|
||||
import { Layout, Menu, theme, Button, Modal, Tag, Tooltip, Descriptions, Alert, Divider, Spin, Drawer } from 'antd';
|
||||
import { navGroups } from './nav.ts';
|
||||
import type { NavItem, NavGroup } from './nav.ts';
|
||||
import { memo, useEffect, useState, useMemo } from 'react';
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
MenuFoldOutlined,
|
||||
SendOutlined,
|
||||
WechatOutlined,
|
||||
WarningOutlined
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import '../styles/sider-menu.css';
|
||||
import { getLatestVersion } from '../api/config.ts';
|
||||
@@ -20,6 +20,7 @@ import { useI18n } from '../i18n';
|
||||
import { useAppWindows } from '../contexts/AppWindowsContext';
|
||||
import WeChatModal from '../components/WeChatModal';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
const { Sider } = Layout;
|
||||
|
||||
export interface SideNavProps {
|
||||
@@ -27,9 +28,20 @@ export interface SideNavProps {
|
||||
onToggle(): void;
|
||||
activeKey: string;
|
||||
onChange(key: string): void;
|
||||
mobile?: boolean;
|
||||
open?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const SideNav = memo(function SideNav({ collapsed, activeKey, onChange, onToggle }: SideNavProps) {
|
||||
const SideNav = memo(function SideNav({
|
||||
collapsed,
|
||||
activeKey,
|
||||
onChange,
|
||||
onToggle,
|
||||
mobile = false,
|
||||
open = false,
|
||||
onClose,
|
||||
}: SideNavProps) {
|
||||
const status = useSystemStatus();
|
||||
const { token } = theme.useToken();
|
||||
const { resolvedMode } = useTheme();
|
||||
@@ -41,174 +53,170 @@ const SideNav = memo(function SideNav({ collapsed, activeKey, onChange, onToggle
|
||||
version: string;
|
||||
body: string;
|
||||
} | null>(null);
|
||||
|
||||
// 根据用户权限过滤导航项
|
||||
|
||||
const filteredNavGroups = useMemo(() => {
|
||||
const isAdmin = user?.is_admin ?? false;
|
||||
return navGroups
|
||||
.map(group => ({
|
||||
.map((group) => ({
|
||||
...group,
|
||||
children: group.children.filter(item => !item.adminOnly || isAdmin)
|
||||
children: group.children.filter((item) => !item.adminOnly || isAdmin),
|
||||
}))
|
||||
.filter(group => group.children.length > 0);
|
||||
.filter((group) => group.children.length > 0);
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
getLatestVersion().then(resp => {
|
||||
getLatestVersion().then((resp) => {
|
||||
if (resp.latest_version && resp.body) {
|
||||
setLatestVersion({
|
||||
version: resp.latest_version,
|
||||
body: resp.body
|
||||
body: resp.body,
|
||||
});
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const showVersionModal = () => {
|
||||
setIsVersionModalOpen(true);
|
||||
};
|
||||
|
||||
const hasUpdate = latestVersion && latestVersion.version !== status?.version;
|
||||
const { windows, restoreWindow } = useAppWindows();
|
||||
const minimized = windows.filter(w => w.minimized);
|
||||
const minimized = windows.filter((w) => w.minimized);
|
||||
const DEFAULT_APP_ICON =
|
||||
'data:image/svg+xml;utf8,' +
|
||||
encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||||
<rect x="3" y="3" width="18" height="18" rx="4" ry="4" fill="currentColor" />
|
||||
<rect x="7" y="7" width="10" height="10" rx="2" ry="2" fill="#fff"/>
|
||||
</svg>`
|
||||
</svg>`,
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Sider
|
||||
collapsedWidth={60}
|
||||
collapsible
|
||||
trigger={null}
|
||||
collapsed={collapsed}
|
||||
width={208}
|
||||
const currentCollapsed = mobile ? false : collapsed;
|
||||
|
||||
const handleChange = (key: string) => {
|
||||
onChange(key);
|
||||
if (mobile) {
|
||||
onClose?.();
|
||||
}
|
||||
};
|
||||
|
||||
const renderNavBody = (bodyCollapsed: boolean, showCollapseButton: boolean) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
background: token.colorBgContainer,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
height: 56,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: collapsed ? 'center' : 'space-between',
|
||||
justifyContent: bodyCollapsed ? 'center' : 'space-between',
|
||||
padding: '0 14px',
|
||||
fontWeight: 600,
|
||||
fontSize: 18,
|
||||
letterSpacing: .5,
|
||||
flexShrink: 0
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<img
|
||||
src={status?.logo}
|
||||
alt="Foxel"
|
||||
letterSpacing: 0.5,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', minWidth: 0 }}>
|
||||
<img
|
||||
src={status?.logo}
|
||||
alt="Foxel"
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
objectFit: 'contain',
|
||||
marginRight: bodyCollapsed ? 0 : 8,
|
||||
...(resolvedMode === 'dark'
|
||||
? { filter: 'brightness(0) invert(1)' }
|
||||
: status?.logo?.endsWith('.svg')
|
||||
? { filter: 'brightness(0) saturate(100%)' }
|
||||
: {}),
|
||||
}}
|
||||
/>
|
||||
{!bodyCollapsed && (
|
||||
<span
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
objectFit: 'contain',
|
||||
marginRight: collapsed ? 0 : 8,
|
||||
...(resolvedMode === 'dark'
|
||||
? { filter: 'brightness(0) invert(1)' }
|
||||
: (status?.logo?.endsWith('.svg') ? { filter: 'brightness(0) saturate(100%)' } : {}))
|
||||
}}
|
||||
/>
|
||||
{!collapsed && (
|
||||
<span style={{ fontWeight: 700, color: resolvedMode === 'dark' ? '#fff' : token.colorText }}>
|
||||
{status?.title}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* 展开时显示收缩按钮 */}
|
||||
{!collapsed && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<MenuFoldOutlined />}
|
||||
onClick={onToggle}
|
||||
style={{ fontSize: 18 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* 分组渲染 */}
|
||||
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '4px 4px 8px' }}>
|
||||
{filteredNavGroups.map((group: NavGroup) => (
|
||||
<div key={group.key} style={{ marginBottom: 12 }}>
|
||||
{group.title && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
letterSpacing: .5,
|
||||
padding: '6px 10px 4px',
|
||||
color: token.colorTextTertiary,
|
||||
textTransform: 'uppercase'
|
||||
}}
|
||||
>{t(group.title)}</div>
|
||||
)}
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectable
|
||||
inlineIndent={12}
|
||||
selectedKeys={[activeKey]}
|
||||
onClick={(e) => onChange(e.key)}
|
||||
items={group.children.map((i: NavItem) => ({ key: i.key, icon: i.icon, label: t(i.label) }))}
|
||||
style={{ borderInline: 'none', background: 'transparent' }}
|
||||
className="sider-menu-group foxel-sider-menu"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
bottom: '10px',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
padding: '12px 8px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
flexShrink: 0,
|
||||
borderTop: `1px solid ${token.colorBorderSecondary}`
|
||||
}}
|
||||
>
|
||||
{/* 最小化应用 Dock */}
|
||||
{!collapsed && minimized.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: collapsed ? 'column' : 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
flexWrap: collapsed ? 'nowrap' : 'wrap',
|
||||
maxHeight: collapsed ? 160 : undefined,
|
||||
overflowY: collapsed ? 'auto' : 'visible',
|
||||
fontWeight: 700,
|
||||
color: resolvedMode === 'dark' ? '#fff' : token.colorText,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{minimized.map(w => {
|
||||
const src = w.app.iconUrl || DEFAULT_APP_ICON;
|
||||
const title = w.kind === 'file' ? `${w.app.name} - ${w.entry.name}` : w.app.name;
|
||||
return (
|
||||
<Tooltip key={w.id} title={title} placement={collapsed ? 'right' : 'top'}>
|
||||
<Button
|
||||
shape="circle"
|
||||
onClick={() => restoreWindow(w.id)}
|
||||
icon={<img src={src} alt={w.app.name} style={{ width: 16, height: 16 }} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{status?.title}
|
||||
</span>
|
||||
)}
|
||||
<div style={{
|
||||
</div>
|
||||
{showCollapseButton && !bodyCollapsed && (
|
||||
<Button type="text" icon={<MenuFoldOutlined />} onClick={onToggle} style={{ fontSize: 18 }} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '4px 4px 8px' }}>
|
||||
{filteredNavGroups.map((group: NavGroup) => (
|
||||
<div key={group.key} style={{ marginBottom: 12 }}>
|
||||
{!!group.title && !bodyCollapsed && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
letterSpacing: 0.5,
|
||||
padding: '6px 10px 4px',
|
||||
color: token.colorTextTertiary,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
{t(group.title)}
|
||||
</div>
|
||||
)}
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectable
|
||||
inlineIndent={12}
|
||||
inlineCollapsed={!mobile && bodyCollapsed}
|
||||
selectedKeys={[activeKey]}
|
||||
onClick={(e) => handleChange(e.key)}
|
||||
items={group.children.map((i: NavItem) => ({ key: i.key, icon: i.icon, label: t(i.label) }))}
|
||||
style={{ borderInline: 'none', background: 'transparent' }}
|
||||
className="sider-menu-group foxel-sider-menu"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 8px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
flexShrink: 0,
|
||||
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||
}}
|
||||
>
|
||||
{!bodyCollapsed && minimized.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{minimized.map((w) => {
|
||||
const src = w.app.iconUrl || DEFAULT_APP_ICON;
|
||||
const title = w.kind === 'file' ? `${w.app.name} - ${w.entry?.name || ''}` : w.app.name;
|
||||
return (
|
||||
<Tooltip key={w.id} title={title} placement={bodyCollapsed ? 'right' : 'top'}>
|
||||
<Button
|
||||
shape="circle"
|
||||
onClick={() => restoreWindow(w.id)}
|
||||
icon={<img src={src} alt={w.app.name} style={{ width: 16, height: 16 }} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: token.colorTextSecondary,
|
||||
textAlign: 'center',
|
||||
@@ -216,67 +224,78 @@ const SideNav = memo(function SideNav({ collapsed, activeKey, onChange, onToggle
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer'
|
||||
}} onClick={showVersionModal}>
|
||||
{hasUpdate ? (
|
||||
<Tooltip title={t('New version found: {version}', { version: latestVersion?.version || '' })} placement={collapsed ? 'right' : 'top'}>
|
||||
<a rel="noopener noreferrer"
|
||||
style={{ textDecoration: 'none' }}>
|
||||
{collapsed ? (
|
||||
<Tag icon={<WarningOutlined />} color="warning" style={{ marginInlineEnd: 0 }} />
|
||||
) : (
|
||||
<Tag icon={<WarningOutlined />} color="warning">
|
||||
{t('Update available')} [{latestVersion?.version}]
|
||||
</Tag>
|
||||
)}
|
||||
</a>
|
||||
</Tooltip>
|
||||
) : (
|
||||
latestVersion ? (
|
||||
<Tooltip title={t('You are on the latest: {version}', { version: status?.version || '' })} placement={collapsed ? 'right' : 'top'}>
|
||||
{collapsed ? (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success" style={{ marginInlineEnd: 0 }} />
|
||||
) : (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
{status?.version}
|
||||
</Tag>
|
||||
)}
|
||||
</Tooltip>
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setIsVersionModalOpen(true)}
|
||||
>
|
||||
{hasUpdate ? (
|
||||
<Tooltip title={t('New version found: {version}', { version: latestVersion?.version || '' })} placement={bodyCollapsed ? 'right' : 'top'}>
|
||||
<a rel="noopener noreferrer" style={{ textDecoration: 'none' }}>
|
||||
{bodyCollapsed ? (
|
||||
<Tag icon={<WarningOutlined />} color="warning" style={{ marginInlineEnd: 0 }} />
|
||||
) : (
|
||||
<Tag icon={<WarningOutlined />} color="warning">
|
||||
{t('Update available')} [{latestVersion?.version}]
|
||||
</Tag>
|
||||
)}
|
||||
</a>
|
||||
</Tooltip>
|
||||
) : latestVersion ? (
|
||||
<Tooltip title={t('You are on the latest: {version}', { version: status?.version || '' })} placement={bodyCollapsed ? 'right' : 'top'}>
|
||||
{bodyCollapsed ? (
|
||||
<Tag icon={<CheckCircleOutlined />} color="success" style={{ marginInlineEnd: 0 }} />
|
||||
) : (
|
||||
collapsed ? null : <Tag>{status?.version}</Tag>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<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>
|
||||
<Tag icon={<CheckCircleOutlined />} color="success">
|
||||
{status?.version}
|
||||
</Tag>
|
||||
)}
|
||||
</Tooltip>
|
||||
) : (
|
||||
!bodyCollapsed && <Tag>{status?.version}</Tag>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</Sider>
|
||||
|
||||
{!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>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{mobile ? (
|
||||
<Drawer
|
||||
placement="left"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={null}
|
||||
width={280}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
{renderNavBody(false, false)}
|
||||
</Drawer>
|
||||
) : (
|
||||
<Sider
|
||||
collapsedWidth={60}
|
||||
collapsible
|
||||
trigger={null}
|
||||
collapsed={collapsed}
|
||||
width={208}
|
||||
style={{
|
||||
background: token.colorBgContainer,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||
}}
|
||||
>
|
||||
{renderNavBody(currentCollapsed, true)}
|
||||
</Sider>
|
||||
)}
|
||||
|
||||
<WeChatModal open={isModalOpen} onClose={() => setIsModalOpen(false)} />
|
||||
<Modal
|
||||
open={isVersionModalOpen}
|
||||
@@ -318,31 +337,42 @@ const SideNav = memo(function SideNav({ collapsed, activeKey, onChange, onToggle
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider titlePlacement="left" plain>{t('Changelog')}</Divider>
|
||||
<div style={{
|
||||
maxHeight: '40vh',
|
||||
overflowY: 'auto',
|
||||
padding: '8px 16px',
|
||||
background: token.colorFillAlter,
|
||||
borderRadius: token.borderRadiusLG,
|
||||
border: `1px solid ${token.colorBorderSecondary}`
|
||||
}}>
|
||||
<Divider titlePlacement="left" plain>
|
||||
{t('Changelog')}
|
||||
</Divider>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '40vh',
|
||||
overflowY: 'auto',
|
||||
padding: '8px 16px',
|
||||
background: token.colorFillAlter,
|
||||
borderRadius: token.borderRadiusLG,
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
}}
|
||||
>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
h3: ({ ...props }) => <h3 style={{
|
||||
fontSize: 16,
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
paddingBottom: 8,
|
||||
marginTop: 24,
|
||||
marginBottom: 16,
|
||||
color: token.colorTextHeading
|
||||
}} {...props} />,
|
||||
h3: ({ ...props }) => (
|
||||
<h3
|
||||
style={{
|
||||
fontSize: 16,
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
paddingBottom: 8,
|
||||
marginTop: 24,
|
||||
marginBottom: 16,
|
||||
color: token.colorTextHeading,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ul: ({ ...props }) => <ul style={{ paddingLeft: 20 }} {...props} />,
|
||||
li: ({ ...props }) => <li style={{ marginBottom: 8 }} {...props} />,
|
||||
p: ({ ...props }) => <p style={{ marginBottom: 8 }} {...props} />,
|
||||
a: ({ ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />
|
||||
a: ({ ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />,
|
||||
}}
|
||||
>{latestVersion.body}</ReactMarkdown>
|
||||
>
|
||||
{latestVersion.body}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
import ProfileModal from '../components/ProfileModal';
|
||||
import NoticesModal from '../components/NoticesModal';
|
||||
import { useSystemStatus } from '../contexts/SystemContext';
|
||||
import useResponsive from '../hooks/useResponsive';
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
@@ -17,9 +18,10 @@ export interface TopHeaderProps {
|
||||
collapsed: boolean;
|
||||
onToggle(): void;
|
||||
onOpenAiAgent(): void;
|
||||
showMenuButton?: boolean;
|
||||
}
|
||||
|
||||
const TopHeader = memo(function TopHeader({ collapsed, onToggle, onOpenAiAgent }: TopHeaderProps) {
|
||||
const TopHeader = memo(function TopHeader({ collapsed, onToggle, onOpenAiAgent, showMenuButton }: TopHeaderProps) {
|
||||
const { token } = theme.useToken();
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
@@ -28,6 +30,7 @@ const TopHeader = memo(function TopHeader({ collapsed, onToggle, onOpenAiAgent }
|
||||
const [profileOpen, setProfileOpen] = useState(false);
|
||||
const [noticesOpen, setNoticesOpen] = useState(false);
|
||||
const status = useSystemStatus();
|
||||
const { isMobile } = useResponsive();
|
||||
|
||||
const handleLogout = () => {
|
||||
authApi.logout();
|
||||
@@ -37,24 +40,39 @@ const TopHeader = memo(function TopHeader({ collapsed, onToggle, onOpenAiAgent }
|
||||
const openProfile = () => setProfileOpen(true);
|
||||
|
||||
return (
|
||||
<Header style={{ background: token.colorBgContainer, borderBottom: `1px solid ${token.colorBorderSecondary}`, display: 'flex', alignItems: 'center', gap: 16, backdropFilter: 'saturate(180%) blur(8px)' }}>
|
||||
{collapsed && (
|
||||
<Header
|
||||
style={{
|
||||
background: token.colorBgContainer,
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: isMobile ? 8 : 16,
|
||||
paddingInline: isMobile ? 12 : 16,
|
||||
minWidth: 0,
|
||||
backdropFilter: 'saturate(180%) blur(8px)',
|
||||
}}
|
||||
>
|
||||
{showMenuButton && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<MenuUnfoldOutlined />}
|
||||
onClick={onToggle}
|
||||
style={{ fontSize: 18, marginRight: 8 }}
|
||||
style={{ fontSize: 18, marginRight: isMobile ? 0 : 8 }}
|
||||
aria-label={collapsed ? t('Open menu') : t('Collapse menu')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
icon={<SearchOutlined />}
|
||||
style={{ maxWidth: 420 }}
|
||||
style={{ maxWidth: isMobile ? 40 : 420, minWidth: isMobile ? 40 : undefined, paddingInline: isMobile ? 0 : undefined }}
|
||||
onClick={() => setSearchOpen(true)}
|
||||
aria-label={t('Search files / tags / types')}
|
||||
>
|
||||
{t('Search files / tags / types')}
|
||||
{!isMobile && t('Search files / tags / types')}
|
||||
</Button>
|
||||
<SearchDialog open={searchOpen} onClose={() => setSearchOpen(false)} />
|
||||
<Flex style={{ marginLeft: 'auto' }} align="center" gap={12}>
|
||||
|
||||
<Flex style={{ marginLeft: 'auto', minWidth: 0 }} align="center" gap={isMobile ? 4 : 12}>
|
||||
<Tooltip title={t('Notices')}>
|
||||
<Button
|
||||
type="text"
|
||||
@@ -78,8 +96,8 @@ const TopHeader = memo(function TopHeader({ collapsed, onToggle, onOpenAiAgent }
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'profile', label: t('Profile'), icon: <UserOutlined />, onClick: openProfile },
|
||||
{ key: 'logout', label: t('Log Out'), icon: <LogoutOutlined />, onClick: handleLogout }
|
||||
]
|
||||
{ key: 'logout', label: t('Log Out'), icon: <LogoutOutlined />, onClick: handleLogout },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Button type="text" style={{ paddingInline: 8, height: 40 }}>
|
||||
@@ -87,9 +105,11 @@ const TopHeader = memo(function TopHeader({ collapsed, onToggle, onOpenAiAgent }
|
||||
<Avatar size={28} src={user?.gravatar_url}>
|
||||
{(user?.full_name || user?.username || 'A').charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Typography.Text style={{ maxWidth: 160 }} ellipsis>
|
||||
{user?.full_name || user?.username || t('Admin')}
|
||||
</Typography.Text>
|
||||
{!isMobile && (
|
||||
<Typography.Text style={{ maxWidth: 160 }} ellipsis>
|
||||
{user?.full_name || user?.username || t('Admin')}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
Reference in New Issue
Block a user