From b0a3dedb99df98b1264adb265650f216be89a4c3 Mon Sep 17 00:00:00 2001 From: puz <13060209078@163.com> Date: Thu, 2 Jul 2026 08:36:49 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=89=8D=E7=AB=AF=E6=95=B4?= =?UTF-8?q?=E4=BD=93=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/AppLayout.tsx | 110 ------- .../ActionHelpPanel/ActionHelpPanel.css | 259 --------------- .../ActionHelpPanel/ActionHelpPanel.jsx | 228 ------------- .../shared/BottomHintBar/BottomHintBar.css | 304 ------------------ .../shared/BottomHintBar/BottomHintBar.jsx | 90 ------ .../ButtonWithGuide/ButtonWithGuide.css | 196 ----------- .../ButtonWithGuide/ButtonWithGuide.jsx | 165 ---------- .../ButtonWithGuideBadge.css | 243 -------------- .../ButtonWithGuideBadge.jsx | 222 ------------- .../ButtonWithHoverCard.css | 189 ----------- .../ButtonWithHoverCard.jsx | 179 ----------- .../shared/ButtonWithTip/ButtonWithTip.css | 163 ---------- .../shared/ButtonWithTip/ButtonWithTip.jsx | 105 ------ .../shared/ChartPanel/ChartPanel.css | 17 - .../shared/ChartPanel/ChartPanel.jsx | 202 ------------ .../shared/ConfirmDialog/ConfirmDialog.jsx | 138 -------- .../shared/DetailDrawer/DetailDrawer.css | 119 ------- .../shared/DetailDrawer/DetailDrawer.tsx | 113 ------- .../ExtendInfoPanel/ExtendInfoPanel.css | 105 ------ .../ExtendInfoPanel/ExtendInfoPanel.jsx | 68 ---- .../components/shared/InfoPanel/InfoPanel.css | 96 ------ .../components/shared/InfoPanel/InfoPanel.jsx | 58 ---- .../shared/ListActionBar/ListActionBar.css | 96 ------ .../shared/ListActionBar/ListActionBar.jsx | 134 -------- .../components/shared/ListTable/ListTable.tsx | 3 +- .../shared/MainLayout/AppHeader.css | 154 --------- .../shared/MainLayout/AppHeader.jsx | 203 ------------ .../components/shared/MainLayout/AppSider.css | 96 ------ .../components/shared/MainLayout/AppSider.jsx | 195 ----------- .../shared/MainLayout/MainLayout.css | 32 -- .../shared/MainLayout/MainLayout.jsx | 31 -- .../src/components/shared/MainLayout/index.js | 4 - .../shared/ModernSidebar/ModernSidebar.css | 221 ------------- .../shared/ModernSidebar/ModernSidebar.jsx | 154 --------- .../components/shared/PDFViewer/PDFViewer.css | 62 ---- .../components/shared/PDFViewer/PDFViewer.jsx | 138 -------- .../shared/PDFViewer/VirtualPDFViewer.css | 132 -------- .../shared/PDFViewer/VirtualPDFViewer.jsx | 268 --------------- .../src/components/shared/ProtectedRoute.jsx | 14 - .../shared/SelectionAlert/SelectionAlert.css | 49 --- .../shared/SelectionAlert/SelectionAlert.jsx | 89 ----- .../shared/SideInfoPanel/SideInfoPanel.css | 88 ----- .../shared/SideInfoPanel/SideInfoPanel.jsx | 58 ---- .../shared/SplitLayout/SplitLayout.css | 72 ----- .../shared/SplitLayout/SplitLayout.jsx | 69 ---- .../components/shared/StatCard/StatCard.css | 108 ------- .../components/shared/StatCard/StatCard.tsx | 82 ----- .../src/components/shared/Toast/Toast.tsx | 81 ----- .../TreeFilterPanel/TreeFilterPanel.css | 58 ---- .../TreeFilterPanel/TreeFilterPanel.jsx | 119 ------- frontend/src/index.css | 8 + .../src/pages/access/permissions/index.tsx | 47 ++- frontend/src/utils/permission.ts | 5 - frontend/vite.config.ts | 22 ++ 54 files changed, 71 insertions(+), 6190 deletions(-) delete mode 100644 frontend/src/components/AppLayout.tsx delete mode 100644 frontend/src/components/shared/ActionHelpPanel/ActionHelpPanel.css delete mode 100644 frontend/src/components/shared/ActionHelpPanel/ActionHelpPanel.jsx delete mode 100644 frontend/src/components/shared/BottomHintBar/BottomHintBar.css delete mode 100644 frontend/src/components/shared/BottomHintBar/BottomHintBar.jsx delete mode 100644 frontend/src/components/shared/ButtonWithGuide/ButtonWithGuide.css delete mode 100644 frontend/src/components/shared/ButtonWithGuide/ButtonWithGuide.jsx delete mode 100644 frontend/src/components/shared/ButtonWithGuideBadge/ButtonWithGuideBadge.css delete mode 100644 frontend/src/components/shared/ButtonWithGuideBadge/ButtonWithGuideBadge.jsx delete mode 100644 frontend/src/components/shared/ButtonWithHoverCard/ButtonWithHoverCard.css delete mode 100644 frontend/src/components/shared/ButtonWithHoverCard/ButtonWithHoverCard.jsx delete mode 100644 frontend/src/components/shared/ButtonWithTip/ButtonWithTip.css delete mode 100644 frontend/src/components/shared/ButtonWithTip/ButtonWithTip.jsx delete mode 100644 frontend/src/components/shared/ChartPanel/ChartPanel.css delete mode 100644 frontend/src/components/shared/ChartPanel/ChartPanel.jsx delete mode 100644 frontend/src/components/shared/ConfirmDialog/ConfirmDialog.jsx delete mode 100644 frontend/src/components/shared/DetailDrawer/DetailDrawer.css delete mode 100644 frontend/src/components/shared/DetailDrawer/DetailDrawer.tsx delete mode 100644 frontend/src/components/shared/ExtendInfoPanel/ExtendInfoPanel.css delete mode 100644 frontend/src/components/shared/ExtendInfoPanel/ExtendInfoPanel.jsx delete mode 100644 frontend/src/components/shared/InfoPanel/InfoPanel.css delete mode 100644 frontend/src/components/shared/InfoPanel/InfoPanel.jsx delete mode 100644 frontend/src/components/shared/ListActionBar/ListActionBar.css delete mode 100644 frontend/src/components/shared/ListActionBar/ListActionBar.jsx delete mode 100644 frontend/src/components/shared/MainLayout/AppHeader.css delete mode 100644 frontend/src/components/shared/MainLayout/AppHeader.jsx delete mode 100644 frontend/src/components/shared/MainLayout/AppSider.css delete mode 100644 frontend/src/components/shared/MainLayout/AppSider.jsx delete mode 100644 frontend/src/components/shared/MainLayout/MainLayout.css delete mode 100644 frontend/src/components/shared/MainLayout/MainLayout.jsx delete mode 100644 frontend/src/components/shared/MainLayout/index.js delete mode 100644 frontend/src/components/shared/ModernSidebar/ModernSidebar.css delete mode 100644 frontend/src/components/shared/ModernSidebar/ModernSidebar.jsx delete mode 100644 frontend/src/components/shared/PDFViewer/PDFViewer.css delete mode 100644 frontend/src/components/shared/PDFViewer/PDFViewer.jsx delete mode 100644 frontend/src/components/shared/PDFViewer/VirtualPDFViewer.css delete mode 100644 frontend/src/components/shared/PDFViewer/VirtualPDFViewer.jsx delete mode 100644 frontend/src/components/shared/ProtectedRoute.jsx delete mode 100644 frontend/src/components/shared/SelectionAlert/SelectionAlert.css delete mode 100644 frontend/src/components/shared/SelectionAlert/SelectionAlert.jsx delete mode 100644 frontend/src/components/shared/SideInfoPanel/SideInfoPanel.css delete mode 100644 frontend/src/components/shared/SideInfoPanel/SideInfoPanel.jsx delete mode 100644 frontend/src/components/shared/SplitLayout/SplitLayout.css delete mode 100644 frontend/src/components/shared/SplitLayout/SplitLayout.jsx delete mode 100644 frontend/src/components/shared/StatCard/StatCard.css delete mode 100644 frontend/src/components/shared/StatCard/StatCard.tsx delete mode 100644 frontend/src/components/shared/Toast/Toast.tsx delete mode 100644 frontend/src/components/shared/TreeFilterPanel/TreeFilterPanel.css delete mode 100644 frontend/src/components/shared/TreeFilterPanel/TreeFilterPanel.jsx delete mode 100644 frontend/src/utils/permission.ts diff --git a/frontend/src/components/AppLayout.tsx b/frontend/src/components/AppLayout.tsx deleted file mode 100644 index c055848..0000000 --- a/frontend/src/components/AppLayout.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Layout, Menu, Space, Button } from "antd"; -import { Link, Outlet, useLocation, useNavigate } from "react-router-dom"; -import { useEffect, useState } from "react"; -import { fetchMyMenuTree } from "../api"; -import { PermissionNode } from "../types"; -import { LogoutOutlined, HomeOutlined, SettingOutlined, UserOutlined, SafetyOutlined, ClusterOutlined, BookOutlined, DesktopOutlined, ShopOutlined, InfoCircleOutlined, ApartmentOutlined } from "@ant-design/icons"; - -const iconMap: Record = { - 'home': , - 'tenant': , - 'org': , - 'user': , - 'role': , - 'permission': , - 'dict': , - 'device': , - 'setting': , - 'logs': -}; - -export default function AppLayout() { - const location = useLocation(); - const navigate = useNavigate(); - const [menuTree, setMenuTree] = useState([]); - const [loading, setLoading] = useState(false); - - useEffect(() => { - const loadMenu = async () => { - setLoading(true); - try { - const tree = await fetchMyMenuTree(); - setMenuTree(tree || []); - } catch (e) { - console.error("Failed to load menu tree", e); - } finally { - setLoading(false); - } - }; - loadMenu(); - }, []); - - const handleLogout = () => { - localStorage.removeItem("accessToken"); - localStorage.removeItem("refreshToken"); - navigate("/login"); - }; - - const renderMenuItems = (nodes: PermissionNode[]): any[] => { - return nodes - .filter(node => node.isVisible !== 0 && node.status !== 0) - .map(node => { - const icon = node.icon ? iconMap[node.icon] || : undefined; - - if (node.children && node.children.length > 0) { - return { - key: node.path || `parent-${node.permId}`, - icon: icon, - label: node.name, - children: renderMenuItems(node.children) - }; - } - - return { - key: node.path, - icon: icon, - label: node.path ? {node.name} : node.name - }; - }); - }; - - const menuItems = [ - { key: "/", label: 总览, icon: }, - ...renderMenuItems(menuTree), - { - key: "logout", - label: 退出登录, - icon: , - danger: true, - style: { marginTop: 'auto' } - } - ]; - - return ( - - -
- logo - MeetingAI -
- - - - - - - - - - - - - - ); -} \ No newline at end of file diff --git a/frontend/src/components/shared/ActionHelpPanel/ActionHelpPanel.css b/frontend/src/components/shared/ActionHelpPanel/ActionHelpPanel.css deleted file mode 100644 index bc039f7..0000000 --- a/frontend/src/components/shared/ActionHelpPanel/ActionHelpPanel.css +++ /dev/null @@ -1,259 +0,0 @@ -/* 帮助面板样式 */ -.action-help-panel .ant-drawer-header { - border-bottom: 2px solid #f0f0f0; -} - -.help-panel-title { - display: flex; - align-items: center; - gap: 12px; - font-size: 16px; - font-weight: 600; -} - -.help-panel-header { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; -} - -.help-panel-header-text { - font-weight: 500; - color: rgba(0, 0, 0, 0.88); -} - -/* 操作详情样式 */ -.help-action-detail { - display: flex; - flex-direction: column; - gap: 20px; -} - -.help-action-header { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 16px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 12px; - color: white; -} - -.help-action-icon { - font-size: 28px; - line-height: 1; - opacity: 0.95; -} - -.help-action-info { - flex: 1; - display: flex; - flex-direction: column; - gap: 6px; -} - -.help-action-title { - margin: 0; - font-size: 18px; - font-weight: 600; - color: white; -} - -.help-action-badge { - align-self: flex-start; - margin: 0; - font-size: 11px; - padding: 2px 8px; - border-radius: 10px; -} - -/* 帮助区块样式 */ -.help-section { - padding: 16px; - background: #f8f9fa; - border-radius: 8px; - border-left: 3px solid #1677ff; -} - -.help-section-warning { - background: #fff7e6; - border-left-color: #faad14; -} - -.help-section-title { - display: flex; - align-items: center; - gap: 6px; - font-size: 14px; - font-weight: 600; - color: rgba(0, 0, 0, 0.88); - margin-bottom: 12px; -} - -.help-section-content { - font-size: 13px; - line-height: 1.8; - color: rgba(0, 0, 0, 0.65); -} - -.help-section-list { - margin: 0; - padding-left: 20px; - list-style-type: disc; -} - -.help-section-list li { - font-size: 13px; - line-height: 1.8; - color: rgba(0, 0, 0, 0.65); - margin-bottom: 8px; -} - -.help-section-list li:last-child { - margin-bottom: 0; -} - -.help-section-steps { - margin: 0; - padding-left: 20px; - counter-reset: step-counter; - list-style: none; -} - -.help-section-steps li { - font-size: 13px; - line-height: 1.8; - color: rgba(0, 0, 0, 0.65); - margin-bottom: 12px; - padding-left: 12px; - position: relative; - counter-increment: step-counter; -} - -.help-section-steps li:before { - content: counter(step-counter); - position: absolute; - left: -20px; - top: 0; - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - background: #1677ff; - color: white; - border-radius: 50%; - font-size: 11px; - font-weight: 600; -} - -.help-section-steps li:last-child { - margin-bottom: 0; -} - -.help-shortcut { - display: inline-block; -} - -.help-shortcut kbd { - display: inline-block; - padding: 6px 12px; - background: linear-gradient(180deg, #ffffff 0%, #f0f0f0 100%); - border: 1px solid #d9d9d9; - border-radius: 6px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 -2px 0 rgba(0, 0, 0, 0.05); - font-size: 12px; - font-family: 'Monaco', 'Consolas', monospace; - color: rgba(0, 0, 0, 0.88); - font-weight: 500; -} - -/* 操作列表样式 */ -.help-actions-list { - display: flex; - flex-direction: column; - gap: 12px; -} - -.help-action-item { - padding: 12px; - background: white; - border: 1px solid #f0f0f0; - border-radius: 8px; - transition: all 0.3s ease; - cursor: pointer; -} - -.help-action-item:hover { - border-color: #1677ff; - box-shadow: 0 2px 8px rgba(22, 119, 255, 0.1); - transform: translateY(-2px); -} - -.help-action-item-header { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 6px; -} - -.help-action-item-icon { - font-size: 16px; - color: #1677ff; -} - -.help-action-item-title { - flex: 1; - font-size: 14px; - font-weight: 500; - color: rgba(0, 0, 0, 0.88); -} - -.help-action-item-shortcut { - padding: 2px 6px; - background: #f0f0f0; - border: 1px solid #d9d9d9; - border-radius: 4px; - font-size: 11px; - font-family: 'Monaco', 'Consolas', monospace; - color: rgba(0, 0, 0, 0.65); -} - -.help-action-item-desc { - font-size: 12px; - line-height: 1.6; - color: rgba(0, 0, 0, 0.45); - padding-left: 24px; -} - -/* 折叠面板自定义样式 */ -.action-help-panel .ant-collapse-ghost > .ant-collapse-item { - margin-bottom: 16px; -} - -.action-help-panel .ant-collapse-ghost > .ant-collapse-item > .ant-collapse-header { - padding: 12px 16px; - background: #fafafa; - border-radius: 8px; - font-weight: 500; -} - -.action-help-panel .ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content { - padding-top: 12px; -} - -/* 响应式调整 */ -@media (max-width: 768px) { - .action-help-panel .ant-drawer-content-wrapper { - width: 100% !important; - } - - .help-action-header { - padding: 12px; - } - - .help-section { - padding: 12px; - } -} diff --git a/frontend/src/components/shared/ActionHelpPanel/ActionHelpPanel.jsx b/frontend/src/components/shared/ActionHelpPanel/ActionHelpPanel.jsx deleted file mode 100644 index a3cde11..0000000 --- a/frontend/src/components/shared/ActionHelpPanel/ActionHelpPanel.jsx +++ /dev/null @@ -1,228 +0,0 @@ -import { useState, useEffect } from 'react' -import { Drawer, Collapse, Badge, Tag, Empty } from 'antd' -import { - QuestionCircleOutlined, - BulbOutlined, - WarningOutlined, - InfoCircleOutlined, - ThunderboltOutlined, -} from '@ant-design/icons' -import './ActionHelpPanel.css' - -const { Panel } = Collapse - -/** - * 操作帮助面板组件 - * 在页面侧边显示当前操作的详细说明和帮助信息 - * @param {Object} props - * @param {boolean} props.visible - 是否显示面板 - * @param {Function} props.onClose - 关闭回调 - * @param {Object} props.currentAction - 当前操作信息 - * @param {Array} props.allActions - 所有可用操作列表 - * @param {string} props.placement - 面板位置 - * @param {Function} props.onActionSelect - 选择操作的回调 - */ -function ActionHelpPanel({ - visible = false, - onClose, - currentAction = null, - allActions = [], - placement = 'right', - onActionSelect, -}) { - const [activeKey, setActiveKey] = useState(['current']) - - // 当 currentAction 变化时,自动展开"当前操作"面板 - useEffect(() => { - if (currentAction && visible) { - setActiveKey(['current']) - } - }, [currentAction, visible]) - - // 渲染当前操作详情 - const renderCurrentAction = () => { - if (!currentAction) { - return ( - - ) - } - - return ( -
- {/* 操作标题 */} -
-
{currentAction.icon}
-
-

{currentAction.title}

- {currentAction.badge && ( - - {currentAction.badge.text} - - )} -
-
- - {/* 操作描述 */} - {currentAction.description && ( -
-
- 功能说明 -
-
{currentAction.description}
-
- )} - - {/* 使用场景 */} - {currentAction.scenarios && currentAction.scenarios.length > 0 && ( -
-
- 使用场景 -
-
    - {currentAction.scenarios.map((scenario, index) => ( -
  • {scenario}
  • - ))} -
-
- )} - - {/* 操作步骤 */} - {currentAction.steps && currentAction.steps.length > 0 && ( -
-
- 操作步骤 -
-
    - {currentAction.steps.map((step, index) => ( -
  1. {step}
  2. - ))} -
-
- )} - - {/* 注意事项 */} - {currentAction.warnings && currentAction.warnings.length > 0 && ( -
-
- 注意事项 -
-
    - {currentAction.warnings.map((warning, index) => ( -
  • {warning}
  • - ))} -
-
- )} - - {/* 快捷键 */} - {currentAction.shortcut && ( -
-
⌨️ 快捷键
-
- {currentAction.shortcut} -
-
- )} - - {/* 权限要求 */} - {currentAction.permission && ( -
-
🔐 权限要求
-
- {currentAction.permission} -
-
- )} -
- ) - } - - // 渲染所有操作列表 - const renderAllActions = () => { - if (allActions.length === 0) { - return - } - - return ( -
- {allActions.map((action, index) => ( -
{ - if (onActionSelect) { - onActionSelect(action) - setActiveKey(['current']) - } - }} - > -
- {action.icon} - {action.title} - {action.shortcut && ( - {action.shortcut} - )} -
-
{action.description}
-
- ))} -
- ) - } - - return ( - - - 操作帮助 - {currentAction && } - - } - placement={placement} - width={420} - open={visible} - onClose={onClose} - className="action-help-panel" - > - - - 当前操作 - {currentAction && ( - - )} - - } - key="current" - > - {renderCurrentAction()} - - - - {renderAllActions()} - - - - ) -} - -export default ActionHelpPanel diff --git a/frontend/src/components/shared/BottomHintBar/BottomHintBar.css b/frontend/src/components/shared/BottomHintBar/BottomHintBar.css deleted file mode 100644 index 36de3a3..0000000 --- a/frontend/src/components/shared/BottomHintBar/BottomHintBar.css +++ /dev/null @@ -1,304 +0,0 @@ -/* 底部提示栏基础样式 */ -.bottom-hint-bar { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: 9999; - padding: 12px 24px; - box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1); - animation: slideUp 0.3s ease; -} - -@keyframes slideUp { - from { - transform: translateY(100%); - opacity: 0; - } - to { - transform: translateY(0); - opacity: 1; - } -} - -/* 主题样式 */ -.bottom-hint-bar-light { - background: #ffffff; - border-top: 1px solid #f0f0f0; -} - -.bottom-hint-bar-dark { - background: #001529; - color: #ffffff; -} - -.bottom-hint-bar-gradient { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: #ffffff; -} - -/* 容器布局 */ -.hint-bar-container { - display: flex; - align-items: center; - gap: 24px; - max-width: 1400px; - margin: 0 auto; -} - -/* 左侧区域 */ -.hint-bar-left { - display: flex; - align-items: center; - gap: 12px; - flex-shrink: 0; -} - -.hint-bar-icon { - font-size: 24px; - opacity: 0.9; -} - -.bottom-hint-bar-light .hint-bar-icon { - color: #1677ff; -} - -.hint-bar-title-section { - display: flex; - flex-direction: column; - gap: 4px; -} - -.hint-bar-title { - margin: 0; - font-size: 15px; - font-weight: 600; - line-height: 1.2; -} - -.bottom-hint-bar-light .hint-bar-title { - color: rgba(0, 0, 0, 0.88); -} - -.hint-bar-badge { - margin: 0; - font-size: 10px; - padding: 1px 6px; - align-self: flex-start; -} - -/* 中间区域 */ -.hint-bar-center { - flex: 1; - display: flex; - align-items: center; - gap: 24px; - flex-wrap: wrap; -} - -.hint-bar-description, -.hint-bar-quick-tip, -.hint-bar-warning { - display: flex; - align-items: center; - gap: 8px; - font-size: 13px; - line-height: 1.4; -} - -.bottom-hint-bar-light .hint-bar-description, -.bottom-hint-bar-light .hint-bar-quick-tip { - color: rgba(0, 0, 0, 0.65); -} - -.hint-info-icon { - font-size: 14px; - opacity: 0.8; -} - -.bottom-hint-bar-light .hint-info-icon { - color: #1677ff; -} - -.hint-tip-icon { - font-size: 14px; - color: #fadb14; -} - -.hint-warning-icon { - font-size: 14px; - color: #ff7a45; -} - -.bottom-hint-bar-light .hint-bar-warning { - color: #d46b08; -} - -/* 右侧区域 */ -.hint-bar-right { - display: flex; - align-items: center; - gap: 16px; - flex-shrink: 0; -} - -.hint-bar-shortcut { - display: flex; - align-items: center; - gap: 8px; -} - -.shortcut-label { - font-size: 11px; - opacity: 0.7; -} - -.shortcut-kbd { - display: inline-block; - padding: 4px 10px; - background: rgba(255, 255, 255, 0.2); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 4px; - font-size: 11px; - font-family: 'Monaco', 'Consolas', monospace; - color: inherit; - font-weight: 500; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.bottom-hint-bar-light .shortcut-kbd { - background: #f0f0f0; - border-color: #d9d9d9; - color: rgba(0, 0, 0, 0.88); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 -2px 0 rgba(0, 0, 0, 0.05); -} - -.hint-bar-close { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 4px; - color: inherit; - cursor: pointer; - transition: all 0.3s ease; -} - -.hint-bar-close:hover { - background: rgba(255, 255, 255, 0.2); - transform: scale(1.05); -} - -.bottom-hint-bar-light .hint-bar-close { - background: #f0f0f0; - border-color: #d9d9d9; - color: rgba(0, 0, 0, 0.45); -} - -.bottom-hint-bar-light .hint-bar-close:hover { - background: #e0e0e0; - color: rgba(0, 0, 0, 0.88); -} - -/* 进度指示条 */ -.hint-bar-progress { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 2px; - background: rgba(255, 255, 255, 0.3); - overflow: hidden; -} - -.hint-bar-progress::after { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(255, 255, 255, 0.6); - animation: progressWave 3s ease-in-out infinite; -} - -.bottom-hint-bar-light .hint-bar-progress { - background: #f0f0f0; -} - -.bottom-hint-bar-light .hint-bar-progress::after { - background: #1677ff; -} - -@keyframes progressWave { - 0%, 100% { - transform: translateX(-100%); - } - 50% { - transform: translateX(0); - } -} - -/* 响应式调整 */ -@media (max-width: 1024px) { - .hint-bar-container { - flex-wrap: wrap; - gap: 12px; - } - - .hint-bar-center { - flex-basis: 100%; - order: 3; - gap: 12px; - } - - .hint-bar-description, - .hint-bar-quick-tip, - .hint-bar-warning { - font-size: 12px; - } -} - -@media (max-width: 768px) { - .bottom-hint-bar { - padding: 10px 16px; - } - - .hint-bar-left { - gap: 8px; - } - - .hint-bar-icon { - font-size: 20px; - } - - .hint-bar-title { - font-size: 14px; - } - - .hint-bar-right { - gap: 8px; - } - - .shortcut-label { - display: none; - } - - .hint-bar-close { - width: 24px; - height: 24px; - } -} - -@media (max-width: 480px) { - .hint-bar-quick-tip { - display: none; - } - - .hint-bar-warning { - flex-basis: 100%; - } -} diff --git a/frontend/src/components/shared/BottomHintBar/BottomHintBar.jsx b/frontend/src/components/shared/BottomHintBar/BottomHintBar.jsx deleted file mode 100644 index 9efe937..0000000 --- a/frontend/src/components/shared/BottomHintBar/BottomHintBar.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Tag } from 'antd' -import { - InfoCircleOutlined, - BulbOutlined, - WarningOutlined, - CloseOutlined, -} from '@ant-design/icons' -import './BottomHintBar.css' - -/** - * 底部固定提示栏组件 - * 在页面底部显示当前悬停按钮的实时说明 - * @param {Object} props - * @param {boolean} props.visible - 是否显示提示栏 - * @param {Object} props.hintInfo - 当前提示信息 - * @param {Function} props.onClose - 关闭回调 - * @param {string} props.theme - 主题:light, dark, gradient - */ -function BottomHintBar({ visible = false, hintInfo = null, onClose, theme = 'gradient' }) { - if (!visible || !hintInfo) return null - - return ( -
e.stopPropagation()} - > -
- {/* 左侧:图标和标题 */} -
-
{hintInfo.icon}
-
-

