fix: 修复proxy_service和account_service中不当的顶层import
- proxy_service.py: 将requests和WhitelistManager改为函数内延迟import, 避免启动时加载不需要的依赖;移除未使用的get_exit_ip_via_proxy导入 - account_service.py: 移除未使用的func、joinedload、AuditLog、 user_has_permission顶层导入
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
|
||||
import { Card, Spin, Tag, theme } from 'antd';
|
||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import type { RealtimeLog } from '../hooks/useWebSocketLogs';
|
||||
|
||||
interface RealtimeLogPanelProps {
|
||||
logs: RealtimeLog[];
|
||||
connected?: boolean;
|
||||
title?: string;
|
||||
emptyText?: ReactNode;
|
||||
height?: number | string;
|
||||
mode?: 'inline' | 'card';
|
||||
collapsible?: boolean;
|
||||
defaultVisible?: boolean;
|
||||
spinWhenEmpty?: boolean;
|
||||
style?: CSSProperties;
|
||||
bodyStyle?: CSSProperties;
|
||||
}
|
||||
|
||||
export default function RealtimeLogPanel({
|
||||
logs,
|
||||
connected = false,
|
||||
title = '实时日志',
|
||||
emptyText = '暂无日志',
|
||||
height = '20vh',
|
||||
mode = 'inline',
|
||||
collapsible = false,
|
||||
defaultVisible = true,
|
||||
spinWhenEmpty = false,
|
||||
style,
|
||||
bodyStyle,
|
||||
}: RealtimeLogPanelProps) {
|
||||
const { token } = theme.useToken();
|
||||
const [visible, setVisible] = useState(defaultVisible);
|
||||
const endRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
endRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [logs, visible]);
|
||||
|
||||
const logColors: Record<string, string> = {
|
||||
error: token.colorError,
|
||||
success: token.colorSuccess,
|
||||
warning: token.colorWarning,
|
||||
info: token.colorText,
|
||||
};
|
||||
|
||||
const logBody = (
|
||||
<div
|
||||
style={{
|
||||
height,
|
||||
overflow: 'auto',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
padding: mode === 'card' ? '8px 16px' : 4,
|
||||
backgroundColor: mode === 'card' ? undefined : token.colorBgLayout,
|
||||
borderRadius: mode === 'card' ? undefined : 4,
|
||||
...bodyStyle,
|
||||
}}
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
spinWhenEmpty && connected ? (
|
||||
<Spin spinning size="small" />
|
||||
) : (
|
||||
<span style={{ color: token.colorTextTertiary }}>{emptyText}</span>
|
||||
)
|
||||
) : (
|
||||
logs.map((log, index) => (
|
||||
<div
|
||||
key={`${index}-${log.message}`}
|
||||
style={{ color: logColors[log.level] || token.colorText, lineHeight: '20px' }}
|
||||
>
|
||||
{log.message}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={endRef} />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (mode === 'card') {
|
||||
return (
|
||||
<Card
|
||||
title={title}
|
||||
size="small"
|
||||
style={{ flex: 1, overflow: 'hidden', ...style }}
|
||||
styles={{ body: { height: '100%', overflow: 'hidden', padding: 0 } }}
|
||||
>
|
||||
{logBody}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ flexShrink: 0, borderTop: `1px solid ${token.colorBorderSecondary}`, ...style }}>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', cursor: collapsible ? 'pointer' : 'default', padding: '4px 0', userSelect: 'none' }}
|
||||
onClick={() => collapsible && setVisible((value) => !value)}
|
||||
>
|
||||
<span style={{ fontWeight: 500, fontSize: 13 }}>{title}</span>
|
||||
{collapsible && (
|
||||
visible ? <UpOutlined style={{ marginLeft: 6, fontSize: 10 }} /> : <DownOutlined style={{ marginLeft: 6, fontSize: 10 }} />
|
||||
)}
|
||||
{logs.length > 0 && <span style={{ marginLeft: 8, fontSize: 12, color: token.colorTextTertiary }}>{logs.length} 条</span>}
|
||||
{connected && <Tag color="processing" style={{ marginLeft: 8 }}>连接中</Tag>}
|
||||
</div>
|
||||
{visible && logBody}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user