diff --git a/web/src/components/dashboard/ChartsPanel.jsx b/web/src/components/dashboard/ChartsPanel.jsx index dc1684a2..0992adac 100644 --- a/web/src/components/dashboard/ChartsPanel.jsx +++ b/web/src/components/dashboard/ChartsPanel.jsx @@ -20,11 +20,6 @@ For commercial licensing, please contact support@quantumnous.com import React from 'react'; import { Card, Tabs, TabPane } from '@douyinfe/semi-ui'; import { PieChart } from 'lucide-react'; -import { - IconHistogram, - IconPulse, - IconPieChart2Stroked, -} from '@douyinfe/semi-icons'; import { VChart } from '@visactor/react-vchart'; const ChartsPanel = ({ @@ -51,46 +46,14 @@ const ChartsPanel = ({ {t('模型数据分析')} - - - {t('消耗分布')} - - } - itemKey='1' - /> - - - {t('消耗趋势')} - - } - itemKey='2' - /> - - - {t('调用次数分布')} - - } - itemKey='3' - /> - - - {t('调用次数排行')} - - } - itemKey='4' - /> + {t('消耗分布')}} itemKey='1' /> + {t('消耗趋势')}} itemKey='2' /> + {t('调用次数分布')}} itemKey='3' /> + {t('调用次数排行')}} itemKey='4' /> } diff --git a/web/src/components/layout/HeaderBar.js b/web/src/components/layout/HeaderBar.js deleted file mode 100644 index fc21dc7b..00000000 --- a/web/src/components/layout/HeaderBar.js +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright (C) 2025 QuantumNous - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact support@quantumnous.com -*/ - -export { default } from './HeaderBar/index'; diff --git a/web/src/components/layout/HeaderBar/HeaderLogo.jsx b/web/src/components/layout/HeaderBar/HeaderLogo.jsx index c81e75d2..73be0516 100644 --- a/web/src/components/layout/HeaderBar/HeaderLogo.jsx +++ b/web/src/components/layout/HeaderBar/HeaderLogo.jsx @@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com import React from 'react'; import { Link } from 'react-router-dom'; import { Typography, Tag } from '@douyinfe/semi-ui'; -import SkeletonWrapper from './SkeletonWrapper'; +import SkeletonWrapper from '../components/SkeletonWrapper'; const HeaderLogo = ({ isMobile, diff --git a/web/src/components/layout/HeaderBar/Navigation.jsx b/web/src/components/layout/HeaderBar/Navigation.jsx index 3a5e3a3b..e2a4a696 100644 --- a/web/src/components/layout/HeaderBar/Navigation.jsx +++ b/web/src/components/layout/HeaderBar/Navigation.jsx @@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com import React from 'react'; import { Link } from 'react-router-dom'; -import SkeletonWrapper from './SkeletonWrapper'; +import SkeletonWrapper from '../components/SkeletonWrapper'; const Navigation = ({ mainNavLinks, diff --git a/web/src/components/layout/HeaderBar/SkeletonWrapper.jsx b/web/src/components/layout/HeaderBar/SkeletonWrapper.jsx deleted file mode 100644 index c6224450..00000000 --- a/web/src/components/layout/HeaderBar/SkeletonWrapper.jsx +++ /dev/null @@ -1,148 +0,0 @@ -/* -Copyright (C) 2025 QuantumNous - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -For commercial licensing, please contact support@quantumnous.com -*/ - -import React from 'react'; -import { Skeleton } from '@douyinfe/semi-ui'; - -const SkeletonWrapper = ({ - loading = false, - type = 'text', - count = 1, - width = 60, - height = 16, - isMobile = false, - className = '', - children, - ...props -}) => { - if (!loading) { - return children; - } - - // 导航链接骨架屏 - const renderNavigationSkeleton = () => { - const skeletonLinkClasses = isMobile - ? 'flex items-center gap-1 p-1 w-full rounded-md' - : 'flex items-center gap-1 p-2 rounded-md'; - - return Array(count) - .fill(null) - .map((_, index) => ( -
- - } - /> -
- )); - }; - - // 用户区域骨架屏 (头像 + 文本) - const renderUserAreaSkeleton = () => { - return ( -
- - } - /> -
- - } - /> -
-
- ); - }; - - // Logo图片骨架屏 - const renderImageSkeleton = () => { - return ( - - } - /> - ); - }; - - // 系统名称骨架屏 - const renderTitleSkeleton = () => { - return ( - } - /> - ); - }; - - // 通用文本骨架屏 - const renderTextSkeleton = () => { - return ( -
- } - /> -
- ); - }; - - // 根据类型渲染不同的骨架屏 - switch (type) { - case 'navigation': - return renderNavigationSkeleton(); - case 'userArea': - return renderUserAreaSkeleton(); - case 'image': - return renderImageSkeleton(); - case 'title': - return renderTitleSkeleton(); - case 'text': - default: - return renderTextSkeleton(); - } -}; - -export default SkeletonWrapper; diff --git a/web/src/components/layout/HeaderBar/UserArea.jsx b/web/src/components/layout/HeaderBar/UserArea.jsx index 5d2c0448..8ea70f47 100644 --- a/web/src/components/layout/HeaderBar/UserArea.jsx +++ b/web/src/components/layout/HeaderBar/UserArea.jsx @@ -28,7 +28,7 @@ import { IconKey, } from '@douyinfe/semi-icons'; import { stringToColor } from '../../../helpers'; -import SkeletonWrapper from './SkeletonWrapper'; +import SkeletonWrapper from '../components/SkeletonWrapper'; const UserArea = ({ userState, diff --git a/web/src/components/layout/PageLayout.jsx b/web/src/components/layout/PageLayout.jsx index 72df89eb..f8cdfb0c 100644 --- a/web/src/components/layout/PageLayout.jsx +++ b/web/src/components/layout/PageLayout.jsx @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import HeaderBar from './HeaderBar'; +import HeaderBar from './headerbar'; import { Layout } from '@douyinfe/semi-ui'; import SiderBar from './SiderBar'; import App from '../../App'; diff --git a/web/src/components/layout/SiderBar.jsx b/web/src/components/layout/SiderBar.jsx index 37e55d76..fad22240 100644 --- a/web/src/components/layout/SiderBar.jsx +++ b/web/src/components/layout/SiderBar.jsx @@ -24,7 +24,9 @@ import { getLucideIcon } from '../../helpers/render'; import { ChevronLeft } from 'lucide-react'; import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed'; import { useSidebar } from '../../hooks/common/useSidebar'; +import { useMinimumLoadingTime } from '../../hooks/common/useMinimumLoadingTime'; import { isAdmin, isRoot, showError } from '../../helpers'; +import SkeletonWrapper from './components/SkeletonWrapper'; import { Nav, Divider, Button } from '@douyinfe/semi-ui'; @@ -56,6 +58,8 @@ const SiderBar = ({ onNavigate = () => {} }) => { loading: sidebarLoading, } = useSidebar(); + const showSkeleton = useMinimumLoadingTime(sidebarLoading, 500); + const [selectedKeys, setSelectedKeys] = useState(['home']); const [chatItems, setChatItems] = useState([]); const [openedKeys, setOpenedKeys] = useState([]); @@ -377,120 +381,137 @@ const SiderBar = ({ onNavigate = () => {} }) => { className='sidebar-container' style={{ width: 'var(--sidebar-current-width)' }} > - + {/* 底部折叠按钮 */}
- + +
); diff --git a/web/src/components/layout/components/SkeletonWrapper.jsx b/web/src/components/layout/components/SkeletonWrapper.jsx new file mode 100644 index 00000000..ba26e696 --- /dev/null +++ b/web/src/components/layout/components/SkeletonWrapper.jsx @@ -0,0 +1,394 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { Skeleton } from '@douyinfe/semi-ui'; + +const SkeletonWrapper = ({ + loading = false, + type = 'text', + count = 1, + width = 60, + height = 16, + isMobile = false, + className = '', + collapsed = false, + showAdmin = true, + children, + ...props +}) => { + if (!loading) { + return children; + } + + // 导航链接骨架屏 + const renderNavigationSkeleton = () => { + const skeletonLinkClasses = isMobile + ? 'flex items-center gap-1 p-1 w-full rounded-md' + : 'flex items-center gap-1 p-2 rounded-md'; + + return Array(count) + .fill(null) + .map((_, index) => ( +
+ + } + /> +
+ )); + }; + + // 用户区域骨架屏 (头像 + 文本) + const renderUserAreaSkeleton = () => { + return ( +
+ + } + /> +
+ + } + /> +
+
+ ); + }; + + // Logo图片骨架屏 + const renderImageSkeleton = () => { + return ( + + } + /> + ); + }; + + // 系统名称骨架屏 + const renderTitleSkeleton = () => { + return ( + } + /> + ); + }; + + // 通用文本骨架屏 + const renderTextSkeleton = () => { + return ( +
+ } + /> +
+ ); + }; + + // 按钮骨架屏(支持圆角) + const renderButtonSkeleton = () => { + return ( +
+ + } + /> +
+ ); + }; + + // 侧边栏导航项骨架屏 (图标 + 文本) + const renderSidebarNavItemSkeleton = () => { + return Array(count) + .fill(null) + .map((_, index) => ( +
+ {/* 图标骨架屏 */} +
+ + } + /> +
+ {/* 文本骨架屏 */} + + } + /> +
+ )); + }; + + // 侧边栏组标题骨架屏 + const renderSidebarGroupTitleSkeleton = () => { + return ( +
+ + } + /> +
+ ); + }; + + // 完整侧边栏骨架屏 - 1:1 还原,去重实现 + const renderSidebarSkeleton = () => { + const NAV_WIDTH = 164; + const NAV_HEIGHT = 30; + const COLLAPSED_WIDTH = 44; + const COLLAPSED_HEIGHT = 44; + const ICON_SIZE = 16; + const TITLE_HEIGHT = 12; + const TEXT_HEIGHT = 16; + + const renderIcon = () => ( + + } + /> + ); + + const renderLabel = (labelWidth) => ( + + } + /> + ); + + const NavRow = ({ labelWidth }) => ( +
+
+ {renderIcon()} +
+ {renderLabel(labelWidth)} +
+ ); + + const CollapsedRow = ({ keyPrefix, index }) => ( +
+ + } + /> +
+ ); + + if (collapsed) { + return ( +
+ {Array(2) + .fill(null) + .map((_, i) => ( + + ))} + {Array(5) + .fill(null) + .map((_, i) => ( + + ))} + {Array(2) + .fill(null) + .map((_, i) => ( + + ))} + {Array(5) + .fill(null) + .map((_, i) => ( + + ))} +
+ ); + } + + const sections = [ + { key: 'chat', titleWidth: 32, itemWidths: [54, 32], wrapper: 'section' }, + { key: 'console', titleWidth: 48, itemWidths: [64, 64, 64, 64, 64] }, + { key: 'personal', titleWidth: 64, itemWidths: [64, 64] }, + ...(showAdmin + ? [{ key: 'admin', titleWidth: 48, itemWidths: [64, 64, 80, 64, 64] }] + : []), + ]; + + return ( +
+ {sections.map((sec, idx) => ( + + {sec.wrapper === 'section' ? ( +
+
+ + } + /> +
+ {sec.itemWidths.map((w, i) => ( + + ))} +
+ ) : ( +
+
+ + } + /> +
+ {sec.itemWidths.map((w, i) => ( + + ))} +
+ )} +
+ ))} +
+ ); + }; + + // 根据类型渲染不同的骨架屏 + switch (type) { + case 'navigation': + return renderNavigationSkeleton(); + case 'userArea': + return renderUserAreaSkeleton(); + case 'image': + return renderImageSkeleton(); + case 'title': + return renderTitleSkeleton(); + case 'sidebarNavItem': + return renderSidebarNavItemSkeleton(); + case 'sidebarGroupTitle': + return renderSidebarGroupTitleSkeleton(); + case 'sidebar': + return renderSidebarSkeleton(); + case 'button': + return renderButtonSkeleton(); + case 'text': + default: + return renderTextSkeleton(); + } +}; + +export default SkeletonWrapper; diff --git a/web/src/components/table/models/modals/UpstreamConflictModal.jsx b/web/src/components/table/models/modals/UpstreamConflictModal.jsx index 5b764663..439166ee 100644 --- a/web/src/components/table/models/modals/UpstreamConflictModal.jsx +++ b/web/src/components/table/models/modals/UpstreamConflictModal.jsx @@ -91,6 +91,7 @@ const UpstreamConflictModal = ({ { title: t('模型'), dataIndex: 'model_name', + fixed: 'left', render: (text) => {text}, }, ]; @@ -235,7 +236,12 @@ const UpstreamConflictModal = ({
{t('仅会覆盖你勾选的字段,未勾选的字段保持本地不变。')}
- +
)} diff --git a/web/src/index.css b/web/src/index.css index fbbd7682..a2e1d08c 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -184,6 +184,7 @@ code { justify-content: center; align-items: center; padding: 12px; + margin-top: auto; cursor: pointer; background-color: var(--semi-color-bg-0); position: sticky;