{hintInfo.title}

- {hintInfo.badge && ( - - {hintInfo.badge.text} - - )} -
-
- - {/* 中间:主要信息 */} -
- {/* 描述 */} - {hintInfo.description && ( -
- - {hintInfo.description} -
- )} - - {/* 快速提示 */} - {hintInfo.quickTip && ( -
- - {hintInfo.quickTip} -
- )} - - {/* 警告 */} - {hintInfo.warning && ( -
- - {hintInfo.warning} -
- )} -
- - {/* 右侧:快捷键和关闭 */} -
- {hintInfo.shortcut && ( -
- 快捷键 - {hintInfo.shortcut} -
- )} - {onClose && ( - - )} -
-
- - {/* 进度指示条 */} -
-
- ) -} - -export default BottomHintBar diff --git a/frontend/src/components/shared/ButtonWithGuide/ButtonWithGuide.css b/frontend/src/components/shared/ButtonWithGuide/ButtonWithGuide.css deleted file mode 100644 index 0968427..0000000 --- a/frontend/src/components/shared/ButtonWithGuide/ButtonWithGuide.css +++ /dev/null @@ -1,196 +0,0 @@ -/* 按钮带引导 - 简洁现代设计 */ -.button-with-guide { - display: inline-flex; - align-items: center; - gap: 4px; -} - -/* 帮助图标按钮 - 简洁扁平设计 */ -.guide-icon-btn { - display: inline-flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - padding: 0; - background: transparent; - border: none; - border-radius: 4px; - color: rgba(0, 0, 0, 0.35); - font-size: 14px; - cursor: pointer; - transition: all 0.2s ease; -} - -.guide-icon-btn:hover { - background: rgba(22, 119, 255, 0.06); - color: #1677ff; -} - -.guide-icon-btn:active { - background: rgba(22, 119, 255, 0.12); -} - -/* 引导弹窗样式 */ -.button-guide-modal .ant-modal-header { - padding: 20px 24px; - border-bottom: 2px solid #f0f0f0; -} - -.button-guide-modal .ant-modal-body { - padding: 24px; - max-height: 600px; - overflow-y: auto; -} - -.guide-modal-header { - display: flex; - align-items: center; - gap: 12px; -} - -.guide-modal-icon { - font-size: 24px; - color: #1677ff; -} - -.guide-modal-title { - font-size: 18px; - font-weight: 600; - color: rgba(0, 0, 0, 0.88); -} - -.guide-modal-badge { - margin: 0; - font-size: 11px; - padding: 2px 8px; -} - -/* 引导区块样式 */ -.guide-section { - margin-bottom: 20px; - padding: 16px; - background: #f8f9fa; - border-radius: 8px; - border-left: 3px solid #1677ff; -} - -.guide-section:last-child { - margin-bottom: 0; -} - -.guide-section-warning { - background: #fff7e6; - border-left-color: #faad14; -} - -.guide-section-title { - display: flex; - align-items: center; - gap: 8px; - font-size: 14px; - font-weight: 600; - color: rgba(0, 0, 0, 0.88); - margin-bottom: 12px; -} - -.guide-section-icon { - font-size: 16px; - color: #1677ff; -} - -.guide-section-warning .guide-section-icon { - color: #faad14; -} - -.guide-section-content { - margin: 0; - font-size: 14px; - line-height: 1.8; - color: rgba(0, 0, 0, 0.65); -} - -.guide-list { - margin: 0; - padding-left: 20px; - list-style-type: disc; -} - -.guide-list li { - font-size: 13px; - line-height: 1.8; - color: rgba(0, 0, 0, 0.65); - margin-bottom: 8px; -} - -.guide-list li:last-child { - margin-bottom: 0; -} - -/* 步骤样式 */ -.guide-steps { - margin-top: 12px; -} - -.guide-steps .ant-steps-item-title { - font-size: 13px !important; - font-weight: 600 !important; -} - -.guide-steps .ant-steps-item-description { - font-size: 13px !important; - line-height: 1.6 !important; - color: rgba(0, 0, 0, 0.65) !important; -} - -/* 引导底部 */ -.guide-footer { - display: flex; - flex-wrap: wrap; - gap: 16px; - margin-top: 20px; - padding: 16px; - background: white; - border-radius: 8px; - border: 1px solid #f0f0f0; -} - -.guide-footer-item { - display: flex; - align-items: center; - gap: 8px; -} - -.guide-footer-label { - font-size: 13px; - color: rgba(0, 0, 0, 0.65); -} - -.guide-footer-kbd { - display: inline-block; - padding: 4px 10px; - background: linear-gradient(180deg, #ffffff 0%, #f0f0f0 100%); - border: 1px solid #d9d9d9; - border-radius: 6px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 -2px 0 rgba(0, 0, 0, 0.05); - font-size: 11px; - font-family: 'Monaco', 'Consolas', monospace; - color: rgba(0, 0, 0, 0.88); - font-weight: 500; -} - -/* 响应式调整 */ -@media (max-width: 768px) { - .button-guide-modal { - max-width: calc(100% - 32px); - } - - .button-guide-modal .ant-modal-body { - max-height: 500px; - } - - .guide-footer { - flex-direction: column; - gap: 12px; - } -} diff --git a/frontend/src/components/shared/ButtonWithGuide/ButtonWithGuide.jsx b/frontend/src/components/shared/ButtonWithGuide/ButtonWithGuide.jsx deleted file mode 100644 index 0f30cc3..0000000 --- a/frontend/src/components/shared/ButtonWithGuide/ButtonWithGuide.jsx +++ /dev/null @@ -1,165 +0,0 @@ -import { useState } from 'react' -import { Button, Modal, Steps, Tag } from 'antd' -import { - QuestionCircleOutlined, - BulbOutlined, - WarningOutlined, - CheckCircleOutlined, - InfoCircleOutlined, -} from '@ant-design/icons' -import './ButtonWithGuide.css' - -/** - * 带引导的按钮组件 - 简洁现代设计 - * 在按钮旁边显示一个简洁的帮助图标,点击后显示详细引导 - */ -function ButtonWithGuide({ - label, - icon, - type = 'default', - danger = false, - disabled = false, - onClick, - guide, - size = 'middle', - ...restProps -}) { - const [showGuideModal, setShowGuideModal] = useState(false) - - const handleGuideClick = (e) => { - e.stopPropagation() - if (guide) { - setShowGuideModal(true) - } - } - - return ( - <> -
- - {guide && !disabled && ( - - )} -
- - {/* 引导弹窗 */} - {guide && ( - - {guide.icon || icon} - {guide.title} - {guide.badge && ( - - {guide.badge.text} - - )} -
- } - open={showGuideModal} - onCancel={() => setShowGuideModal(false)} - footer={[ - , - ]} - width={600} - className="button-guide-modal" - > - {/* 功能描述 */} - {guide.description && ( -
-
- - 功能说明 -
-

{guide.description}

-
- )} - - {/* 使用步骤 */} - {guide.steps && guide.steps.length > 0 && ( -
-
- - 操作步骤 -
- ({ - title: `步骤 ${index + 1}`, - description: step, - status: 'wait', - }))} - className="guide-steps" - /> -
- )} - - {/* 使用场景 */} - {guide.scenarios && guide.scenarios.length > 0 && ( -
-
- - 适用场景 -
-
    - {guide.scenarios.map((scenario, index) => ( -
  • {scenario}
  • - ))} -
-
- )} - - {/* 注意事项 */} - {guide.warnings && guide.warnings.length > 0 && ( -
-
- - 注意事项 -
-
    - {guide.warnings.map((warning, index) => ( -
  • {warning}
  • - ))} -
-
- )} - - {/* 快捷键和权限 */} - {(guide.shortcut || guide.permission) && ( -
- {guide.shortcut && ( -
- 快捷键: - {guide.shortcut} -
- )} - {guide.permission && ( -
- 权限要求: - {guide.permission} -
- )} -
- )} - - )} - - ) -} - -export default ButtonWithGuide diff --git a/frontend/src/components/shared/ButtonWithGuideBadge/ButtonWithGuideBadge.css b/frontend/src/components/shared/ButtonWithGuideBadge/ButtonWithGuideBadge.css deleted file mode 100644 index 334f120..0000000 --- a/frontend/src/components/shared/ButtonWithGuideBadge/ButtonWithGuideBadge.css +++ /dev/null @@ -1,243 +0,0 @@ -.button-guide-badge-wrapper { - display: inline-block; - position: relative; -} - -/* 引导徽章样式 - 改为放在右上角外部 */ -.button-guide-badge-wrapper .ant-badge { - display: block; -} - -.button-guide-badge-wrapper .ant-badge-count { - top: -8px; - right: -8px; - transform: none; -} - -/* 引导徽章样式 */ -.guide-badge { - display: flex; - align-items: center; - justify-content: center; - min-width: 20px; - height: 20px; - padding: 0 6px; - background: #1677ff; - border-radius: 10px; - color: white; - font-size: 12px; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; - animation: pulseBadge 2s ease-in-out infinite; - box-shadow: 0 2px 8px rgba(22, 119, 255, 0.4); - border: 2px solid white; -} - -.guide-badge:hover { - animation: none; - transform: scale(1.2); - box-shadow: 0 4px 12px rgba(22, 119, 255, 0.6); -} - -.guide-badge-new { - background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); - box-shadow: 0 2px 8px rgba(82, 196, 26, 0.4); -} - -.guide-badge-new:hover { - box-shadow: 0 4px 12px rgba(82, 196, 26, 0.6); -} - -.guide-badge-help { - background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%); - box-shadow: 0 2px 8px rgba(22, 119, 255, 0.4); -} - -.guide-badge-help:hover { - box-shadow: 0 4px 12px rgba(22, 119, 255, 0.6); -} - -.guide-badge-warn { - background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%); - box-shadow: 0 2px 8px rgba(250, 173, 20, 0.4); -} - -.guide-badge-warn:hover { - box-shadow: 0 4px 12px rgba(250, 173, 20, 0.6); -} - -@keyframes pulseBadge { - 0%, 100% { - transform: scale(1); - opacity: 1; - } - 50% { - transform: scale(1.15); - opacity: 0.8; - } -} - -/* 引导弹窗样式 */ -.button-guide-modal .ant-modal-header { - padding: 20px 24px; - border-bottom: 2px solid #f0f0f0; -} - -.button-guide-modal .ant-modal-body { - padding: 24px; - max-height: 600px; - overflow-y: auto; -} - -.guide-modal-header { - display: flex; - align-items: center; - gap: 12px; -} - -.guide-modal-icon { - font-size: 24px; - color: #1677ff; -} - -.guide-modal-title { - font-size: 18px; - font-weight: 600; - color: rgba(0, 0, 0, 0.88); -} - -.guide-modal-badge { - margin: 0; - font-size: 11px; - padding: 2px 8px; -} - -/* 引导区块样式 */ -.guide-section { - margin-bottom: 20px; - padding: 16px; - background: #f8f9fa; - border-radius: 8px; - border-left: 3px solid #1677ff; -} - -.guide-section:last-child { - margin-bottom: 0; -} - -.guide-section-warning { - background: #fff7e6; - border-left-color: #faad14; -} - -.guide-section-title { - display: flex; - align-items: center; - gap: 8px; - font-size: 14px; - font-weight: 600; - color: rgba(0, 0, 0, 0.88); - margin-bottom: 12px; -} - -.guide-section-icon { - font-size: 16px; - color: #1677ff; -} - -.guide-section-warning .guide-section-icon { - color: #faad14; -} - -.guide-section-content { - margin: 0; - font-size: 14px; - line-height: 1.8; - color: rgba(0, 0, 0, 0.65); -} - -.guide-list { - margin: 0; - padding-left: 20px; - list-style-type: disc; -} - -.guide-list li { - font-size: 13px; - line-height: 1.8; - color: rgba(0, 0, 0, 0.65); - margin-bottom: 8px; -} - -.guide-list li:last-child { - margin-bottom: 0; -} - -/* 步骤样式 */ -.guide-steps { - margin-top: 12px; -} - -.guide-steps .ant-steps-item-title { - font-size: 13px !important; - font-weight: 600 !important; -} - -.guide-steps .ant-steps-item-description { - font-size: 13px !important; - line-height: 1.6 !important; - color: rgba(0, 0, 0, 0.65) !important; -} - -/* 引导底部 */ -.guide-footer { - display: flex; - flex-wrap: wrap; - gap: 16px; - margin-top: 20px; - padding: 16px; - background: white; - border-radius: 8px; - border: 1px solid #f0f0f0; -} - -.guide-footer-item { - display: flex; - align-items: center; - gap: 8px; -} - -.guide-footer-label { - font-size: 13px; - color: rgba(0, 0, 0, 0.65); -} - -.guide-footer-kbd { - display: inline-block; - padding: 4px 10px; - background: linear-gradient(180deg, #ffffff 0%, #f0f0f0 100%); - border: 1px solid #d9d9d9; - border-radius: 6px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 -2px 0 rgba(0, 0, 0, 0.05); - font-size: 11px; - font-family: 'Monaco', 'Consolas', monospace; - color: rgba(0, 0, 0, 0.88); - font-weight: 500; -} - -/* 响应式调整 */ -@media (max-width: 768px) { - .button-guide-modal { - max-width: calc(100% - 32px); - } - - .button-guide-modal .ant-modal-body { - max-height: 500px; - } - - .guide-footer { - flex-direction: column; - gap: 12px; - } -} diff --git a/frontend/src/components/shared/ButtonWithGuideBadge/ButtonWithGuideBadge.jsx b/frontend/src/components/shared/ButtonWithGuideBadge/ButtonWithGuideBadge.jsx deleted file mode 100644 index 35a8224..0000000 --- a/frontend/src/components/shared/ButtonWithGuideBadge/ButtonWithGuideBadge.jsx +++ /dev/null @@ -1,222 +0,0 @@ -import { useState } from 'react' -import { Button, Badge, Modal, Steps, Tag, Divider } from 'antd' -import { - QuestionCircleOutlined, - BulbOutlined, - WarningOutlined, - CheckCircleOutlined, - InfoCircleOutlined, -} from '@ant-design/icons' -import './ButtonWithGuideBadge.css' - -/** - * 智能引导徽章按钮组件 - * 为新功能或复杂按钮添加脉冲动画的徽章,点击后显示详细引导 - * @param {Object} props - * @param {string} props.label - 按钮文本 - * @param {ReactNode} props.icon - 按钮图标 - * @param {string} props.type - 按钮类型 - * @param {boolean} props.danger - 危险按钮 - * @param {boolean} props.disabled - 禁用状态 - * @param {Function} props.onClick - 点击回调 - * @param {Object} props.guide - 引导配置 - * @param {boolean} props.showBadge - 是否显示徽章 - * @param {string} props.badgeType - 徽章类型:new, help, warn - * @param {string} props.size - 按钮大小 - */ -function ButtonWithGuideBadge({ - label, - icon, - type = 'default', - danger = false, - disabled = false, - onClick, - guide, - showBadge = true, - badgeType = 'help', - size = 'middle', - ...restProps -}) { - const [showGuideModal, setShowGuideModal] = useState(false) - - const handleBadgeClick = (e) => { - e.stopPropagation() - if (guide) { - setShowGuideModal(true) - } - } - - const getBadgeConfig = () => { - const configs = { - new: { - text: 'NEW', - color: '#52c41a', - icon: , - }, - help: { - text: '?', - color: '#1677ff', - icon: , - }, - warn: { - text: '!', - color: '#faad14', - icon: , - }, - } - return configs[badgeType] || configs.help - } - - const badgeConfig = getBadgeConfig() - - return ( - <> -
- {showBadge && guide && !disabled ? ( - - {badgeConfig.icon} -
- } - offset={[-5, 5]} - > - - - ) : ( - - )} - - - {/* 引导弹窗 */} - {guide && ( - - {guide.icon || icon} - {guide.title} - {guide.badge && ( - - {guide.badge.text} - - )} - - } - open={showGuideModal} - onCancel={() => setShowGuideModal(false)} - footer={[ - , - ]} - width={600} - className="button-guide-modal" - > - {/* 功能描述 */} - {guide.description && ( -
-
- - 功能说明 -
-

{guide.description}

-
- )} - - {/* 使用步骤 */} - {guide.steps && guide.steps.length > 0 && ( -
-
- - 操作步骤 -
- ({ - title: `步骤 ${index + 1}`, - description: step, - status: 'wait', - }))} - className="guide-steps" - /> -
- )} - - {/* 使用场景 */} - {guide.scenarios && guide.scenarios.length > 0 && ( -
-
- - 适用场景 -
-
    - {guide.scenarios.map((scenario, index) => ( -
  • {scenario}
  • - ))} -
-
- )} - - {/* 注意事项 */} - {guide.warnings && guide.warnings.length > 0 && ( -
-
- - 注意事项 -
-
    - {guide.warnings.map((warning, index) => ( -
  • {warning}
  • - ))} -
-
- )} - - {/* 快捷键和权限 */} - {(guide.shortcut || guide.permission) && ( -
- {guide.shortcut && ( -
- 快捷键: - {guide.shortcut} -
- )} - {guide.permission && ( -
- 权限要求: - {guide.permission} -
- )} -
- )} -
- )} - - ) -} - -export default ButtonWithGuideBadge diff --git a/frontend/src/components/shared/ButtonWithHoverCard/ButtonWithHoverCard.css b/frontend/src/components/shared/ButtonWithHoverCard/ButtonWithHoverCard.css deleted file mode 100644 index eb42451..0000000 --- a/frontend/src/components/shared/ButtonWithHoverCard/ButtonWithHoverCard.css +++ /dev/null @@ -1,189 +0,0 @@ -.button-hover-card-wrapper { - display: inline-block; - position: relative; -} - -/* 悬浮卡片 */ -.hover-info-card { - position: fixed; - z-index: 10000; - transform: translateY(-50%); - opacity: 0; - animation: slideInRight 0.3s ease forwards; - pointer-events: none; -} - -.hover-info-card-visible { - opacity: 1; -} - -@keyframes slideInRight { - from { - opacity: 0; - transform: translateY(-50%) translateX(-20px); - } - to { - opacity: 1; - transform: translateY(-50%) translateX(0); - } -} - -.hover-info-card-content { - width: 340px; - background: white; - border-radius: 12px; - box-shadow: - 0 12px 28px rgba(0, 0, 0, 0.12), - 0 6px 12px rgba(0, 0, 0, 0.08), - 0 0 2px rgba(0, 0, 0, 0.04); - overflow: hidden; -} - -.hover-info-card-content .ant-card-body { - padding: 16px; -} - -/* 卡片头部 */ -.hover-card-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - margin-bottom: 12px; - padding-bottom: 12px; - border-bottom: 1px solid #f0f0f0; -} - -.hover-card-title-wrapper { - display: flex; - align-items: center; - gap: 8px; - flex: 1; -} - -.hover-card-icon { - font-size: 20px; - color: #1677ff; -} - -.hover-card-title { - margin: 0; - font-size: 16px; - font-weight: 600; - color: rgba(0, 0, 0, 0.88); -} - -.hover-card-badge { - margin: 0; - font-size: 11px; - padding: 2px 8px; - border-radius: 10px; -} - -/* 卡片描述 */ -.hover-card-description { - margin: 0; - font-size: 13px; - line-height: 1.6; - color: rgba(0, 0, 0, 0.65); -} - -/* 卡片区块 */ -.hover-card-section { - margin-top: 12px; - padding: 10px; - background: #f8f9fa; - border-radius: 8px; - border-left: 3px solid #1677ff; -} - -.hover-card-warning { - background: #fff7e6; - border-left-color: #faad14; -} - -.hover-card-section-title { - display: flex; - align-items: center; - gap: 6px; - font-size: 12px; - font-weight: 600; - color: rgba(0, 0, 0, 0.88); - margin-bottom: 8px; -} - -.section-icon { - font-size: 12px; - color: #1677ff; -} - -.hover-card-warning .section-icon { - color: #faad14; -} - -.hover-card-list { - margin: 0; - padding-left: 16px; - list-style-type: disc; -} - -.hover-card-list li { - font-size: 12px; - line-height: 1.6; - color: rgba(0, 0, 0, 0.65); - margin-bottom: 4px; -} - -.hover-card-list li:last-child { - margin-bottom: 0; -} - -/* 卡片底部 */ -.hover-card-footer { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 12px; - padding-top: 12px; - border-top: 1px solid #f0f0f0; -} - -.footer-label { - font-size: 12px; - color: rgba(0, 0, 0, 0.45); -} - -.footer-kbd { - display: inline-block; - padding: 4px 10px; - background: linear-gradient(180deg, #ffffff 0%, #f0f0f0 100%); - border: 1px solid #d9d9d9; - border-radius: 6px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 -2px 0 rgba(0, 0, 0, 0.05); - font-size: 11px; - font-family: 'Monaco', 'Consolas', monospace; - color: rgba(0, 0, 0, 0.88); - font-weight: 500; -} - -/* 响应式调整 */ -@media (max-width: 768px) { - .hover-info-card-content { - width: 280px; - } - - .hover-info-card { - left: 50% !important; - transform: translateX(-50%) translateY(-50%); - } - - @keyframes slideInRight { - from { - opacity: 0; - transform: translateX(-50%) translateY(-50%) scale(0.95); - } - to { - opacity: 1; - transform: translateX(-50%) translateY(-50%) scale(1); - } - } -} diff --git a/frontend/src/components/shared/ButtonWithHoverCard/ButtonWithHoverCard.jsx b/frontend/src/components/shared/ButtonWithHoverCard/ButtonWithHoverCard.jsx deleted file mode 100644 index 434cc27..0000000 --- a/frontend/src/components/shared/ButtonWithHoverCard/ButtonWithHoverCard.jsx +++ /dev/null @@ -1,179 +0,0 @@ -import { useState, useRef } from 'react' -import { createPortal } from 'react-dom' -import { Button, Card, Tag } from 'antd' -import { - BulbOutlined, - WarningOutlined, - ThunderboltOutlined, -} from '@ant-design/icons' -import './ButtonWithHoverCard.css' - -/** - * 悬浮展开卡片按钮组件 - * 鼠标悬停时,在按钮旁边展开一个精美的信息卡片 - * @param {Object} props - * @param {string} props.label - 按钮文本 - * @param {ReactNode} props.icon - 按钮图标 - * @param {string} props.type - 按钮类型 - * @param {boolean} props.danger - 危险按钮 - * @param {boolean} props.disabled - 禁用状态 - * @param {Function} props.onClick - 点击回调 - * @param {Object} props.cardInfo - 卡片信息配置 - * @param {string} props.size - 按钮大小 - */ -function ButtonWithHoverCard({ - label, - icon, - type = 'default', - danger = false, - disabled = false, - onClick, - cardInfo, - size = 'middle', - ...restProps -}) { - const [showCard, setShowCard] = useState(false) - const [cardPosition, setCardPosition] = useState({ top: 0, left: 0 }) - const wrapperRef = useRef(null) - - const handleMouseEnter = () => { - if (!cardInfo || disabled) return - - if (wrapperRef.current) { - const rect = wrapperRef.current.getBoundingClientRect() - setCardPosition({ - top: rect.top + rect.height / 2, - left: rect.right + 12, - }) - } - setShowCard(true) - } - - const handleMouseLeave = () => { - setShowCard(false) - } - - // 渲染悬浮卡片 - const renderCard = () => { - if (!showCard || !cardInfo) return null - - return ( -
- - {/* 标题区 */} -
-
- {cardInfo.icon && ( - {cardInfo.icon} - )} -

{cardInfo.title}

-
- {cardInfo.badge && ( - - {cardInfo.badge.text} - - )} -
- - {/* 描述 */} - {cardInfo.description && ( -
-

{cardInfo.description}

-
- )} - - {/* 使用场景 */} - {cardInfo.scenarios && cardInfo.scenarios.length > 0 && ( -
-
- - 使用场景 -
-
    - {cardInfo.scenarios.slice(0, 2).map((scenario, index) => ( -
  • {scenario}
  • - ))} -
-
- )} - - {/* 快速提示 */} - {cardInfo.quickTips && cardInfo.quickTips.length > 0 && ( -
-
- - 快速提示 -
-
    - {cardInfo.quickTips.map((tip, index) => ( -
  • {tip}
  • - ))} -
-
- )} - - {/* 注意事项 */} - {cardInfo.warnings && cardInfo.warnings.length > 0 && ( -
-
- - 注意 -
-
    - {cardInfo.warnings.slice(0, 2).map((warning, index) => ( -
  • {warning}
  • - ))} -
-
- )} - - {/* 快捷键 */} - {cardInfo.shortcut && ( -
- 快捷键 - {cardInfo.shortcut} -
- )} -
-
- ) - } - - return ( - <> -
- -
- - {/* 使用 Portal 渲染悬浮卡片到 body */} - {typeof document !== 'undefined' && createPortal(renderCard(), document.body)} - - ) -} - -export default ButtonWithHoverCard diff --git a/frontend/src/components/shared/ButtonWithTip/ButtonWithTip.css b/frontend/src/components/shared/ButtonWithTip/ButtonWithTip.css deleted file mode 100644 index f1d665a..0000000 --- a/frontend/src/components/shared/ButtonWithTip/ButtonWithTip.css +++ /dev/null @@ -1,163 +0,0 @@ -/* 按钮包裹容器 */ -.button-with-tip-wrapper { - position: relative; - display: inline-flex; - align-items: center; - gap: 4px; -} - -.button-with-tip { - transition: all 0.3s ease; -} - -/* 提示指示器 */ -.button-tip-indicator { - font-size: 12px; - color: rgba(0, 0, 0, 0.25); - cursor: help; - transition: all 0.3s ease; - animation: pulse 2s ease-in-out infinite; -} - -.button-with-tip-wrapper:hover .button-tip-indicator { - color: #1677ff; - animation: none; -} - -/* 脉冲动画 */ -@keyframes pulse { - 0%, 100% { - opacity: 1; - transform: scale(1); - } - 50% { - opacity: 0.6; - transform: scale(1.1); - } -} - -/* 提示框样式 */ -.button-tip-overlay { - max-width: 360px; -} - -.button-tip-overlay .ant-tooltip-inner { - padding: 12px 16px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 8px; - box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3); -} - -.button-tip-overlay .ant-tooltip-arrow { - --antd-arrow-background-color: #667eea; -} - -.button-tip-overlay .ant-tooltip-arrow-content { - background: #667eea; -} - -/* 提示内容布局 */ -.button-tip-content { - display: flex; - flex-direction: column; - gap: 8px; - color: #ffffff; - font-size: 13px; - line-height: 1.6; -} - -.button-tip-title { - font-size: 14px; - font-weight: 600; - color: #ffffff; - border-bottom: 1px solid rgba(255, 255, 255, 0.2); - padding-bottom: 6px; -} - -.button-tip-description { - color: rgba(255, 255, 255, 0.95); - font-size: 13px; -} - -.button-tip-shortcut { - display: flex; - align-items: center; - gap: 6px; - margin-top: 4px; - padding-top: 8px; - border-top: 1px solid rgba(255, 255, 255, 0.15); -} - -.tip-label { - font-size: 12px; - color: rgba(255, 255, 255, 0.8); -} - -.tip-kbd { - display: inline-block; - padding: 2px 8px; - background: rgba(255, 255, 255, 0.2); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 4px; - font-size: 11px; - font-family: 'Monaco', 'Consolas', monospace; - color: #ffffff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.button-tip-notes { - margin-top: 4px; - padding-top: 8px; - border-top: 1px solid rgba(255, 255, 255, 0.15); -} - -.tip-notes-title { - font-size: 12px; - font-weight: 500; - color: rgba(255, 255, 255, 0.9); - margin-bottom: 6px; -} - -.tip-notes-list { - margin: 0; - padding-left: 16px; - list-style-type: disc; -} - -.tip-notes-list li { - font-size: 12px; - color: rgba(255, 255, 255, 0.85); - margin-bottom: 4px; -} - -.tip-notes-list li:last-child { - margin-bottom: 0; -} - -/* 不同主题的提示框 */ -.tip-theme-success.button-tip-overlay .ant-tooltip-inner { - background: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%); -} - -.tip-theme-warning.button-tip-overlay .ant-tooltip-inner { - background: linear-gradient(135deg, #f7971e 0%, #ffd200 100%); -} - -.tip-theme-danger.button-tip-overlay .ant-tooltip-inner { - background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%); -} - -.tip-theme-info.button-tip-overlay .ant-tooltip-inner { - background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); -} - -/* 响应式调整 */ -@media (max-width: 768px) { - .button-tip-overlay { - max-width: 280px; - } - - .button-tip-indicator { - display: none; - } -} diff --git a/frontend/src/components/shared/ButtonWithTip/ButtonWithTip.jsx b/frontend/src/components/shared/ButtonWithTip/ButtonWithTip.jsx deleted file mode 100644 index f8d4130..0000000 --- a/frontend/src/components/shared/ButtonWithTip/ButtonWithTip.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Button, Tooltip } from 'antd' -import { QuestionCircleOutlined } from '@ant-design/icons' -import './ButtonWithTip.css' - -/** - * 带有增强提示的按钮组件 - * @param {Object} props - * @param {string} props.label - 按钮文本 - * @param {ReactNode} props.icon - 按钮图标 - * @param {string} props.type - 按钮类型 - * @param {boolean} props.danger - 危险按钮 - * @param {boolean} props.disabled - 禁用状态 - * @param {Function} props.onClick - 点击回调 - * @param {Object} props.tip - 提示配置 - * @param {string} props.tip.title - 提示标题 - * @param {string} props.tip.description - 详细描述 - * @param {string} props.tip.shortcut - 快捷键提示 - * @param {Array} props.tip.notes - 注意事项列表 - * @param {string} props.tip.placement - 提示位置 - * @param {boolean} props.showTipIcon - 是否显示提示图标 - * @param {string} props.size - 按钮大小 - */ -function ButtonWithTip({ - label, - icon, - type = 'default', - danger = false, - disabled = false, - onClick, - tip, - showTipIcon = true, - size = 'middle', - ...restProps -}) { - // 如果没有提示配置,直接返回普通按钮 - if (!tip) { - return ( - - ) - } - - // 构建提示内容 - const tooltipContent = ( -
- {tip.title &&
{tip.title}
} - {tip.description &&
{tip.description}
} - {tip.shortcut && ( -
- 快捷键: - {tip.shortcut} -
- )} - {tip.notes && tip.notes.length > 0 && ( -
-
注意事项:
-
    - {tip.notes.map((note, index) => ( -
  • {note}
  • - ))} -
-
- )} -
- ) - - return ( - -
- - {showTipIcon && !disabled && ( - - )} -
-
- ) -} - -export default ButtonWithTip diff --git a/frontend/src/components/shared/ChartPanel/ChartPanel.css b/frontend/src/components/shared/ChartPanel/ChartPanel.css deleted file mode 100644 index 69a0cc2..0000000 --- a/frontend/src/components/shared/ChartPanel/ChartPanel.css +++ /dev/null @@ -1,17 +0,0 @@ -/* 图表面板 */ -.chart-panel { - margin-bottom: 16px; -} - -.chart-panel:last-child { - margin-bottom: 0; -} - -.chart-panel-title { - font-size: 13px; - font-weight: 600; - color: rgba(0, 0, 0, 0.88); - margin-bottom: 12px; - padding-left: 8px; - border-left: 3px solid #1677ff; -} diff --git a/frontend/src/components/shared/ChartPanel/ChartPanel.jsx b/frontend/src/components/shared/ChartPanel/ChartPanel.jsx deleted file mode 100644 index 5f26c8f..0000000 --- a/frontend/src/components/shared/ChartPanel/ChartPanel.jsx +++ /dev/null @@ -1,202 +0,0 @@ -import { useEffect, useRef } from 'react' -import * as echarts from 'echarts' -import './ChartPanel.css' - -/** - * 图表面板组件 - * @param {Object} props - * @param {string} props.type - 图表类型: 'line' | 'bar' | 'pie' | 'ring' - * @param {string} props.title - 图表标题 - * @param {Object} props.data - 图表数据 - * @param {number} props.height - 图表高度,默认 200px - * @param {Object} props.option - 自定义 ECharts 配置 - * @param {string} props.className - 自定义类名 - */ -function ChartPanel({ type = 'line', title, data, height = 200, option = {}, className = '' }) { - const chartRef = useRef(null) - const chartInstance = useRef(null) - - useEffect(() => { - if (!chartRef.current || !data) return - - // 使用 setTimeout 确保 DOM 完全渲染 - const timer = setTimeout(() => { - // 初始化图表 - if (!chartInstance.current) { - chartInstance.current = echarts.init(chartRef.current) - } - - // 根据类型生成配置 - const chartOption = getChartOption(type, data, option) - chartInstance.current.setOption(chartOption, true) - }, 0) - - // 窗口大小改变时重绘(使用 passive 选项) - const handleResize = () => { - if (chartInstance.current) { - chartInstance.current.resize() - } - } - - // 添加被动事件监听器 - window.addEventListener('resize', handleResize, { passive: true }) - - return () => { - clearTimeout(timer) - window.removeEventListener('resize', handleResize) - } - }, [type, data, option]) - - // 组件卸载时销毁图表 - useEffect(() => { - return () => { - chartInstance.current?.dispose() - } - }, []) - - return ( -
- {title &&
{title}
} -
-
- ) -} - -/** - * 根据图表类型生成 ECharts 配置 - */ -function getChartOption(type, data, customOption) { - const baseOption = { - grid: { - left: '10%', - right: '5%', - top: '15%', - bottom: '15%', - }, - tooltip: { - trigger: type === 'pie' || type === 'ring' ? 'item' : 'axis', - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderColor: '#e8e8e8', - borderWidth: 1, - textStyle: { - color: '#333', - }, - }, - } - - switch (type) { - case 'line': - return { - ...baseOption, - xAxis: { - type: 'category', - data: data.xAxis || [], - boundaryGap: false, - axisLine: { lineStyle: { color: '#e8e8e8' } }, - axisLabel: { color: '#8c8c8c', fontSize: 11 }, - }, - yAxis: { - type: 'value', - axisLine: { lineStyle: { color: '#e8e8e8' } }, - axisLabel: { color: '#8c8c8c', fontSize: 11 }, - splitLine: { lineStyle: { color: '#f0f0f0' } }, - }, - series: [ - { - type: 'line', - data: data.series || [], - smooth: true, - lineStyle: { width: 2, color: '#1677ff' }, - areaStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { offset: 0, color: 'rgba(22, 119, 255, 0.3)' }, - { offset: 1, color: 'rgba(22, 119, 255, 0.05)' }, - ]), - }, - symbol: 'circle', - symbolSize: 6, - itemStyle: { color: '#1677ff' }, - }, - ], - ...customOption, - } - - case 'bar': - return { - ...baseOption, - xAxis: { - type: 'category', - data: data.xAxis || [], - axisLine: { lineStyle: { color: '#e8e8e8' } }, - axisLabel: { color: '#8c8c8c', fontSize: 11 }, - }, - yAxis: { - type: 'value', - axisLine: { lineStyle: { color: '#e8e8e8' } }, - axisLabel: { color: '#8c8c8c', fontSize: 11 }, - splitLine: { lineStyle: { color: '#f0f0f0' } }, - }, - series: [ - { - type: 'bar', - data: data.series || [], - itemStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { offset: 0, color: '#4096ff' }, - { offset: 1, color: '#1677ff' }, - ]), - borderRadius: [4, 4, 0, 0], - }, - barWidth: '50%', - }, - ], - ...customOption, - } - - case 'pie': - case 'ring': - return { - ...baseOption, - grid: undefined, - legend: { - orient: 'vertical', - right: '10%', - top: 'center', - textStyle: { color: '#8c8c8c', fontSize: 12 }, - }, - series: [ - { - type: 'pie', - radius: type === 'ring' ? ['40%', '65%'] : '65%', - center: ['40%', '50%'], - data: data.series || [], - label: { - fontSize: 11, - color: '#8c8c8c', - }, - labelLine: { - lineStyle: { color: '#d9d9d9' }, - }, - itemStyle: { - borderRadius: 4, - borderColor: '#fff', - borderWidth: 2, - }, - emphasis: { - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.3)', - }, - }, - }, - ], - ...customOption, - } - - default: - return { ...baseOption, ...customOption } - } -} - -export default ChartPanel diff --git a/frontend/src/components/shared/ConfirmDialog/ConfirmDialog.jsx b/frontend/src/components/shared/ConfirmDialog/ConfirmDialog.jsx deleted file mode 100644 index d7095b0..0000000 --- a/frontend/src/components/shared/ConfirmDialog/ConfirmDialog.jsx +++ /dev/null @@ -1,138 +0,0 @@ -import { Modal } from 'antd' -import { ExclamationCircleOutlined, DeleteOutlined } from '@ant-design/icons' - -/** - * 标准确认对话框组件 - * @param {Object} options - 对话框配置 - * @param {string} options.title - 标题 - * @param {string|ReactNode} options.content - 内容 - * @param {string} options.okText - 确认按钮文字 - * @param {string} options.cancelText - 取消按钮文字 - * @param {string} options.type - 类型: 'warning', 'danger', 'info' - * @param {Function} options.onOk - 确认回调 - * @param {Function} options.onCancel - 取消回调 - */ -const ConfirmDialog = { - /** - * 显示删除确认对话框(单个项目) - */ - delete: ({ title = '确认删除', itemName, itemInfo, onOk, onCancel }) => { - Modal.confirm({ - title, - content: ( -
-

您确定要删除以下项目吗?

-
-

{itemName}

- {itemInfo && ( -

{itemInfo}

- )} -
-

- 此操作不可恢复,请谨慎操作! -

-
- ), - okText: '确认删除', - cancelText: '取消', - okType: 'danger', - centered: true, - icon: , - onOk, - onCancel, - }) - }, - - /** - * 显示批量删除确认对话框 - */ - batchDelete: ({ count, items, onOk, onCancel }) => { - Modal.confirm({ - title: '批量删除确认', - content: ( -
-

您确定要删除选中的 {count} 个项目吗?

-
- {items.map((item, index) => ( -
- {item.name} - {item.info && ( - - ({item.info}) - - )} -
- ))} -
-

- 此操作不可恢复,请谨慎操作! -

-
- ), - okText: '确认删除', - cancelText: '取消', - okType: 'danger', - centered: true, - icon: , - onOk, - onCancel, - }) - }, - - /** - * 显示警告确认对话框 - */ - warning: ({ title, content, okText = '确定', cancelText = '取消', onOk, onCancel }) => { - Modal.confirm({ - title, - content, - okText, - cancelText, - centered: true, - icon: , - onOk, - onCancel, - }) - }, - - /** - * 显示通用确认对话框 - */ - confirm: ({ - title, - content, - okText = '确定', - cancelText = '取消', - okType = 'primary', - onOk, - onCancel, - }) => { - Modal.confirm({ - title, - content, - okText, - cancelText, - okType, - centered: true, - onOk, - onCancel, - }) - }, -} - -export default ConfirmDialog diff --git a/frontend/src/components/shared/DetailDrawer/DetailDrawer.css b/frontend/src/components/shared/DetailDrawer/DetailDrawer.css deleted file mode 100644 index c5db2f2..0000000 --- a/frontend/src/components/shared/DetailDrawer/DetailDrawer.css +++ /dev/null @@ -1,119 +0,0 @@ -/* 详情抽屉容器 */ -.detail-drawer-content { - height: 100%; - display: flex; - flex-direction: column; -} - -/* 顶部信息区域 - 固定不滚动 */ -.detail-drawer-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px; - background: #fafafa; - border-bottom: 1px solid #f0f0f0; - flex-shrink: 0; -} - -.detail-drawer-header-left { - display: flex; - align-items: center; - gap: 16px; -} - -.detail-drawer-close-button { - font-size: 18px; - color: #666; -} - -.detail-drawer-close-button:hover { - color: #1677ff; -} - -.detail-drawer-header-info { - display: flex; - align-items: center; - gap: 12px; -} - -.detail-drawer-title-icon { - font-size: 18px; - color: #1677ff; -} - -.detail-drawer-title { - margin: 0; - font-size: 18px; - font-weight: 600; - color: rgba(0, 0, 0, 0.88); -} - -.detail-drawer-badge { - display: flex; - align-items: center; -} - -.detail-drawer-header-right { - flex: 1; - display: flex; - justify-content: flex-end; -} - -/* 可滚动内容区域 */ -.detail-drawer-scrollable-content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - padding: 24px; -} - -/* 标签页区域 */ -.detail-drawer-tabs { - background: #ffffff; - padding: 0; - min-height: 400px; -} - -.detail-drawer-tabs :global(.ant-tabs) { - height: 100%; -} - -.detail-drawer-tabs :global(.ant-tabs-content-holder) { - overflow: visible; -} - -.detail-drawer-tabs :global(.ant-tabs-nav) { - padding: 0; - margin: 0 0 16px 0; - background: transparent; -} - -.detail-drawer-tabs :global(.ant-tabs-nav::before) { - border-bottom: 1px solid #f0f0f0; -} - -.detail-drawer-tabs :global(.ant-tabs-tab) { - padding: 12px 0; - margin: 0 32px 0 0; - font-size: 14px; - font-weight: 500; -} - -.detail-drawer-tabs :global(.ant-tabs-tab:first-child) { - margin-left: 0; -} - -.detail-drawer-tabs :global(.ant-tabs-tab-active .ant-tabs-tab-btn) { - color: #d946ef; -} - -.detail-drawer-tabs :global(.ant-tabs-ink-bar) { - background: #d946ef; - height: 3px; -} - -.detail-drawer-tab-content { - padding: 0; - background: #ffffff; -} diff --git a/frontend/src/components/shared/DetailDrawer/DetailDrawer.tsx b/frontend/src/components/shared/DetailDrawer/DetailDrawer.tsx deleted file mode 100644 index 0286dda..0000000 --- a/frontend/src/components/shared/DetailDrawer/DetailDrawer.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Drawer, Button, Space, Tabs } from "antd"; -import { CloseOutlined } from "@ant-design/icons"; -import type { ReactNode } from "react"; -import "./DetailDrawer.css"; - -type DrawerTitle = { - text: string; - badge?: ReactNode; - icon?: ReactNode; -}; - -type HeaderAction = { - key: string; - label: string; - type?: "default" | "primary" | "dashed" | "link" | "text"; - icon?: ReactNode; - danger?: boolean; - disabled?: boolean; - onClick: () => void; -}; - -type DrawerTab = { - key: string; - label: ReactNode; - content: ReactNode; -}; - -interface DetailDrawerProps { - visible: boolean; - onClose: () => void; - title?: DrawerTitle; - headerActions?: HeaderAction[]; - width?: number; - children?: ReactNode; - tabs?: DrawerTab[]; -} - -function DetailDrawer({ - visible, - onClose, - title, - headerActions = [], - width = 1080, - children, - tabs, -}: DetailDrawerProps) { - return ( - -
-
-
-
-
- - {headerActions.map((action) => ( - - ))} - -
-
- -
- {children} - - {tabs && tabs.length > 0 && ( -
- ({ - key: tab.key, - label: tab.label, - children:
{tab.content}
, - }))} - /> -
- )} -
-
-
- ); -} - -export default DetailDrawer; diff --git a/frontend/src/components/shared/ExtendInfoPanel/ExtendInfoPanel.css b/frontend/src/components/shared/ExtendInfoPanel/ExtendInfoPanel.css deleted file mode 100644 index 8ea059b..0000000 --- a/frontend/src/components/shared/ExtendInfoPanel/ExtendInfoPanel.css +++ /dev/null @@ -1,105 +0,0 @@ -/* 扩展信息面板容器 */ -.extend-info-panel { - display: flex; - gap: 16px; - width: 100%; -} - -/* 垂直布局(默认) */ -.extend-info-panel-vertical { - flex-direction: column; -} - -/* 水平布局 */ -.extend-info-panel-horizontal { - flex-direction: row; - flex-wrap: wrap; -} - -/* 信息区块 */ -.extend-info-section { - background: #ffffff; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); - overflow: hidden; - transition: all 0.3s ease; -} - -/* 水平布局时区块自适应宽度 */ -.extend-info-panel-horizontal .extend-info-section { - flex: 1; - min-width: 0; -} - -.extend-info-section:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -/* 区块头部 */ -.extend-info-section-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 20px; - background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%); - border-bottom: 1px solid #e8e8e8; - cursor: pointer; - user-select: none; - transition: background 0.2s ease; -} - -.extend-info-section-header:hover { - background: linear-gradient(135deg, #f0f4ff 0%, #e8f0ff 100%); -} - -.extend-info-section-title { - display: flex; - align-items: center; - gap: 8px; - font-size: 14px; - font-weight: 600; - color: rgba(0, 0, 0, 0.88); -} - -.extend-info-section-icon { - display: flex; - align-items: center; - font-size: 16px; - color: #1677ff; -} - -.extend-info-section-toggle { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border: none; - background: transparent; - color: #8c8c8c; - cursor: pointer; - transition: all 0.2s ease; - border-radius: 4px; -} - -.extend-info-section-toggle:hover { - background: rgba(0, 0, 0, 0.06); - color: #1677ff; -} - -/* 区块内容 */ -.extend-info-section-content { - padding: 16px 20px; - animation: expandContent 0.3s ease-out; -} - -@keyframes expandContent { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} diff --git a/frontend/src/components/shared/ExtendInfoPanel/ExtendInfoPanel.jsx b/frontend/src/components/shared/ExtendInfoPanel/ExtendInfoPanel.jsx deleted file mode 100644 index 03e5166..0000000 --- a/frontend/src/components/shared/ExtendInfoPanel/ExtendInfoPanel.jsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useState } from 'react' -import { UpOutlined, DownOutlined } from '@ant-design/icons' -import './ExtendInfoPanel.css' - -/** - * 扩展信息面板组件 - * @param {Object} props - * @param {Array} props.sections - 信息区块配置数组 - * @param {string} props.sections[].key - 区块唯一键 - * @param {string} props.sections[].title - 区块标题 - * @param {ReactNode} props.sections[].icon - 标题图标 - * @param {ReactNode} props.sections[].content - 区块内容 - * @param {boolean} props.sections[].defaultCollapsed - 默认是否折叠 - * @param {boolean} props.sections[].hideTitleBar - 是否隐藏该区块的标题栏(默认 false) - * @param {string} props.layout - 布局方式:'vertical'(垂直堆叠)| 'horizontal'(水平排列) - * @param {string} props.className - 自定义类名 - */ -function ExtendInfoPanel({ sections = [], layout = 'vertical', className = '' }) { - const [collapsedSections, setCollapsedSections] = useState(() => { - const initial = {} - sections.forEach((section) => { - if (section.defaultCollapsed) { - initial[section.key] = true - } - }) - return initial - }) - - const toggleSection = (key) => { - setCollapsedSections((prev) => ({ - ...prev, - [key]: !prev[key], - })) - } - - return ( -
- {sections.map((section) => { - const isCollapsed = collapsedSections[section.key] - const hideTitleBar = section.hideTitleBar === true - - return ( -
- {/* 区块头部 - 可配置隐藏 */} - {!hideTitleBar && ( -
toggleSection(section.key)}> -
- {section.icon && {section.icon}} - {section.title} -
- -
- )} - - {/* 区块内容 - 如果隐藏标题栏则总是显示,否则根据折叠状态 */} - {(hideTitleBar || !isCollapsed) && ( -
{section.content}
- )} -
- ) - })} -
- ) -} - -export default ExtendInfoPanel diff --git a/frontend/src/components/shared/InfoPanel/InfoPanel.css b/frontend/src/components/shared/InfoPanel/InfoPanel.css deleted file mode 100644 index 463adb6..0000000 --- a/frontend/src/components/shared/InfoPanel/InfoPanel.css +++ /dev/null @@ -1,96 +0,0 @@ -/* 信息面板 */ -.info-panel { - padding: 0; - background: #ffffff; -} - -/* 信息区域容器 */ -.info-panel > :global(.ant-row) { - padding: 24px; - background: #ffffff; - border-bottom: 1px solid #f0f0f0; -} - -.info-panel-item { - display: flex; - flex-direction: column; - gap: 5px; - padding: 10px 0; - border-bottom: 1px solid #f0f0f0; - transition: all 0.2s ease; - position: relative; -} - -.info-panel-item:last-child { - border-bottom: none; -} - -/* 添加左侧装饰条 */ -.info-panel-item::before { - content: ''; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - width: 0; - height: 0; - background: linear-gradient(180deg, #1677ff 0%, #4096ff 100%); - border-radius: 2px; - transition: all 0.3s ease; -} - -.info-panel-item:hover { - background: linear-gradient(90deg, #f0f7ff 0%, transparent 100%); - padding-left: 10px; - padding-right: 16px; - margin-left: -12px; - margin-right: -16px; - border-radius: 8px; - border-bottom-color: transparent; -} - -.info-panel-item:hover::before { - width: 3px; - height: 60%; -} - -.info-panel-label { - color: rgba(0, 0, 0, 0.45); - font-size: 13px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 1px; - margin-bottom: 4px; -} - -.info-panel-value { - color: rgba(0, 0, 0, 0.88); - font-size: 15px; - font-weight: 500; - word-break: break-all; - line-height: 1.6; -} - -/* 操作按钮区 */ -.info-panel-actions { - padding: 24px 32px; - background: linear-gradient(to bottom, #fafafa 0%, #f5f5f5 100%); - border-top: 2px solid #e8e8e8; - position: relative; -} - -/* 操作区域顶部装饰线 */ -.info-panel-actions::before { - content: ''; - position: absolute; - top: -2px; - left: 0; - right: 0; - height: 2px; - background: linear-gradient(90deg, #1677ff 0%, transparent 50%, #1677ff 100%); - opacity: 0.3; -} - - - - diff --git a/frontend/src/components/shared/InfoPanel/InfoPanel.jsx b/frontend/src/components/shared/InfoPanel/InfoPanel.jsx deleted file mode 100644 index c60f2c9..0000000 --- a/frontend/src/components/shared/InfoPanel/InfoPanel.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Row, Col, Space, Button } from 'antd' -import './InfoPanel.css' - -/** - * 信息展示面板组件 - * @param {Object} props - * @param {Object} props.data - 数据源 - * @param {Array} props.fields - 字段配置数组 - * @param {Array} props.actions - 操作按钮配置(可选) - * @param {Array} props.gutter - Grid间距配置 - */ -function InfoPanel({ data, fields = [], actions = [], gutter = [24, 16] }) { - if (!data) { - return null - } - - return ( -
- - {fields.map((field) => { - const value = data[field.key] - const displayValue = field.render ? field.render(value, data) : value - - return ( - -
-
{field.label}
-
{displayValue}
-
- - ) - })} -
- - {/* 可选的操作按钮区 */} - {actions && actions.length > 0 && ( -
- - {actions.map((action) => ( - - ))} - -
- )} -
- ) -} - -export default InfoPanel diff --git a/frontend/src/components/shared/ListActionBar/ListActionBar.css b/frontend/src/components/shared/ListActionBar/ListActionBar.css deleted file mode 100644 index eb9cbf7..0000000 --- a/frontend/src/components/shared/ListActionBar/ListActionBar.css +++ /dev/null @@ -1,96 +0,0 @@ -.list-action-bar { - position: sticky; - top: 0; - z-index: 10; - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 4px; - padding: 16px; - background: var(--card-bg); - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); - width: 100%; - border: 1px solid var(--border-color); -} - -.list-action-bar-left, -.list-action-bar-right { - display: flex; - gap: 12px; - align-items: center; -} - -/* 搜索和筛选组合 */ -.list-action-bar-right :global(.ant-space-compact) { - display: flex; -} - -.list-action-bar-right :global(.ant-space-compact .ant-input-search) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -.list-action-bar-right :global(.ant-space-compact > .ant-btn) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -/* 批量操作区域样式 */ -.selection-info { - display: flex; - align-items: center; - gap: 12px; - padding: 8px 16px; - background: var(--bg-color-secondary); - border: 1px solid var(--link-color); - border-radius: 6px; - font-size: 14px; -} - -.selection-count { - color: var(--text-color); -} - -.selection-count strong { - color: var(--link-color); - font-weight: 600; - margin: 0 4px; -} - -.all-pages-tag { - color: var(--link-color); - font-weight: 500; - margin-left: 4px; -} - -.select-all-link, -.clear-selection-link { - color: var(--link-color); - cursor: pointer; - text-decoration: none; - white-space: nowrap; - padding: 2px 8px; - border-radius: 4px; - transition: all 0.2s; -} - -.select-all-link:hover, -.clear-selection-link:hover { - background: rgba(22, 119, 255, 0.1); - text-decoration: underline; -} - -/* 响应式 */ -@media (max-width: 768px) { - .list-action-bar { - flex-direction: column; - gap: 12px; - align-items: stretch; - } - - .list-action-bar-left, - .list-action-bar-right { - flex-wrap: wrap; - } -} diff --git a/frontend/src/components/shared/ListActionBar/ListActionBar.jsx b/frontend/src/components/shared/ListActionBar/ListActionBar.jsx deleted file mode 100644 index 7dff36d..0000000 --- a/frontend/src/components/shared/ListActionBar/ListActionBar.jsx +++ /dev/null @@ -1,134 +0,0 @@ - import { Button, Input, Space, Popover } from 'antd' -import { ReloadOutlined, FilterOutlined } from '@ant-design/icons' -import './ListActionBar.css' - -const { Search } = Input - -/** - * 列表操作栏组件 - * @param {Object} props - * @param {Array} props.actions - 左侧操作按钮配置数组 - * @param {Array} props.batchActions - 批量操作按钮配置数组(仅在有选中项时显示) - * @param {Object} props.selectionInfo - 选中信息 { count: 选中数量, total: 总数量, isAllPagesSelected: 是否跨页全选 } - * @param {Function} props.onSelectAllPages - 选择所有页回调 - * @param {Function} props.onClearSelection - 清除选择回调 - * @param {Object} props.search - 搜索配置 - * @param {Object} props.filter - 高级筛选配置(可选) - * @param {boolean} props.showRefresh - 是否显示刷新按钮 - * @param {Function} props.onRefresh - 刷新回调 - */ -function ListActionBar({ - actions = [], - batchActions = [], - selectionInfo, - onSelectAllPages, - onClearSelection, - search, - filter, - showRefresh = false, - onRefresh, -}) { - // 是否有选中项 - const hasSelection = selectionInfo && selectionInfo.count > 0 - return ( -
- {/* 左侧操作按钮区 */} -
- {/* 常规操作按钮(无选中时显示) */} - {!hasSelection && actions.map((action) => ( - - ))} - - {/* 批量操作区域(有选中时显示) */} - {hasSelection && ( - - {/* 选中信息 */} -
- - 已选择 {selectionInfo.count} 项 - {selectionInfo.isAllPagesSelected && ( - (全部页) - )} - - {!selectionInfo.isAllPagesSelected && selectionInfo.total > selectionInfo.count && ( - - 选择全部 {selectionInfo.total} 项 - - )} - - 清除 - -
- - {/* 批量操作按钮 */} - {batchActions.map((action) => ( - - ))} -
- )} -
- - {/* 右侧搜索筛选区 */} -
- - search?.onChange?.(e.target.value)} - value={search?.value} - /> - {filter && ( - - - {filter.title || '高级筛选'} -
- } - trigger="click" - open={filter.visible} - onOpenChange={filter.onVisibleChange} - placement="bottomRight" - overlayClassName="filter-popover" - > - - - )} - - {showRefresh && ( - - )} -
-
- ) -} - -export default ListActionBar diff --git a/frontend/src/components/shared/ListTable/ListTable.tsx b/frontend/src/components/shared/ListTable/ListTable.tsx index 414bbbf..e8d6997 100644 --- a/frontend/src/components/shared/ListTable/ListTable.tsx +++ b/frontend/src/components/shared/ListTable/ListTable.tsx @@ -134,6 +134,7 @@ function ListTable>({ const emptyText = locale?.emptyText ?? ( ); + const emptyContent = typeof emptyText === "function" ? emptyText() : emptyText; const tableLocale = { ...locale, emptyText: null, @@ -158,7 +159,7 @@ function ListTable>({ className: selectedRow?.[rowKey] === record[rowKey] ? "row-selected" : "", })} /> - {isEmptyTable ?
{emptyText}
: null} + {isEmptyTable ?
{emptyContent}
: null} ); } diff --git a/frontend/src/components/shared/MainLayout/AppHeader.css b/frontend/src/components/shared/MainLayout/AppHeader.css deleted file mode 100644 index 6429ab2..0000000 --- a/frontend/src/components/shared/MainLayout/AppHeader.css +++ /dev/null @@ -1,154 +0,0 @@ -.app-header { - background: var(--header-bg); - padding: 0 24px; - display: flex; - align-items: center; - justify-content: space-between; - box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); - height: 64px; - border-bottom: 1px solid var(--border-color); - color: var(--text-color); -} - -/* 左侧区域 */ -.header-left { - display: flex; - align-items: center; - gap: 16px; -} - -/* Logo 区域 */ -.header-logo { - display: flex; - align-items: center; - justify-content: center; - width: 168px; - transition: width 0.2s; -} - -.trigger { - font-size: 18px; - cursor: pointer; - transition: color 0.3s; - padding: 8px; - border-radius: 4px; - color: var(--text-color-secondary); - display: flex; - align-items: center; -} - -.trigger:hover { - color: #1677ff; - background: rgba(22, 119, 255, 0.08); -} - -/* 右侧区域 */ -.header-right { - display: flex; - align-items: center; - gap: 16px; -} - -.header-actions { - display: flex; - align-items: center; - gap: 16px; -} - -/* Icon Buttons */ -.header-icon-btn { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 8px; - cursor: pointer; - color: var(--text-color-secondary); - transition: all 0.2s; - background: transparent; -} - -.header-icon-btn:hover { - background-color: var(--item-hover-bg); - color: var(--text-color); -} - -/* 通知面板样式 */ -.header-notification-popover .ant-popover-inner-content { - padding: 0; -} - -.notification-popover { - width: 320px; -} - -.popover-header { - padding: 12px 16px; - border-bottom: 1px solid var(--border-color); - display: flex; - justify-content: space-between; - align-items: center; -} - -.popover-header .title { - font-weight: 600; - font-size: 16px; - color: var(--text-color); -} - -.notification-list { - max-height: 400px; - overflow-y: auto; -} - -.notification-item { - padding: 12px 16px !important; - cursor: pointer; - transition: background 0.3s; - background: var(--bg-color); -} - -.notification-item:hover { - background: var(--item-hover-bg); -} - -.notification-item.unread { - background: #e6f7ff; -} - -/* Dark mode adjustment for unread */ -body.dark .notification-item.unread { - background: #111d2c; -} - -.notification-item.unread:hover { - background: #bae7ff; -} - -body.dark .notification-item.unread:hover { - background: #112a45; -} - -.content-text { - font-size: 13px; - color: var(--text-color-secondary); - margin-top: 4px; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.time { - font-size: 12px; - color: var(--text-color-secondary); - opacity: 0.8; - margin-top: 4px; -} - -.popover-footer { - padding: 8px; - border-top: 1px solid var(--border-color); - text-align: center; -} \ No newline at end of file diff --git a/frontend/src/components/shared/MainLayout/AppHeader.jsx b/frontend/src/components/shared/MainLayout/AppHeader.jsx deleted file mode 100644 index ddb3bf7..0000000 --- a/frontend/src/components/shared/MainLayout/AppHeader.jsx +++ /dev/null @@ -1,203 +0,0 @@ -import { useState, useEffect } from 'react' -import { Layout, Badge, Avatar, Dropdown, Space, Popover, List, Tabs, Button, Empty, Typography, Segmented, Tooltip } from 'antd' -import { useNavigate } from 'react-router-dom' -import { - MenuFoldOutlined, - MenuUnfoldOutlined, - BellOutlined, - ProjectOutlined, - TeamOutlined, - NotificationOutlined, - MoonOutlined, - SunOutlined, - GlobalOutlined -} from '@ant-design/icons' -import useUserStore from '@/stores/userStore' -import useNotificationStore from '@/stores/notificationStore' -import useThemeStore from '@/stores/themeStore' -import { getNotifications, getUnreadCount, markAsRead, markAllAsRead } from '@/api/notification' -import Toast from '@/components/Toast/Toast' -import './AppHeader.css' - -const { Header } = Layout -const { Text } = Typography - -function AppHeader({ collapsed, onToggle, showLogo = true }) { - const navigate = useNavigate() - const { user } = useUserStore() - const { unreadCount, fetchUnreadCount, decrementUnreadCount, resetUnreadCount } = useNotificationStore() - const { isDarkMode, toggleTheme } = useThemeStore() - - const [notifications, setNotifications] = useState([]) - const [loading, setLoading] = useState(false) - const [popoverVisible, setPopoverVisible] = useState(false) - const [lang, setLang] = useState('zh') - - useEffect(() => { - if (user) { - fetchUnreadCount() - const timer = setInterval(fetchUnreadCount, 120000) - return () => clearInterval(timer) - } - }, [user]) - - const fetchNotifications = async () => { - setLoading(true) - try { - const res = await getNotifications({ page: 1, page_size: 5 }) - setNotifications(res.data || []) - } catch (error) { - console.error('Fetch notifications error:', error) - } finally { - setLoading(false) - } - } - - const handleMarkRead = async (id) => { - try { - await markAsRead(id) - setNotifications(notifications.map(n => n.id === id ? { ...n, is_read: true } : n)) - decrementUnreadCount() - } catch (error) { - console.error('Mark read error:', error) - } - } - - const handleMarkAllRead = async () => { - try { - await markAllAsRead() - setNotifications(notifications.map(n => ({ ...n, is_read: true }))) - resetUnreadCount() - Toast.success('操作成功', '所有通知已标记为已读') - } catch (error) { - console.error('Mark all read error:', error) - } - } - - const handleNotificationClick = (n) => { - if (!n.is_read) { - handleMarkRead(n.id) - } - if (n.link) { - navigate(n.link) - setPopoverVisible(false) - } - } - - const getCategoryIcon = (category) => { - switch (category) { - case 'project': return - case 'collaboration': return - default: return - } - } - - const notificationContent = ( -
-
- 消息通知 - {unreadCount > 0 && ( - - )} -
- }} - renderItem={(item) => ( - handleNotificationClick(item)} - > - } - title={{item.title}} - description={ -
-
{item.content}
-
{new Date(item.created_at).toLocaleString('zh-CN')}
-
- } - /> -
- )} - /> -
- -
-
- ) - - return ( -
- {/* 左侧:Logo + 折叠按钮 */} - {showLogo && ( -
- {/* Logo 区域 */} -
- logo -

NexDocus

-
- - {/* 折叠按钮 */} -
- {collapsed ? : } -
-
- )} - {!showLogo &&
} {/* Spacer if left is empty */} - - {/* 右侧:功能按钮 */} -
- - - {/* 1. 主题切换 */} -
- {isDarkMode ? : } -
- - {/* 2. 语言切换 */} - - - {/* 3. 消息通知 */} - { - setPopoverVisible(visible) - if (visible) { - fetchNotifications() - } - }} - placement="bottomRight" - overlayClassName="header-notification-popover" - > -
- - - -
-
- -
-
-
- ) -} - -export default AppHeader \ No newline at end of file diff --git a/frontend/src/components/shared/MainLayout/AppSider.css b/frontend/src/components/shared/MainLayout/AppSider.css deleted file mode 100644 index f4a98aa..0000000 --- a/frontend/src/components/shared/MainLayout/AppSider.css +++ /dev/null @@ -1,96 +0,0 @@ -.app-sider { - height: 100%; - overflow: auto; - background: #fafafa; - border-right: 1px solid #f0f0f0; - transition: all 0.2s; -} - -.app-sider::-webkit-scrollbar { - width: 6px; -} - -.app-sider::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.1); - border-radius: 3px; -} - -.app-sider::-webkit-scrollbar-thumb:hover { - background: rgba(0, 0, 0, 0.2); -} - -/* 菜单样式 */ -.sider-menu { - border-right: none; - padding-top: 8px; - background: #fafafa; -} - -/* 收起状态下的图标放大 */ -:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-item) { - padding: 0 !important; - display: flex; - align-items: center; - justify-content: center; - height: 56px; - margin: 8px 0; -} - -/* 收起状态下的 SubMenu 样式 */ -:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-submenu) { - padding: 0 !important; -} - -:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-submenu-title) { - padding: 0 !important; - display: flex; - align-items: center; - justify-content: center; - height: 56px; - margin: 8px 0; -} - -:global(.ant-layout-sider-collapsed) .sider-menu :global(.anticon) { - font-size: 24px; - margin: 0; -} - -/* 收起状态下的 Tooltip */ -:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-item-icon) { - font-size: 24px; -} - -:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-submenu-title) :global(.anticon) { - font-size: 24px; - margin: 0; -} - -/* 菜单项徽章 */ -.menu-item-with-badge { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; -} - -.menu-badge { - font-size: 10px; - height: 18px; - line-height: 18px; - border-radius: 9px; - padding: 0 6px; - margin-left: 8px; -} - -.badge-hot :global(.ant-badge-count) { - background: #ff4d4f; -} - -.badge-new :global(.ant-badge-count) { - background: #52c41a; -} - -/* 收起状态下隐藏徽章 */ -:global(.ant-layout-sider-collapsed) .menu-badge { - display: none; -} diff --git a/frontend/src/components/shared/MainLayout/AppSider.jsx b/frontend/src/components/shared/MainLayout/AppSider.jsx deleted file mode 100644 index 29e945a..0000000 --- a/frontend/src/components/shared/MainLayout/AppSider.jsx +++ /dev/null @@ -1,195 +0,0 @@ -import { useState, useEffect } from 'react' -import { useNavigate, useLocation } from 'react-router-dom' -import { - DashboardOutlined, - DesktopOutlined, - GlobalOutlined, - CloudServerOutlined, - UserOutlined, - AppstoreOutlined, - SettingOutlined, - BlockOutlined, - FolderOutlined, - FileTextOutlined, - SafetyOutlined, - TeamOutlined, - ProjectOutlined, - RocketOutlined, - ReadOutlined, - BookOutlined, -} from '@ant-design/icons' -import { App } from 'antd' -import { getUserMenus } from '@/api/menu' -import useUserStore from '@/stores/userStore' -import ModernSidebar from '../ModernSidebar/ModernSidebar' - -// 图标映射 -const iconMap = { - DashboardOutlined: , - DesktopOutlined: , - GlobalOutlined: , - CloudServerOutlined: , - UserOutlined: , - AppstoreOutlined: , - SettingOutlined: , - BlockOutlined: , - FolderOutlined: , - FileTextOutlined: , - SafetyOutlined: , - TeamOutlined: , - ProjectOutlined: , - ReadOutlined: , - BookOutlined: , -} - -function AppSider({ collapsed, onToggle }) { - const { message } = App.useApp(); - const navigate = useNavigate() - const location = useLocation() - const { user, logout } = useUserStore() - const [menuGroups, setMenuGroups] = useState([]) - - // 加载菜单数据 - useEffect(() => { - loadMenus() - }, []) - - const loadMenus = async () => { - try { - const res = await getUserMenus() - if (res.data) { - // 过滤菜单:只显示 type=1 (目录) 和 type=2 (菜单) - const validMenus = res.data.filter(item => [1, 2].includes(item.menu_type)) - transformMenuData(validMenus) - } - } catch (error) { - console.error('Load menus error:', error) - message.error('加载菜单失败') - } - } - - const transformMenuData = (data) => { - const groups = [] - - // 默认组 (用于存放一级菜单即是叶子节点的情况) - const defaultGroup = { - title: '', // 空标题或 '通用' - items: [] - } - - data.forEach(item => { - // 检查是否有子菜单 - const validChildren = item.children ? item.children.filter(child => [1, 2].includes(child.menu_type)) : [] - - if (validChildren.length > 0) { - // 一级菜单作为组标题 - const groupItems = validChildren.map(child => { - const icon = typeof child.icon === 'string' ? (iconMap[child.icon] || ) : child.icon - return { - key: child.menu_code, - label: child.menu_name, - icon: icon, - path: child.path - } - }) - - groups.push({ - title: item.menu_name, // e.g. "系统管理" - items: groupItems - }) - } else { - // 一级菜单是叶子节点,放入默认组 - const icon = typeof item.icon === 'string' ? (iconMap[item.icon] || ) : item.icon - defaultGroup.items.push({ - key: item.menu_code, - label: item.menu_name, - icon: icon, - path: item.path - }) - } - }) - - // 如果默认组有内容,放在最前面 - if (defaultGroup.items.length > 0) { - groups.unshift(defaultGroup) - } - - setMenuGroups(groups) - } - - const handleNavigate = (key, item) => { - if (item.path) { - navigate(item.path) - } - } - - const handleLogout = () => { - logout() - navigate('/login') - } - - const handleProfileClick = () => { - navigate('/profile') - } - - // 获取当前激活的 key - // 简单匹配 path - const getActiveKey = () => { - const path = location.pathname - // 遍历所有 items 找匹配 - for (const group of menuGroups) { - for (const item of group.items) { - if (item.path === path) return item.key - } - } - return '' - } - - const logoNode = ( -
- logo - {!collapsed && ( - NexDocus - )} -
- ) - - // 获取用户头像URL - const getUserAvatarUrl = () => { - if (!user?.avatar) return null - // avatar 字段存储的是相对路径,如:2/avatar/xxx.jpg - // 需要转换为 API 端点: /api/v1/auth/avatar/{user_id}/{filename} - // 如果已经是 http 开头(第三方),则直接返回 - if (user.avatar.startsWith('http')) return user.avatar - - const parts = user.avatar.split('/') - if (parts.length >= 3) { - const userId = parts[0] - const filename = parts[2] - return `/api/v1/auth/avatar/${userId}/${filename}` - } - return null - } - - const userObj = user ? { - name: user.nickname || user.username, - role: user.role_name || 'Admin', - avatar: getUserAvatarUrl() - } : null - - return ( - - ) -} - -export default AppSider diff --git a/frontend/src/components/shared/MainLayout/MainLayout.css b/frontend/src/components/shared/MainLayout/MainLayout.css deleted file mode 100644 index 13e2e3d..0000000 --- a/frontend/src/components/shared/MainLayout/MainLayout.css +++ /dev/null @@ -1,32 +0,0 @@ -.main-layout { - min-height: 100vh; - display: flex; - flex-direction: row; /* Changed to row for Sider-Left layout */ - background: var(--bg-color-secondary); -} - -.main-content-wrapper { - display: flex; - flex-direction: column; - flex: 1; - height: 100vh; - background: var(--bg-color-secondary); - overflow: hidden; -} - -.main-content { - background: var(--bg-color-secondary); - overflow: hidden; - flex: 1; - padding: 16px; - display: flex; - flex-direction: column; -} - -.content-wrapper { - padding: 0; - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; -} \ No newline at end of file diff --git a/frontend/src/components/shared/MainLayout/MainLayout.jsx b/frontend/src/components/shared/MainLayout/MainLayout.jsx deleted file mode 100644 index 726c328..0000000 --- a/frontend/src/components/shared/MainLayout/MainLayout.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useState } from 'react' -import { Layout } from 'antd' -import AppSider from './AppSider' -import AppHeader from './AppHeader' -import './MainLayout.css' - -const { Content } = Layout - -function MainLayout({ children }) { - const [collapsed, setCollapsed] = useState(false) - - const toggleCollapsed = () => { - setCollapsed(!collapsed) - } - - return ( - - - - - -
- {children} -
-
-
-
- ) -} - -export default MainLayout diff --git a/frontend/src/components/shared/MainLayout/index.js b/frontend/src/components/shared/MainLayout/index.js deleted file mode 100644 index 12555fd..0000000 --- a/frontend/src/components/shared/MainLayout/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { default } from './MainLayout' -export { default as MainLayout } from './MainLayout' -export { default as AppSider } from './AppSider' -export { default as AppHeader } from './AppHeader' diff --git a/frontend/src/components/shared/ModernSidebar/ModernSidebar.css b/frontend/src/components/shared/ModernSidebar/ModernSidebar.css deleted file mode 100644 index b22ec6b..0000000 --- a/frontend/src/components/shared/ModernSidebar/ModernSidebar.css +++ /dev/null @@ -1,221 +0,0 @@ -.modern-sidebar { - height: 100vh; - position: relative; - background: var(--sider-bg) !important; - border-right: 1px solid var(--border-color); - display: flex; - flex-direction: column; -} - -.modern-sidebar .ant-layout-sider-children { - display: flex; - flex-direction: column; - height: 100%; -} - -/* Header */ -.modern-sidebar-header { - padding: 24px 20px; - position: relative; - display: flex; - align-items: center; - height: 80px; - flex-shrink: 0; -} - -.logo-container { - display: flex; - align-items: center; - overflow: hidden; - white-space: nowrap; -} - -/* Collapse Trigger */ -.collapse-trigger { - position: absolute; - right: -12px; - top: 32px; - width: 24px; - height: 24px; - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - z-index: 10; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); - color: var(--text-color-secondary); - transition: all 0.3s; -} - -.collapse-trigger:hover { - color: #1677ff; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -/* Menu Area */ -.modern-sidebar-menu { - flex: 1; - overflow-y: auto; - padding: 0 16px; -} - -.modern-sidebar-menu::-webkit-scrollbar { - width: 4px; -} - -.modern-sidebar-menu::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 2px; -} - -/* Menu Group */ -.menu-group { - margin-bottom: 24px; -} - -.group-title { - font-size: 12px; - color: var(--text-color-secondary); - font-weight: 600; - letter-spacing: 0.5px; - margin-bottom: 12px; - padding-left: 12px; - text-transform: uppercase; -} - -/* Menu Item */ -.modern-sidebar-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 16px; - margin-bottom: 8px; - cursor: pointer; - border-radius: 12px; /* Rounded corners */ - transition: all 0.2s; - color: var(--text-color); - font-weight: 500; -} - -.modern-sidebar-item:hover { - background-color: var(--item-hover-bg); - color: var(--text-color); -} - -.modern-sidebar-item.active { - background-color: #2563eb; /* Royal Blue */ - color: #fff; - box-shadow: 0 4px 12px rgba(37, 99, 235, 0.25); -} - -.item-content { - display: flex; - align-items: center; - gap: 12px; -} - -.item-icon { - font-size: 18px; - display: flex; - align-items: center; -} - -.item-label { - font-size: 14px; -} - -.item-arrow { - font-size: 12px; - opacity: 0.8; -} - -/* Collapsed State */ -.modern-sidebar-item.collapsed { - justify-content: center; - padding: 12px; - border-radius: 12px; -} - -/* Footer */ -.modern-sidebar-footer { - padding: 16px; - flex-shrink: 0; - background: var(--sider-bg); -} - -.footer-link { - display: flex; - align-items: center; - gap: 8px; - color: var(--text-color-secondary); - font-size: 14px; - margin-bottom: 16px; - padding-left: 12px; - cursor: pointer; - transition: color 0.2s; -} - -.footer-link:hover { - color: var(--text-color); -} - -.footer-link.collapsed { - justify-content: center; - padding-left: 0; -} - -/* User Card */ -.user-card { - background-color: var(--bg-color-secondary); /* Light gray background */ - border-radius: 12px; - padding: 12px; - display: flex; - align-items: center; - justify-content: space-between; - transition: all 0.2s; -} - -.user-info { - display: flex; - align-items: center; - gap: 12px; - overflow: hidden; -} - -.user-details { - display: flex; - flex-direction: column; - overflow: hidden; -} - -.user-name { - font-size: 14px; - font-weight: 600; - color: var(--text-color); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.user-role { - font-size: 12px; - color: var(--text-color-secondary); - text-transform: uppercase; - font-weight: 500; -} - -.logout-btn { - color: var(--text-color-secondary); - cursor: pointer; - padding: 4px; - border-radius: 4px; - transition: all 0.2s; -} - -.logout-btn:hover { - background-color: var(--border-color); - color: #ef4444; /* Red for logout */ -} \ No newline at end of file diff --git a/frontend/src/components/shared/ModernSidebar/ModernSidebar.jsx b/frontend/src/components/shared/ModernSidebar/ModernSidebar.jsx deleted file mode 100644 index d4ce841..0000000 --- a/frontend/src/components/shared/ModernSidebar/ModernSidebar.jsx +++ /dev/null @@ -1,154 +0,0 @@ -import React, { useState } from 'react'; -import { Layout, Avatar, Tooltip, Button } from 'antd'; -import { - MenuUnfoldOutlined, - MenuFoldOutlined, - LogoutOutlined, - QuestionCircleOutlined, - RightOutlined, - LeftOutlined -} from '@ant-design/icons'; -import './ModernSidebar.css'; - -const { Sider } = Layout; - -const ModernSidebar = ({ - logo, - menuGroups = [], - activeKey, - onNavigate, - user, - onLogout, - onProfileClick, - collapsed, - onCollapse, - width = 260, - collapsedWidth = 80, - className = '', - style = {} -}) => { - - const handleItemClick = (item) => { - if (onNavigate) { - onNavigate(item.key, item); - } - }; - - const renderMenuItem = (item) => { - const isActive = activeKey === item.key; - - // 如果是折叠状态,只显示图标,并使用Tooltip - if (collapsed) { - return ( - -
handleItemClick(item)} - > -
{item.icon}
-
-
- ); - } - - return ( -
handleItemClick(item)} - > -
-
{item.icon}
- {item.label} -
- {isActive && } -
- ); - }; - - return ( - - {/* 顶部 Logo 区域 */} -
-
- {logo} -
- {/* 折叠按钮 - 悬浮在边缘 */} -
onCollapse && onCollapse(!collapsed)} - > - {collapsed ? : } -
-
- - {/* 菜单列表区域 */} -
- {menuGroups.map((group, index) => ( -
- {!collapsed && group.title && ( -
{group.title}
- )} -
- {group.items.map(item => renderMenuItem(item))} -
-
- ))} -
- - {/* 底部区域 */} -
- {/* 帮助支持 */} - {!collapsed && ( -
- - 帮助支持 -
- )} - {collapsed && ( -
- -
- )} - - {/* 用户卡片 */} -
-
- - {user?.name?.[0]?.toUpperCase() || 'U'} - - {!collapsed && ( -
-
{user?.name || 'User'}
-
{user?.role || 'Member'}
-
- )} -
- {!collapsed && ( -
- -
- )} -
-
-
- ); -}; - -export default ModernSidebar; diff --git a/frontend/src/components/shared/PDFViewer/PDFViewer.css b/frontend/src/components/shared/PDFViewer/PDFViewer.css deleted file mode 100644 index c850dba..0000000 --- a/frontend/src/components/shared/PDFViewer/PDFViewer.css +++ /dev/null @@ -1,62 +0,0 @@ -.pdf-viewer-container { - display: flex; - flex-direction: column; - height: 100%; - width: 100%; - background: #f5f5f5; - flex: 1; - min-height: 0; -} - -.pdf-toolbar { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - background: #fff; - border-bottom: 1px solid #e8e8e8; - flex-shrink: 0; -} - -.pdf-content { - flex: 1; - overflow: auto; - display: flex; - justify-content: center; - align-items: flex-start; - padding: 20px; -} - -.pdf-content .react-pdf__Document { - display: flex; - justify-content: center; -} - -.pdf-content .react-pdf__Page { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - margin-bottom: 20px; - background: #fff; -} - -.pdf-content .react-pdf__Page canvas { - max-width: 100%; - height: auto !important; -} - -.pdf-loading { - display: flex; - justify-content: center; - align-items: center; - min-height: 200px; - color: #999; - font-size: 14px; -} - -.pdf-error { - display: flex; - justify-content: center; - align-items: center; - min-height: 200px; - color: #f5222d; - font-size: 14px; -} diff --git a/frontend/src/components/shared/PDFViewer/PDFViewer.jsx b/frontend/src/components/shared/PDFViewer/PDFViewer.jsx deleted file mode 100644 index 1fc59b6..0000000 --- a/frontend/src/components/shared/PDFViewer/PDFViewer.jsx +++ /dev/null @@ -1,138 +0,0 @@ -import { useState, useMemo } from 'react' -import { Document, Page, pdfjs } from 'react-pdf' -import { Button, Space, InputNumber, Spin, App } from 'antd' -import { - LeftOutlined, - RightOutlined, - ZoomInOutlined, - ZoomOutOutlined, -} from '@ant-design/icons' -import 'react-pdf/dist/Page/AnnotationLayer.css' -import 'react-pdf/dist/Page/TextLayer.css' -import './PDFViewer.css' - -// 配置 PDF.js worker - 使用本地文件 -pdfjs.GlobalWorkerOptions.workerSrc = '/pdf-worker/pdf.worker.min.mjs' - -function PDFViewer({ url, filename }) { - const { message } = App.useApp(); - const [numPages, setNumPages] = useState(null) - const [pageNumber, setPageNumber] = useState(1) - const [scale, setScale] = useState(1.0) - - // 使用 useMemo 避免不必要的重新加载 - const fileConfig = useMemo(() => ({ url }), [url]) - - const onDocumentLoadSuccess = ({ numPages }) => { - setNumPages(numPages) - setPageNumber(1) - } - - const onDocumentLoadError = (error) => { - message.error('PDF文件加载失败') - } - - const goToPrevPage = () => { - setPageNumber((prev) => Math.max(prev - 1, 1)) - } - - const goToNextPage = () => { - setPageNumber((prev) => Math.min(prev + 1, numPages)) - } - - const zoomIn = () => { - setScale((prev) => Math.min(prev + 0.2, 3.0)) - } - - const zoomOut = () => { - setScale((prev) => Math.max(prev - 0.2, 0.5)) - } - - const handlePageChange = (value) => { - if (value >= 1 && value <= numPages) { - setPageNumber(value) - } - } - - return ( -
- {/* 工具栏 */} -
- - - - - - - - - - - - - {Math.round(scale * 100)}% - - - -
- - {/* PDF内容区 */} -
- - -
正在加载PDF...
-
- } - error={
PDF加载失败,请稍后重试
} - > - - -
正在渲染页面...
-
- } - /> - - - - ) -} - -export default PDFViewer diff --git a/frontend/src/components/shared/PDFViewer/VirtualPDFViewer.css b/frontend/src/components/shared/PDFViewer/VirtualPDFViewer.css deleted file mode 100644 index e95ef79..0000000 --- a/frontend/src/components/shared/PDFViewer/VirtualPDFViewer.css +++ /dev/null @@ -1,132 +0,0 @@ -.virtual-pdf-viewer-container { - display: flex; - flex-direction: column; - height: 100%; - background: var(--bg-color-secondary); -} - -.pdf-toolbar { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - background: var(--card-bg); - border-bottom: 1px solid var(--border-color); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); - z-index: 10; - color: var(--text-color); -} - -.pdf-content { - flex: 1; - overflow: auto; - position: relative; -} - -.pdf-virtual-list { - background: var(--bg-color-secondary); -} - -.pdf-page-wrapper { - display: flex; - flex-direction: column; - align-items: center; - padding: 20px; - background: var(--bg-color-secondary); -} - -.pdf-page-wrapper canvas { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - background: white; - margin-bottom: 8px; -} - -.pdf-page-number { - margin-top: 8px; - font-size: 13px; - color: var(--text-color); - font-weight: 600; - text-align: center; -} - -.pdf-page-loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 600px; - gap: 8px; - color: var(--text-color-secondary); - font-size: 14px; -} - -.pdf-page-placeholder { - display: flex; - align-items: center; - justify-content: center; - min-height: 600px; - background: var(--item-hover-bg); - border: 1px dashed var(--border-color); - color: var(--text-color-secondary); - font-size: 14px; -} - -.pdf-page-skeleton { - display: flex; - justify-content: center; - align-items: center; - padding: 20px; - min-height: 800px; -} - -.pdf-page-error { - display: flex; - justify-content: center; - align-items: center; - min-height: 400px; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 4px; - margin: 20px; -} - -.pdf-page-error p { - color: #ff4d4f; - font-size: 14px; -} - -.pdf-loading { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 100%; - min-height: 400px; - color: var(--text-color); -} - -.pdf-error { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - min-height: 400px; - color: #ff4d4f; - font-size: 16px; -} - -/* 文本层样式优化 */ -.react-pdf__Page__textContent { - user-select: text; -} - -/* 注释层样式优化 */ -.react-pdf__Page__annotations { - user-select: none; -} - -/* Document容器样式 - 确保不限制高度 */ -.react-pdf__Document { - height: 100%; - width: 100%; -} \ No newline at end of file diff --git a/frontend/src/components/shared/PDFViewer/VirtualPDFViewer.jsx b/frontend/src/components/shared/PDFViewer/VirtualPDFViewer.jsx deleted file mode 100644 index 51df365..0000000 --- a/frontend/src/components/shared/PDFViewer/VirtualPDFViewer.jsx +++ /dev/null @@ -1,268 +0,0 @@ -import { useState, useMemo, useRef, useEffect, useCallback } from 'react' -import { Document, Page, pdfjs } from 'react-pdf' -import { Button, Space, InputNumber, App, Spin } from 'antd' -import { - ZoomInOutlined, - ZoomOutOutlined, - VerticalAlignTopOutlined, - LeftOutlined, - RightOutlined, -} from '@ant-design/icons' -import 'react-pdf/dist/Page/AnnotationLayer.css' -import 'react-pdf/dist/Page/TextLayer.css' -import './VirtualPDFViewer.css' - -// 配置 PDF.js worker -pdfjs.GlobalWorkerOptions.workerSrc = '/pdf-worker/pdf.worker.min.mjs' - -function VirtualPDFViewer({ url, filename }) { - const { message } = App.useApp(); - const [numPages, setNumPages] = useState(null) - const [scale, setScale] = useState(1.0) - const [pdfOriginalSize, setPdfOriginalSize] = useState({ width: 595, height: 842 }) // 默认 A4 - const [currentPage, setCurrentPage] = useState(1) - const [visiblePages, setVisiblePages] = useState(new Set([1])) - const containerRef = useRef(null) - const pageRefs = useRef({}) - - // 使用 useMemo 避免不必要的重新加载 - const fileConfig = useMemo(() => ({ url }), [url]) - - // 离线部署环境不依赖 unpkg 等外部静态资源。 - const pdfOptions = useMemo(() => undefined, []) - - // 根据 PDF 实际宽高和缩放比例计算页面高度 - const pageHeight = useMemo(() => { - // 计算内容高度:缩放后的 PDF 高度 + 上下 padding (40px) + 页码文字区域 (20px) - return Math.ceil(pdfOriginalSize.height * scale) + 60 - }, [scale, pdfOriginalSize.height]) - - const onDocumentLoadError = (error) => { - console.error('[PDF] Document load error:', error) - message.error('PDF文件加载失败') - } - - - - // Handle scroll to update visible pages - const handleScroll = useCallback(() => { - if (!containerRef.current || !numPages) return - - const container = containerRef.current - const scrollTop = container.scrollTop - const containerHeight = container.clientHeight - - // Calculate which pages are visible - // Add small tolerance (1px) to handle browser scroll precision issues - const pageIndex = scrollTop / pageHeight - let firstVisiblePage = Math.max(1, Math.ceil(pageIndex + 0.001)) - - // Special case: if scrolled to bottom, show last page - const isAtBottom = scrollTop + containerHeight >= container.scrollHeight - 1 - if (isAtBottom) { - firstVisiblePage = numPages - } - - const lastVisiblePage = Math.min(numPages, Math.ceil((scrollTop + containerHeight) / pageHeight)) - - - - // Add buffer pages (2 before and 2 after) - const newVisiblePages = new Set() - for (let i = Math.max(1, firstVisiblePage - 2); i <= Math.min(numPages, lastVisiblePage + 2); i++) { - newVisiblePages.add(i) - } - - setVisiblePages(newVisiblePages) - setCurrentPage(firstVisiblePage) - }, [numPages, pageHeight]) - - const onDocumentLoadSuccess = useCallback(async (pdf) => { - setNumPages(pdf.numPages) - - try { - // 获取第一页的原始尺寸,用于计算初始缩放 - const page = await pdf.getPage(1) - const viewport = page.getViewport({ scale: 1.0 }) - const { width, height } = viewport - setPdfOriginalSize({ width, height }) - - // 自动适应宽度:仅当 PDF 宽度超过容器时才进行缩放 - if (containerRef.current) { - const containerWidth = containerRef.current.clientWidth - 40 // 减去左右内边距 - if (width > containerWidth) { - const autoScale = Math.floor((containerWidth / width) * 10) / 10 // 保留一位小数 - setScale(Math.min(Math.max(autoScale, 0.5), 1.0)) // 限制缩放比例,最高不超 1.0 - } else { - setScale(1.0) // 宽度足够则保持 100% - } - } - } catch (err) { - console.error('Error calculating initial scale:', err) - } - - // Initially show first 3 pages - setVisiblePages(new Set([1, 2, 3])) - - // Trigger scroll calculation after a short delay to ensure DOM is ready - setTimeout(() => { - handleScroll() - }, 200) - }, [handleScroll]) - - // Attach scroll listener - useEffect(() => { - const container = containerRef.current - if (!container) return - - container.addEventListener('scroll', handleScroll) - return () => container.removeEventListener('scroll', handleScroll) - }, [handleScroll, numPages, pageHeight]) - - const zoomIn = () => { - setScale((prev) => Math.min(prev + 0.2, 3.0)) - } - - const zoomOut = () => { - setScale((prev) => Math.max(prev - 0.2, 0.5)) - } - - const handlePageChange = (value) => { - if (value >= 1 && value <= numPages && containerRef.current) { - const scrollTop = (value - 1) * pageHeight - const container = containerRef.current - container.scrollTo({ top: scrollTop, behavior: 'auto' }) - - // Manually trigger handleScroll after scrolling to ensure page number updates - setTimeout(() => { - handleScroll() - }, 50) - } - } - - const scrollToTop = () => { - if (containerRef.current) { - containerRef.current.scrollTo({ top: 0, behavior: 'smooth' }) - } - } - - return ( -
- {/* 工具栏 */} -
- - - - - - - - - - {Math.round(scale * 100)}% - - - -
- - {/* PDF内容区 - 自定义虚拟滚动 */} -
- - -
正在加载PDF...
-
- } - error={
PDF加载失败,请稍后重试
} - options={pdfOptions} - > - {numPages && ( -
- {Array.from({ length: numPages }, (_, index) => { - const pageNumber = index + 1 - const isVisible = visiblePages.has(pageNumber) - - return ( -
pageRefs.current[pageNumber] = el} - className="pdf-page-wrapper" - style={{ - position: 'absolute', - top: index * pageHeight, - left: 0, - right: 0, - height: pageHeight, - }} - > - {isVisible ? ( - <> - - -
加载第 {pageNumber} 页...
-
- } - /> -
第 {pageNumber} 页
- - ) : ( -
-
第 {pageNumber} 页
-
- )} -
- ) - })} -
- )} - - - - ) -} - -export default VirtualPDFViewer diff --git a/frontend/src/components/shared/ProtectedRoute.jsx b/frontend/src/components/shared/ProtectedRoute.jsx deleted file mode 100644 index a6b1512..0000000 --- a/frontend/src/components/shared/ProtectedRoute.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Navigate } from 'react-router-dom' -import useUserStore from '@/stores/userStore' - -function ProtectedRoute({ children }) { - const token = localStorage.getItem('access_token') - - if (!token) { - return - } - - return children -} - -export default ProtectedRoute diff --git a/frontend/src/components/shared/SelectionAlert/SelectionAlert.css b/frontend/src/components/shared/SelectionAlert/SelectionAlert.css deleted file mode 100644 index bfd2069..0000000 --- a/frontend/src/components/shared/SelectionAlert/SelectionAlert.css +++ /dev/null @@ -1,49 +0,0 @@ -.selection-alert-container { - margin-bottom: 16px; -} - -.selection-alert-content { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; -} - -.selection-alert-content span { - flex: 1; - color: rgba(0, 0, 0, 0.85); -} - -.selection-alert-content strong { - color: #1677ff; - font-weight: 600; - margin: 0 4px; -} - -.selection-alert-content a { - color: #1677ff; - cursor: pointer; - white-space: nowrap; - transition: all 0.2s; - text-decoration: none; - padding: 0 8px; - border-radius: 4px; -} - -.selection-alert-content a:hover { - background: rgba(22, 119, 255, 0.08); - text-decoration: underline; -} - -/* 响应式处理 */ -@media (max-width: 768px) { - .selection-alert-content { - flex-direction: column; - align-items: flex-start; - gap: 8px; - } - - .selection-alert-content a { - padding: 4px 8px; - } -} diff --git a/frontend/src/components/shared/SelectionAlert/SelectionAlert.jsx b/frontend/src/components/shared/SelectionAlert/SelectionAlert.jsx deleted file mode 100644 index 82502cd..0000000 --- a/frontend/src/components/shared/SelectionAlert/SelectionAlert.jsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Alert } from 'antd' -import './SelectionAlert.css' - -/** - * 全选提示条组件 - * @param {Object} props - * @param {number} props.currentPageCount - 当前页选中数量 - * @param {number} props.totalCount - 总数据量 - * @param {boolean} props.isAllPagesSelected - 是否已选择所有页 - * @param {Function} props.onSelectAllPages - 选择所有页的回调 - * @param {Function} props.onClearSelection - 清除选择的回调 - */ -function SelectionAlert({ - currentPageCount, - totalCount, - isAllPagesSelected, - onSelectAllPages, - onClearSelection, -}) { - // 如果没有选中任何项,不显示 - if (currentPageCount === 0) { - return null - } - - // 如果已选择所有页 - if (isAllPagesSelected) { - return ( -
- - - 已选择全部 {totalCount} 条数据 - - 清除选择 -
- } - type="info" - showIcon - closable={false} - /> - - ) - } - - // 如果只选择了当前页,且总数大于当前页 - if (currentPageCount > 0 && totalCount > currentPageCount) { - return ( -
- - - 已选择当前页 {currentPageCount} 条数据 - - - 选择全部 {totalCount} 条数据 - -
- } - type="warning" - showIcon - closable={false} - /> - - ) - } - - // 只选择了部分数据,且总数等于当前页(单页情况) - return ( -
- - - 已选择 {currentPageCount} 条数据 - - 清除选择 -
- } - type="info" - showIcon - closable={false} - /> - - ) -} - -export default SelectionAlert diff --git a/frontend/src/components/shared/SideInfoPanel/SideInfoPanel.css b/frontend/src/components/shared/SideInfoPanel/SideInfoPanel.css deleted file mode 100644 index f6f38a4..0000000 --- a/frontend/src/components/shared/SideInfoPanel/SideInfoPanel.css +++ /dev/null @@ -1,88 +0,0 @@ -/* 侧边信息面板容器 */ -.side-info-panel { - display: flex; - flex-direction: column; - gap: 16px; -} - -/* 信息区块 */ -.side-info-section { - background: #ffffff; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); - overflow: hidden; - transition: all 0.3s ease; -} - -.side-info-section:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -/* 区块头部 */ -.side-info-section-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 20px; - background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%); - border-bottom: 1px solid #e8e8e8; - cursor: pointer; - user-select: none; - transition: background 0.2s ease; -} - -.side-info-section-header:hover { - background: linear-gradient(135deg, #f0f4ff 0%, #e8f0ff 100%); -} - -.side-info-section-title { - display: flex; - align-items: center; - gap: 8px; - font-size: 14px; - font-weight: 600; - color: rgba(0, 0, 0, 0.88); -} - -.side-info-section-icon { - display: flex; - align-items: center; - font-size: 16px; - color: #1677ff; -} - -.side-info-section-toggle { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border: none; - background: transparent; - color: #8c8c8c; - cursor: pointer; - transition: all 0.2s ease; - border-radius: 4px; -} - -.side-info-section-toggle:hover { - background: rgba(0, 0, 0, 0.06); - color: #1677ff; -} - -/* 区块内容 */ -.side-info-section-content { - padding: 16px 20px; - animation: expandContent 0.3s ease-out; -} - -@keyframes expandContent { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} diff --git a/frontend/src/components/shared/SideInfoPanel/SideInfoPanel.jsx b/frontend/src/components/shared/SideInfoPanel/SideInfoPanel.jsx deleted file mode 100644 index b08e4bc..0000000 --- a/frontend/src/components/shared/SideInfoPanel/SideInfoPanel.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useState } from 'react' -import { UpOutlined, DownOutlined } from '@ant-design/icons' -import './SideInfoPanel.css' - -/** - * 侧边信息面板组件 - * @param {Object} props - * @param {Array} props.sections - 信息区块配置数组 - * @param {string} props.className - 自定义类名 - */ -function SideInfoPanel({ sections = [], className = '' }) { - const [collapsedSections, setCollapsedSections] = useState(() => { - const initial = {} - sections.forEach((section) => { - if (section.defaultCollapsed) { - initial[section.key] = true - } - }) - return initial - }) - - const toggleSection = (key) => { - setCollapsedSections((prev) => ({ - ...prev, - [key]: !prev[key], - })) - } - - return ( -
- {sections.map((section) => { - const isCollapsed = collapsedSections[section.key] - - return ( -
- {/* 区块头部 */} -
toggleSection(section.key)}> -
- {section.icon && {section.icon}} - {section.title} -
- -
- - {/* 区块内容 */} - {!isCollapsed && ( -
{section.content}
- )} -
- ) - })} -
- ) -} - -export default SideInfoPanel diff --git a/frontend/src/components/shared/SplitLayout/SplitLayout.css b/frontend/src/components/shared/SplitLayout/SplitLayout.css deleted file mode 100644 index 097fa77..0000000 --- a/frontend/src/components/shared/SplitLayout/SplitLayout.css +++ /dev/null @@ -1,72 +0,0 @@ -/* 分栏布局容器 */ -.split-layout { - display: flex; - width: 100%; - align-items: flex-start; -} - -/* 横向布局(左右分栏) */ -.split-layout-horizontal { - flex-direction: row; -} - -/* 纵向布局(上下分栏) */ -.split-layout-vertical { - flex-direction: column; -} - -/* 主内容区 */ -.split-layout-main { - flex: 1; - min-width: 0; - width: 100%; - display: flex; - flex-direction: column; -} - -/* 扩展信息区 */ -.split-layout-extend { - flex-shrink: 0; - background: #ffffff; -} - -/* 右侧扩展区(横向布局) */ -.split-layout-extend-right { - height: 693px; - overflow-y: auto; - overflow-x: hidden; - position: sticky; - top: 16px; - padding-right: 4px; -} - -/* 顶部扩展区(纵向布局) */ -.split-layout-extend-top { - width: 100%; -} - -/* 滚动条样式(横向布局右侧扩展区) */ -.split-layout-extend-right::-webkit-scrollbar { - width: 6px; -} - -.split-layout-extend-right::-webkit-scrollbar-track { - background: #f5f5f5; - border-radius: 3px; -} - -.split-layout-extend-right::-webkit-scrollbar-thumb { - background: #d9d9d9; - border-radius: 3px; -} - -.split-layout-extend-right::-webkit-scrollbar-thumb:hover { - background: #bfbfbf; -} - -/* 响应式:小屏幕时隐藏右侧扩展区 */ -@media (max-width: 1200px) { - .split-layout-extend-right { - display: none; - } -} diff --git a/frontend/src/components/shared/SplitLayout/SplitLayout.jsx b/frontend/src/components/shared/SplitLayout/SplitLayout.jsx deleted file mode 100644 index b54a2f7..0000000 --- a/frontend/src/components/shared/SplitLayout/SplitLayout.jsx +++ /dev/null @@ -1,69 +0,0 @@ -import './SplitLayout.css' - -/** - * 主内容区布局组件 - * @param {Object} props - * @param {string} props.direction - 布局方向:'horizontal'(左右)| 'vertical'(上下) - * @param {ReactNode} props.mainContent - 主内容区 - * @param {ReactNode} props.extendContent - 扩展内容区 - * @param {number} props.extendSize - 扩展区尺寸(horizontal 模式下为宽度,px) - * @param {number} props.gap - 主内容与扩展区间距(px) - * @param {boolean} props.showExtend - 是否显示扩展区 - * @param {string} props.extendPosition - 扩展区位置(horizontal: 'right', vertical: 'top') - * @param {string} props.className - 自定义类名 - * - * @deprecated 旧参数(向后兼容):leftContent, rightContent, rightWidth, showRight - */ -function SplitLayout({ - // 新 API - direction = 'horizontal', - mainContent, - extendContent, - extendSize = 360, - gap = 16, - showExtend = true, - extendPosition, - className = '', - // 旧 API(向后兼容) - leftContent, - rightContent, - rightWidth, - showRight, -}) { - // 向后兼容:如果使用旧 API,转换为新 API - const actualMainContent = mainContent || leftContent - const actualExtendContent = extendContent || rightContent - const actualExtendSize = extendSize !== 360 ? extendSize : (rightWidth || 360) - const actualShowExtend = showExtend !== undefined ? showExtend : (showRight !== undefined ? showRight : true) - const actualDirection = direction - const actualExtendPosition = extendPosition || (actualDirection === 'horizontal' ? 'right' : 'top') - - return ( -
- {/* 纵向布局且扩展区在顶部时,先渲染扩展区 */} - {actualDirection === 'vertical' && actualExtendPosition === 'top' && actualShowExtend && actualExtendContent && ( -
- {actualExtendContent} -
- )} - - {/* 主内容区 */} -
{actualMainContent}
- - {/* 横向布局时,扩展区在右侧 */} - {actualDirection === 'horizontal' && actualShowExtend && actualExtendContent && ( -
- {actualExtendContent} -
- )} -
- ) -} - -export default SplitLayout diff --git a/frontend/src/components/shared/StatCard/StatCard.css b/frontend/src/components/shared/StatCard/StatCard.css deleted file mode 100644 index 509d577..0000000 --- a/frontend/src/components/shared/StatCard/StatCard.css +++ /dev/null @@ -1,108 +0,0 @@ -/* 统计卡片 */ -.stat-card { - padding: 16px; - background: #ffffff; - border-radius: 8px; - border: 1px solid #f0f0f0; - transition: all 0.3s ease; -} - -.stat-card:hover { - border-color: #d9d9d9; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); -} - -/* 一列布局(默认) */ -.stat-card-column { - /* 继承默认样式 */ -} - -/* 两列布局 */ -.stat-card.stat-card-row { - display: flex; - align-items: center; - gap: 16px; -} - -.stat-card.stat-card-row .stat-card-header { - flex: 1; - margin-bottom: 0; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 8px; -} - -.stat-card.stat-card-row .stat-card-body { - flex-shrink: 0; - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 4px; -} - -/* 卡片头部 */ -.stat-card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; -} - -.stat-card-title { - font-size: 13px; - color: rgba(0, 0, 0, 0.65); - font-weight: 500; -} - -.stat-card-icon { - font-size: 18px; - display: flex; - align-items: center; -} - -/* 卡片内容 */ -.stat-card-body { - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: 8px; -} - -.stat-card-value { - font-size: 24px; - font-weight: 600; - line-height: 1; -} - -.stat-card-suffix { - font-size: 14px; - font-weight: 400; - margin-left: 4px; - color: rgba(0, 0, 0, 0.45); -} - -/* 趋势指示器 */ -.stat-card-trend { - display: flex; - align-items: center; - gap: 4px; - font-size: 12px; - font-weight: 500; - padding: 2px 6px; - border-radius: 4px; -} - -.stat-card-trend.trend-up { - color: #52c41a; - background: #f6ffed; -} - -.stat-card-trend.trend-down { - color: #ff4d4f; - background: #fff1f0; -} - -.stat-card-trend svg { - font-size: 10px; -} diff --git a/frontend/src/components/shared/StatCard/StatCard.tsx b/frontend/src/components/shared/StatCard/StatCard.tsx deleted file mode 100644 index 92c44c1..0000000 --- a/frontend/src/components/shared/StatCard/StatCard.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons' -import './StatCard.css' -import { ReactNode } from 'react' - -export interface StatCardProps { - title: string - value: number | string - icon?: ReactNode - color?: string - trend?: { value: number; direction: 'up' | 'down' } - suffix?: string - layout?: 'column' | 'row' - gridColumn?: string - className?: string - onClick?: () => void - style?: React.CSSProperties -} - -/** - * 统计卡片组件 - */ -function StatCard({ - title, - value, - icon, - color = 'blue', - trend, - suffix = '', - layout = 'column', - gridColumn, - className = '', - onClick, - style: customStyle = {}, -}: StatCardProps) { - const colorMap: Record = { - blue: '#1677ff', - green: '#52c41a', - orange: '#faad14', - red: '#ff4d4f', - purple: '#722ed1', - gray: '#8c8c8c', - } - - const themeColor = colorMap[color] || color - - const style = { - ...(gridColumn ? { gridColumn } : {}), - ...customStyle, - } - - return ( -
-
- {title} - {icon && ( - - )} -
- -
-
- {value} - {suffix && {suffix}} -
- - {trend && ( -
- {trend.direction === 'up' ?
- )} -
-
- ) -} - -export default StatCard \ No newline at end of file diff --git a/frontend/src/components/shared/Toast/Toast.tsx b/frontend/src/components/shared/Toast/Toast.tsx deleted file mode 100644 index 5049247..0000000 --- a/frontend/src/components/shared/Toast/Toast.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { notification } from 'antd'; -import { - CheckCircleOutlined, - CloseCircleOutlined, - ExclamationCircleOutlined, - InfoCircleOutlined, -} from "@ant-design/icons"; - -notification.config({ - placement: "topRight", - top: 24, - duration: 3, - maxCount: 3, -}); - -const Toast = { - success: (message: string, description = "", duration = 3) => { - notification.success({ - message, - description, - duration, - icon: , - style: { - borderRadius: "8px", - boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", - }, - }); - }, - - error: (message: string, description = "", duration = 3) => { - notification.error({ - message, - description, - duration, - icon: , - style: { - borderRadius: "8px", - boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", - }, - }); - }, - - warning: (message: string, description = "", duration = 3) => { - notification.warning({ - message, - description, - duration, - icon: , - style: { - borderRadius: "8px", - boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", - }, - }); - }, - - info: (message: string, description = "", duration = 3) => { - notification.info({ - message, - description, - duration, - icon: , - style: { - borderRadius: "8px", - boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", - }, - }); - }, - - custom: (config: any) => { - notification.open({ - ...config, - style: { - borderRadius: "8px", - boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", - ...config.style, - }, - }); - }, -}; - -export default Toast; diff --git a/frontend/src/components/shared/TreeFilterPanel/TreeFilterPanel.css b/frontend/src/components/shared/TreeFilterPanel/TreeFilterPanel.css deleted file mode 100644 index fd6341c..0000000 --- a/frontend/src/components/shared/TreeFilterPanel/TreeFilterPanel.css +++ /dev/null @@ -1,58 +0,0 @@ -/* 树形筛选面板 */ -.tree-filter-panel { - width: 320px; - max-height: 500px; - overflow-y: auto; -} - -/* 已选择的筛选条件 */ -.tree-filter-selected { - min-height: 40px; - padding: 12px; - background: #f5f7fa; - border-radius: 6px; - border: 1px dashed #d9d9d9; -} - -.tree-filter-tag { - display: flex; - align-items: center; - gap: 8px; -} - -.tree-filter-label { - font-size: 13px; - color: rgba(0, 0, 0, 0.65); - font-weight: 500; -} - -.tree-filter-placeholder { - display: flex; - align-items: center; - justify-content: center; - min-height: 24px; -} - -.tree-filter-placeholder span { - color: #8c8c8c; - font-size: 13px; -} - -/* 树形选择器容器 */ -.tree-filter-container { - max-height: 280px; - overflow-y: auto; -} - -.tree-filter-header { - font-size: 14px; - font-weight: 500; - margin-bottom: 12px; - color: rgba(0, 0, 0, 0.85); -} - -/* 操作按钮 */ -.tree-filter-actions { - display: flex; - justify-content: flex-end; -} diff --git a/frontend/src/components/shared/TreeFilterPanel/TreeFilterPanel.jsx b/frontend/src/components/shared/TreeFilterPanel/TreeFilterPanel.jsx deleted file mode 100644 index 2723c22..0000000 --- a/frontend/src/components/shared/TreeFilterPanel/TreeFilterPanel.jsx +++ /dev/null @@ -1,119 +0,0 @@ -import { Tree, Tag, Divider, Button, Space } from 'antd' -import { useState, useEffect } from 'react' -import './TreeFilterPanel.css' - -/** - * 树形筛选面板组件 - * @param {Object} props - * @param {Array} props.treeData - 树形数据 - * @param {string} props.selectedKey - 当前选中的节点ID - * @param {string} props.tempSelectedKey - 临时选中的节点ID(确认前) - * @param {string} props.treeTitle - 树标题 - * @param {Function} props.onSelect - 选择变化回调 - * @param {Function} props.onConfirm - 确认筛选 - * @param {Function} props.onClear - 清除筛选 - * @param {string} props.placeholder - 占位提示文本 - */ -function TreeFilterPanel({ - treeData, - selectedKey, - tempSelectedKey, - treeTitle = '分组筛选', - onSelect, - onConfirm, - onClear, - placeholder = '请选择分组进行筛选', -}) { - // 获取所有节点的key用于默认展开 - const getAllKeys = (nodes) => { - let keys = [] - const traverse = (node) => { - keys.push(node.key) - if (node.children) { - node.children.forEach(traverse) - } - } - nodes.forEach(traverse) - return keys - } - - const [expandedKeys, setExpandedKeys] = useState([]) - - // 初始化时展开所有节点 - useEffect(() => { - if (treeData && treeData.length > 0) { - setExpandedKeys(getAllKeys(treeData)) - } - }, [treeData]) - - // 查找节点名称 - const findNodeName = (nodes, id) => { - for (const node of nodes) { - if (node.key === id) return node.title - if (node.children) { - const found = findNodeName(node.children, id) - if (found) return found - } - } - return '' - } - - const handleTreeSelect = (selectedKeys) => { - const key = selectedKeys[0] || null - onSelect?.(key) - } - - const handleExpand = (keys) => { - setExpandedKeys(keys) - } - - return ( -
- {/* 已选择的筛选条件 */} -
- {tempSelectedKey ? ( -
- 已选择分组: - onSelect?.(null)}> - {findNodeName(treeData, tempSelectedKey)} - -
- ) : ( -
- {placeholder} -
- )} -
- - - - {/* 树形选择器 */} -
-
{treeTitle}
- -
- - - - {/* 操作按钮 */} -
- - - - -
-
- ) -} - -export default TreeFilterPanel diff --git a/frontend/src/index.css b/frontend/src/index.css index ae10293..3117af9 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -11,6 +11,7 @@ linear-gradient(90deg, rgba(255, 255, 255, 0.34) 1px, transparent 1px); --app-bg-overlay-size: 36px 36px; --app-bg-card: rgba(255, 255, 255, 0.74); + --app-bg-layout: #f5f6fa; --app-text-main: #1f2937; --app-text-secondary: #66758f; --app-border-color: rgba(103, 126, 189, 0.12); @@ -19,6 +20,7 @@ --app-bg-surface: rgba(255, 255, 255, 0.68); --app-bg-surface-soft: rgba(255, 255, 255, 0.56); --app-bg-surface-strong: rgba(255, 255, 255, 0.82); + --app-surface-color: rgba(255, 255, 255, 0.82); --app-text-muted: #66758f; --item-hover-bg: rgba(22, 119, 255, 0.08); --text-color-secondary: #66758f; @@ -35,6 +37,7 @@ linear-gradient(rgba(255, 255, 255, 0.24) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.24) 1px, transparent 1px); --app-bg-card: rgba(255, 255, 255, 0.82); + --app-bg-layout: #f5f6fa; --app-text-main: #111827; --app-text-secondary: #5b6474; --app-border-color: rgba(148, 163, 184, 0.16); @@ -43,6 +46,7 @@ --app-bg-surface: rgba(255, 255, 255, 0.74); --app-bg-surface-soft: rgba(255, 255, 255, 0.62); --app-bg-surface-strong: rgba(255, 255, 255, 0.86); + --app-surface-color: rgba(255, 255, 255, 0.86); --app-text-muted: #5b6474; --item-hover-bg: rgba(22, 119, 255, 0.08); --text-color-secondary: #5b6474; @@ -58,6 +62,7 @@ linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px); --app-bg-card: rgba(13, 23, 39, 0.62); + --app-bg-layout: #08101c; --app-text-main: #e2e8f0; --app-text-secondary: rgba(190, 206, 229, 0.74); --app-border-color: rgba(88, 151, 255, 0.18); @@ -66,6 +71,7 @@ --app-bg-surface: rgba(10, 21, 37, 0.78); --app-bg-surface-soft: rgba(10, 21, 37, 0.72); --app-bg-surface-strong: rgba(8, 17, 31, 0.88); + --app-surface-color: rgba(8, 17, 31, 0.88); --app-text-muted: rgba(190, 206, 229, 0.74); --item-hover-bg: rgba(88, 151, 255, 0.18); --text-color-secondary: rgba(190, 206, 229, 0.74); @@ -1306,6 +1312,7 @@ body::after { --app-bg-main: #f5f6fa; --app-bg-overlay: none; --app-bg-card: #fff; + --app-bg-layout: #f5f6fa; --app-text-main: #333333; --app-text-secondary: #9095a1; --app-border-color: #e6e6e6; @@ -1314,6 +1321,7 @@ body::after { --app-bg-surface: #fff; --app-bg-surface-soft: #fafafa; --app-bg-surface-strong: #fff; + --app-surface-color: #fff; --app-text-muted: #9095a1; --text-color-secondary: #9095a1; --link-color: #1677ff; diff --git a/frontend/src/pages/access/permissions/index.tsx b/frontend/src/pages/access/permissions/index.tsx index 092b444..0f430ab 100644 --- a/frontend/src/pages/access/permissions/index.tsx +++ b/frontend/src/pages/access/permissions/index.tsx @@ -1,12 +1,33 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Button, Card, Col, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Table, Tag, Tooltip, Typography, App } from 'antd'; -import * as AntIcons from "@ant-design/icons"; +import { Button, Col, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Table, Tag, Tooltip, Typography, App } from 'antd'; import { DndContext, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core'; import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; -import { CheckSquareOutlined, ClusterOutlined, DeleteOutlined, EditOutlined, FolderOutlined, InfoCircleOutlined, MenuOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, HolderOutlined } from "@ant-design/icons"; +import { + ApartmentOutlined, + BookOutlined, + CheckSquareOutlined, + ClusterOutlined, + DashboardOutlined, + DeleteOutlined, + DesktopOutlined, + EditOutlined, + FolderOutlined, + HolderOutlined, + InfoCircleOutlined, + MenuOutlined, + PlusOutlined, + ReloadOutlined, + SafetyCertificateOutlined, + SearchOutlined, + SettingOutlined, + ShopOutlined, + TeamOutlined, + UserOutlined, + VideoCameraOutlined +} from "@ant-design/icons"; import { createPermission, deletePermission, listMyPermissions, updatePermission } from "@/api"; import { useDict } from "@/hooks/useDict"; import { usePermission } from "@/hooks/usePermission"; @@ -32,6 +53,19 @@ const legacyIconAliases: Record = { setting: "SettingOutlined" }; +const menuIconComponents: Record> = { + ApartmentOutlined, + BookOutlined, + DashboardOutlined, + DesktopOutlined, + SafetyCertificateOutlined, + SettingOutlined, + ShopOutlined, + TeamOutlined, + UserOutlined, + VideoCameraOutlined +}; + interface RowProps extends React.HTMLAttributes { 'data-row-key': string; } @@ -80,15 +114,12 @@ const DragHandle = () => { ); }; -const menuIconOptions = Object.keys(AntIcons) - .filter((key) => /(?:Outlined|Filled|TwoTone)$/.test(key)) - .sort((left, right) => left.localeCompare(right)); +const menuIconOptions = Object.keys(menuIconComponents).sort((left, right) => left.localeCompare(right)); function renderSelectableIcon(iconName?: string) { const resolvedName = iconName ? (legacyIconAliases[iconName] || iconName) : undefined; if (!resolvedName) return null; - const iconsMap = AntIcons as unknown as Record>; - const IconComponent = iconsMap[resolvedName]; + const IconComponent = menuIconComponents[resolvedName]; return IconComponent ?