优化样式,添加明暗主题
This commit is contained in:
		
							parent
							
								
									d1ae365964
								
							
						
					
					
						commit
						6cd942fb29
					
				
							
								
								
									
										414
									
								
								front/THEME_GUIDE.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										414
									
								
								front/THEME_GUIDE.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,414 @@ | |||||||
|  | # 主题系统使用指南 | ||||||
|  | 
 | ||||||
|  | ## 概述 | ||||||
|  | 
 | ||||||
|  | 项目已成功集成完整的亮色/暗色主题系统,支持一键切换并自动保存用户偏好。 | ||||||
|  | 
 | ||||||
|  | ## ✨ 主要特性 | ||||||
|  | 
 | ||||||
|  | - ✅ 完整的亮色和暗色主题定义 | ||||||
|  | - ✅ 自动检测系统主题偏好 | ||||||
|  | - ✅ 用户偏好持久化(localStorage) | ||||||
|  | - ✅ 监听系统主题变化 | ||||||
|  | - ✅ 平滑的过渡动画 | ||||||
|  | - ✅ 50+ CSS 变量覆盖所有UI场景 | ||||||
|  | - ✅ TypeScript 类型支持 | ||||||
|  | - ✅ 响应式数据绑定 | ||||||
|  | 
 | ||||||
|  | ## 🚀 快速开始 | ||||||
|  | 
 | ||||||
|  | ### 1. 全局初始化(已完成) | ||||||
|  | 
 | ||||||
|  | 主题系统已在 `App.vue` 中全局初始化,整个应用自动支持主题切换功能。 | ||||||
|  | 
 | ||||||
|  | ### 2. 主题切换按钮 | ||||||
|  | 
 | ||||||
|  | 在头部工具栏可以看到主题切换按钮(🌙/☀️图标),点击即可切换主题。 | ||||||
|  | 
 | ||||||
|  | ### 3. 手动切换主题 | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | import { useTheme } from '@/utils/theme'; | ||||||
|  | 
 | ||||||
|  | // 在组件中使用 | ||||||
|  | const { toggle, setTheme, currentTheme, isDark } = useTheme(); | ||||||
|  | 
 | ||||||
|  | // 切换主题 | ||||||
|  | toggle(); | ||||||
|  | 
 | ||||||
|  | // 设置为特定主题 | ||||||
|  | setTheme('dark'); // 或 'light' | ||||||
|  | 
 | ||||||
|  | // 获取当前主题状态 | ||||||
|  | console.log(isDark()); // true 或 false | ||||||
|  | console.log(currentTheme); // 'light' 或 'dark' | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 4. 在样式文件中使用主题变量 | ||||||
|  | 
 | ||||||
|  | ```scss | ||||||
|  | .my-component { | ||||||
|  |   background: var(--background-color); | ||||||
|  |   color: var(--text-color); | ||||||
|  |   border: 1px solid var(--border-color); | ||||||
|  |    | ||||||
|  |   &:hover { | ||||||
|  |     background: var(--background-hover); | ||||||
|  |     border-color: var(--border-color-hover); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## 📋 可用的 CSS 变量 | ||||||
|  | 
 | ||||||
|  | ### 主题标识 | ||||||
|  | - `--theme-mode`: 当前主题模式 ('light' 或 'dark') | ||||||
|  | 
 | ||||||
|  | ### 主要颜色 | ||||||
|  | - `--primary-color`, `--primary-hover`, `--primary-active` | ||||||
|  | - `--secondary-color`, `--secondary-hover`, `--secondary-active` | ||||||
|  | - `--accent-color`, `--accent-hover` | ||||||
|  | 
 | ||||||
|  | ### 背景颜色 | ||||||
|  | - `--background-color`: 主要背景 | ||||||
|  | - `--background-secondary`, `--background-tertiary` | ||||||
|  | - `--background-hover` | ||||||
|  | 
 | ||||||
|  | ### 文本颜色 | ||||||
|  | - `--text-color`: 主要文本 | ||||||
|  | - `--text-secondary`, `--text-tertiary` | ||||||
|  | - `--text-inverse`: 反转文本色 | ||||||
|  | 
 | ||||||
|  | ### 边框颜色 | ||||||
|  | - `--border-color`, `--border-color-hover`, `--border-color-active` | ||||||
|  | 
 | ||||||
|  | ### 状态颜色 | ||||||
|  | - `--success-color`, `--success-bg` | ||||||
|  | - `--warning-color`, `--warning-bg` | ||||||
|  | - `--error-color`, `--error-bg` | ||||||
|  | - `--info-color`, `--info-bg` | ||||||
|  | 
 | ||||||
|  | ### 组件颜色 | ||||||
|  | - `--card-bg`, `--card-shadow` | ||||||
|  | - `--sidebar-bg`, `--sidebar-hover` | ||||||
|  | - `--header-bg` | ||||||
|  | 
 | ||||||
|  | ### 阴影 | ||||||
|  | - `--shadow-sm`, `--shadow-md`, `--shadow-lg`, `--shadow-xl` | ||||||
|  | 
 | ||||||
|  | ### 圆角 | ||||||
|  | - `--border-radius`, `--border-radius-sm` | ||||||
|  | - `--border-radius-lg`, `--border-radius-xl` | ||||||
|  | - `--border-radius-full` | ||||||
|  | 
 | ||||||
|  | ### 过渡 | ||||||
|  | - `--transition-base`: all 0.3s | ||||||
|  | - `--transition-fast`: all 0.15s | ||||||
|  | - `--transition-slow`: all 0.5s | ||||||
|  | 
 | ||||||
|  | ### 字体 | ||||||
|  | - `--font-family-base` | ||||||
|  | 
 | ||||||
|  | ## 🎨 自定义滚动条 | ||||||
|  | 
 | ||||||
|  | 项目已经内置了自定义滚动条样式,支持主题切换: | ||||||
|  | 
 | ||||||
|  | ### 特性 | ||||||
|  | - ✅ 自动适配亮色/暗色主题 | ||||||
|  | - ✅ 支持 WebKit 和 Firefox 浏览器 | ||||||
|  | - ✅ 平滑的过渡动画 | ||||||
|  | - ✅ 悬停效果增强 | ||||||
|  | - ✅ 支持隐藏滚动条(保持滚动功能) | ||||||
|  | 
 | ||||||
|  | ### 滚动条大小 | ||||||
|  | - 宽度/高度:8px | ||||||
|  | - 适合现代 UI 风格 | ||||||
|  | - 不影响内容布局 | ||||||
|  | 
 | ||||||
|  | ### 使用方式 | ||||||
|  | 
 | ||||||
|  | **1. 默认滚动条(自动应用)** | ||||||
|  | 所有滚动容器都会自动使用主题滚动条样式。 | ||||||
|  | 
 | ||||||
|  | **2. 隐藏滚动条** | ||||||
|  | 如需隐藏滚动条但保持滚动功能: | ||||||
|  | 
 | ||||||
|  | ```vue | ||||||
|  | <div class="scrollbar-hide"> | ||||||
|  |   <!-- 内容 --> | ||||||
|  | </div> | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | **3. 自定义滚动条颜色** | ||||||
|  | 如需特定区域使用不同的滚动条颜色,可在组件样式中覆盖: | ||||||
|  | 
 | ||||||
|  | ```scss | ||||||
|  | .my-custom-scroll { | ||||||
|  |   &::-webkit-scrollbar-thumb { | ||||||
|  |     background: var(--primary-color); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   scrollbar-color: var(--primary-color) var(--background-secondary); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 浏览器兼容性 | ||||||
|  | - ✅ Chrome/Edge: 完美支持 | ||||||
|  | - ✅ Firefox: 完美支持 | ||||||
|  | - ✅ Safari: 完美支持 | ||||||
|  | - ✅ Opera: 完美支持 | ||||||
|  | - ⚠️ IE: 使用原生滚动条 | ||||||
|  | 
 | ||||||
|  | ## 📁 文件结构 | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | front/ | ||||||
|  | ├── src/ | ||||||
|  | │   ├── App.vue                    # 全局初始化主题系统 ⭐ | ||||||
|  | │   ├── main.ts                    # 入口文件,引入样式 | ||||||
|  | │   ├── assets/ | ||||||
|  | │   │   └── css/ | ||||||
|  | │   │       ├── root.scss           # 主题变量定义 | ||||||
|  | │   │       └── theme-usage.md     # 详细使用文档 | ||||||
|  | │   ├── utils/ | ||||||
|  | │   │   └── theme.ts               # 主题切换工具 | ||||||
|  | │   ├── components/ | ||||||
|  | │   │   └── ThemeToggle.vue        # 主题切换组件 | ||||||
|  | │   └── views/ | ||||||
|  | │       └── components/ | ||||||
|  | │           └── layout.vue         # 已集成主题切换按钮 | ||||||
|  | ├── THEME_GUIDE.md                 # 本文件 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## 💡 使用示例 | ||||||
|  | 
 | ||||||
|  | ### 示例 1: 基础组件 | ||||||
|  | 
 | ||||||
|  | ```vue | ||||||
|  | <template> | ||||||
|  |   <div class="card"> | ||||||
|  |     <h3>{{ title }}</h3> | ||||||
|  |     <p>{{ content }}</p> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | defineProps<{ | ||||||
|  |   title: string; | ||||||
|  |   content: string; | ||||||
|  | }>(); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | .card { | ||||||
|  |   background: var(--card-bg); | ||||||
|  |   color: var(--text-color); | ||||||
|  |   padding: 20px; | ||||||
|  |   border: 1px solid var(--border-color); | ||||||
|  |   border-radius: var(--border-radius-lg); | ||||||
|  |   box-shadow: var(--card-shadow); | ||||||
|  |   transition: var(--transition-base); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .card:hover { | ||||||
|  |   box-shadow: var(--shadow-lg); | ||||||
|  |   border-color: var(--border-color-hover); | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 示例 2: 按钮组件 | ||||||
|  | 
 | ||||||
|  | ```vue | ||||||
|  | <template> | ||||||
|  |   <button :class="buttonClass"> | ||||||
|  |     {{ text }} | ||||||
|  |   </button> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | defineProps<{ | ||||||
|  |   type?: 'primary' | 'secondary' | 'danger'; | ||||||
|  |   text: string; | ||||||
|  | }>(); | ||||||
|  | 
 | ||||||
|  | const buttonClass = computed(() => ({ | ||||||
|  |   'btn-primary': true, | ||||||
|  |   [`btn-${props.type || 'primary'}`]: true, | ||||||
|  | })); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | button { | ||||||
|  |   padding: 8px 16px; | ||||||
|  |   border-radius: var(--border-radius); | ||||||
|  |   border: none; | ||||||
|  |   cursor: pointer; | ||||||
|  |   transition: var(--transition-fast); | ||||||
|  |   font-weight: 500; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .btn-primary { | ||||||
|  |   background: var(--primary-color); | ||||||
|  |   color: var(--text-inverse); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .btn-primary:hover { | ||||||
|  |   background: var(--primary-hover); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .btn-secondary { | ||||||
|  |   background: var(--secondary-color); | ||||||
|  |   color: var(--text-inverse); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .btn-danger { | ||||||
|  |   background: var(--error-color); | ||||||
|  |   color: var(--text-inverse); | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 示例 3: 状态提示 | ||||||
|  | 
 | ||||||
|  | ```vue | ||||||
|  | <template> | ||||||
|  |   <div :class="['alert', `alert-${type}`]"> | ||||||
|  |     <i :class="iconClass"></i> | ||||||
|  |     <span>{{ message }}</span> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | const props = defineProps<{ | ||||||
|  |   type: 'success' | 'warning' | 'error' | 'info'; | ||||||
|  |   message: string; | ||||||
|  | }>(); | ||||||
|  | 
 | ||||||
|  | const iconClass = computed(() => { | ||||||
|  |   const icons = { | ||||||
|  |     success: 'fa-check-circle', | ||||||
|  |     warning: 'fa-exclamation-triangle', | ||||||
|  |     error: 'fa-times-circle', | ||||||
|  |     info: 'fa-info-circle', | ||||||
|  |   }; | ||||||
|  |   return `fas ${icons[props.type]}`; | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | .alert { | ||||||
|  |   padding: 12px 16px; | ||||||
|  |   border-radius: var(--border-radius); | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  |   gap: 8px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .alert-success { | ||||||
|  |   color: var(--success-color); | ||||||
|  |   background: var(--success-bg); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .alert-warning { | ||||||
|  |   color: var(--warning-color); | ||||||
|  |   background: var(--warning-bg); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .alert-error { | ||||||
|  |   color: var(--error-color); | ||||||
|  |   background: var(--error-bg); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .alert-info { | ||||||
|  |   color: var(--info-color); | ||||||
|  |   background: var(--info-bg); | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## 🔧 API 参考 | ||||||
|  | 
 | ||||||
|  | ### ThemeManager 类 | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | class ThemeManager { | ||||||
|  |   // 初始化主题 | ||||||
|  |   init(): void; | ||||||
|  |    | ||||||
|  |   // 设置主题 | ||||||
|  |   setTheme(theme: 'light' | 'dark', save?: boolean): void; | ||||||
|  |    | ||||||
|  |   // 切换主题 | ||||||
|  |   toggle(): ThemeMode; | ||||||
|  |    | ||||||
|  |   // 获取当前主题 | ||||||
|  |   getCurrentTheme(): ThemeMode; | ||||||
|  |    | ||||||
|  |   // 检查是否为暗色 | ||||||
|  |   isDark(): boolean; | ||||||
|  |    | ||||||
|  |   // 检查是否为亮色 | ||||||
|  |   isLight(): boolean; | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### useTheme Hook | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | export const useTheme = () => { | ||||||
|  |   return { | ||||||
|  |     currentTheme: ThemeMode;  // 当前主题 | ||||||
|  |     toggle(): void;           // 切换主题 | ||||||
|  |     setTheme(theme: ThemeMode): void;  // 设置主题 | ||||||
|  |     isDark(): boolean;        // 是否暗色 | ||||||
|  |     isLight(): boolean;       // 是否亮色 | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## 🎨 主题配色方案 | ||||||
|  | 
 | ||||||
|  | ### 亮色主题 | ||||||
|  | - 背景: 白色 → 浅灰 | ||||||
|  | - 文本: 深灰 → 黑色 | ||||||
|  | - 主色: 蓝色系 (#3498db) | ||||||
|  | - 次色: 绿色系 (#2ecc71) | ||||||
|  | - 强调: 黄色系 (#ffcc00) | ||||||
|  | 
 | ||||||
|  | ### 暗色主题 | ||||||
|  | - 背景: 深灰 → 黑色 | ||||||
|  | - 文本: 浅灰 → 白色 | ||||||
|  | - 主色: 浅蓝系 (#5dade2) | ||||||
|  | - 次色: 浅绿系 (#58d68d) | ||||||
|  | - 强调: 浅黄系 (#f7dc6f) | ||||||
|  | 
 | ||||||
|  | ## 📝 注意事项 | ||||||
|  | 
 | ||||||
|  | 1. **始终使用 CSS 变量**: 不要在样式中硬编码颜色值 | ||||||
|  | 2. **响应式绑定**: 在 Vue 组件中使用 `computed` 来响应式获取主题 | ||||||
|  | 3. **测试两个主题**: 确保在亮色和暗色主题下都有良好的可读性 | ||||||
|  | 4. **避免高对比度**: 暗色主题使用柔和的色彩,减少眼部疲劳 | ||||||
|  | 5. **合理使用阴影**: 暗色主题中使用更深的阴影增强层次感 | ||||||
|  | 
 | ||||||
|  | ## 🐛 故障排除 | ||||||
|  | 
 | ||||||
|  | ### 主题切换不生效 | ||||||
|  | - 确保在组件中正确导入并使用 `useTheme()` | ||||||
|  | - 检查是否正确添加了 `root.scss` 到主样式文件 | ||||||
|  | 
 | ||||||
|  | ### 样式未应用 | ||||||
|  | - 确保使用 CSS 变量而不是硬编码值 | ||||||
|  | - 检查元素是否正确继承了主题变量 | ||||||
|  | 
 | ||||||
|  | ### TypeScript 错误 | ||||||
|  | - 确保已正确导入类型定义 | ||||||
|  | - 重启 VS Code 的 TypeScript 服务器 | ||||||
|  | 
 | ||||||
|  | ## 📞 支持 | ||||||
|  | 
 | ||||||
|  | 如有问题或建议,请查看: | ||||||
|  | - `front/src/assets/css/theme-usage.md` - 详细使用文档 | ||||||
|  | - `front/src/utils/theme.ts` - 源代码和注释 | ||||||
|  | - `front/src/components/ThemeToggle.vue` - 组件示例 | ||||||
|  | 
 | ||||||
| @ -1,4 +1,18 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|  | import { onBeforeMount } from 'vue'; | ||||||
|  | import themeManager from '@/utils/theme'; | ||||||
|  | 
 | ||||||
|  | // 在组件挂载前初始化全局主题系统 | ||||||
|  | // 这样确保在首次渲染时就能应用正确的主题,避免闪烁 | ||||||
|  | onBeforeMount(() => { | ||||||
|  |   // 初始化主题管理器,自动检测系统偏好或读取保存的主题 | ||||||
|  |   themeManager.init(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | // 导出主题管理器供全局使用(可选,用于调试) | ||||||
|  | if (import.meta.env.DEV) { | ||||||
|  |   (window as any).themeManager = themeManager; | ||||||
|  | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| // 主题色变量 | // 主题色变量(SCSS变量 - 默认为亮色主题) | ||||||
| $primary-color: #3498db; | $primary-color: #3498db; | ||||||
| $secondary-color: #2ecc71; | $secondary-color: #2ecc71; | ||||||
| $accent-color: #ffcc00; | $accent-color: #ffcc00; | ||||||
| @ -21,56 +21,519 @@ $box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |||||||
| // 过渡 | // 过渡 | ||||||
| $transition-base: all 0.3s cubic-bezier(.25,.8,.25,1); | $transition-base: all 0.3s cubic-bezier(.25,.8,.25,1); | ||||||
| 
 | 
 | ||||||
| // 主题色快捷类 | // ========================================== | ||||||
| :root { | // 主题系统 - 亮色和暗色主题 | ||||||
|   --primary-color: #3498db; | // 使用 data-theme="light" 或 data-theme="dark" 来切换主题 | ||||||
|  | // ========================================== | ||||||
|  | 
 | ||||||
|  | // 默认亮色主题 | ||||||
|  | :root[data-theme="light"], | ||||||
|  | :root:not([data-theme]) { | ||||||
|  |   // 主题标识 | ||||||
|  |   --theme-mode: light; | ||||||
|  |    | ||||||
|  |   // 主要颜色 | ||||||
|  |   --primary-color: #1890ff; | ||||||
|  |   --primary-hover: #40a9ff; | ||||||
|  |   --primary-active: #096dd9; | ||||||
|   --secondary-color: #2ecc71; |   --secondary-color: #2ecc71; | ||||||
|  |   --secondary-hover: #27ae60; | ||||||
|  |   --secondary-active: #229954; | ||||||
|   --accent-color: #ffcc00; |   --accent-color: #ffcc00; | ||||||
|   --background-color: #f8f9fa; |   --accent-hover: #e6b800; | ||||||
|   --text-color: #333; |    | ||||||
|  |   // 背景颜色 | ||||||
|  |   --background-color: #ffffff; | ||||||
|  |   --background-secondary: #f8f9fa; | ||||||
|  |   --background-tertiary: #f0f2f5; | ||||||
|  |   --background-hover: #e9ecef; | ||||||
|  |    | ||||||
|  |   // 文本颜色 | ||||||
|  |   --text-color: #212529; | ||||||
|  |   --text-secondary: #6c757d; | ||||||
|  |   --text-tertiary: #adb5bd; | ||||||
|  |   --text-inverse: #ffffff; | ||||||
|  |    | ||||||
|  |   // 边框颜色 | ||||||
|  |   --border-color: #dee2e6; | ||||||
|  |   --border-color-hover: #ced4da; | ||||||
|  |   --border-color-active: #adb5bd; | ||||||
|  |    | ||||||
|  |   // 状态颜色 | ||||||
|  |   --success-color: #28a745; | ||||||
|  |   --success-bg: #d4edda; | ||||||
|  |   --warning-color: #ffc107; | ||||||
|  |   --warning-bg: #fff3cd; | ||||||
|  |   --error-color: #dc3545; | ||||||
|  |   --error-bg: #f8d7da; | ||||||
|  |   --info-color: #17a2b8; | ||||||
|  |   --info-bg: #d1ecf1; | ||||||
|  |    | ||||||
|  |   // 组件颜色 | ||||||
|  |   --card-bg: #ffffff; | ||||||
|  |   --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); | ||||||
|  |   --sidebar-bg: #f8f9fa; | ||||||
|  |   --sidebar-hover: #e9ecef; | ||||||
|  |   --header-bg: #ffffff; | ||||||
|  |    | ||||||
|  |   // Hero 渐变(亮色主题 - 现代蓝色渐变) | ||||||
|  |   --hero-gradient-start: #667eea; | ||||||
|  |   --hero-gradient-end: #764ba2; | ||||||
|  |    | ||||||
|  |   // 阴影 | ||||||
|  |   --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); | ||||||
|  |   --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08); | ||||||
|  |   --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.1); | ||||||
|  |   --shadow-xl: 0 8px 24px rgba(0, 0, 0, 0.12); | ||||||
|  |    | ||||||
|  |   // 圆角 | ||||||
|   --border-radius: 4px; |   --border-radius: 4px; | ||||||
|   --box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |   --border-radius-sm: 2px; | ||||||
|  |   --border-radius-lg: 8px; | ||||||
|  |   --border-radius-xl: 12px; | ||||||
|  |   --border-radius-full: 9999px; | ||||||
|  |    | ||||||
|  |   // 过渡 | ||||||
|   --transition-base: all 0.3s cubic-bezier(.25,.8,.25,1); |   --transition-base: all 0.3s cubic-bezier(.25,.8,.25,1); | ||||||
|  |   --transition-fast: all 0.15s ease-in-out; | ||||||
|  |   --transition-slow: all 0.5s ease-in-out; | ||||||
|  |    | ||||||
|  |   // 字体 | ||||||
|  |   --font-family-base: 'Segoe UI', 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 暗色主题 | ||||||
|  | :root[data-theme="dark"] { | ||||||
|  |   // 主题标识 | ||||||
|  |   --theme-mode: dark; | ||||||
|  |    | ||||||
|  |   // 主要颜色 | ||||||
|  |   --primary-color: #409EFF; | ||||||
|  |   --primary-hover: #66b1ff; | ||||||
|  |   --primary-active: #337ecc; | ||||||
|  |   --secondary-color: #58d68d; | ||||||
|  |   --secondary-hover: #2ecc71; | ||||||
|  |   --secondary-active: #27ae60; | ||||||
|  |   --accent-color: #f7dc6f; | ||||||
|  |   --accent-hover: #f4d03f; | ||||||
|  |    | ||||||
|  |   // 背景颜色 | ||||||
|  |   --background-color: #1a1a1a; | ||||||
|  |   --background-secondary: #2d2d2d; | ||||||
|  |   --background-tertiary: #3d3d3d; | ||||||
|  |   --background-hover: #4a4a4a; | ||||||
|  |    | ||||||
|  |   // 文本颜色 | ||||||
|  |   --text-color: #e9ecef; | ||||||
|  |   --text-secondary: #adb5bd; | ||||||
|  |   --text-tertiary: #6c757d; | ||||||
|  |   --text-inverse: #212529; | ||||||
|  |    | ||||||
|  |   // 边框颜色 | ||||||
|  |   --border-color: #404040; | ||||||
|  |   --border-color-hover: #4d4d4d; | ||||||
|  |   --border-color-active: #5a5a5a; | ||||||
|  |    | ||||||
|  |   // 状态颜色 | ||||||
|  |   --success-color: #58d68d; | ||||||
|  |   --success-bg: #1e3c24; | ||||||
|  |   --warning-color: #f7dc6f; | ||||||
|  |   --warning-bg: #4d3e1a; | ||||||
|  |   --error-color: #ec7063; | ||||||
|  |   --error-bg: #3c2626; | ||||||
|  |   --info-color: #5dade2; | ||||||
|  |   --info-bg: #1e2a33; | ||||||
|  |    | ||||||
|  |   // 组件颜色 | ||||||
|  |   --card-bg: #2d2d2d; | ||||||
|  |   --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); | ||||||
|  |   --sidebar-bg: #1f1f1f; | ||||||
|  |   --sidebar-hover: #3d3d3d; | ||||||
|  |   --header-bg: #2d2d2d; | ||||||
|  |    | ||||||
|  |   // Hero 渐变(暗色主题 - 深紫色渐变) | ||||||
|  |   --hero-gradient-start: #4a5568; | ||||||
|  |   --hero-gradient-end: #2d3748; | ||||||
|  |    | ||||||
|  |   // 阴影 | ||||||
|  |   --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); | ||||||
|  |   --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4); | ||||||
|  |   --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.5); | ||||||
|  |   --shadow-xl: 0 8px 24px rgba(0, 0, 0, 0.6); | ||||||
|  |    | ||||||
|  |   // 圆角 | ||||||
|  |   --border-radius: 4px; | ||||||
|  |   --border-radius-sm: 2px; | ||||||
|  |   --border-radius-lg: 8px; | ||||||
|  |   --border-radius-xl: 12px; | ||||||
|  |   --border-radius-full: 9999px; | ||||||
|  |    | ||||||
|  |   // 过渡 | ||||||
|  |   --transition-base: all 0.3s cubic-bezier(.25,.8,.25,1); | ||||||
|  |   --transition-fast: all 0.15s ease-in-out; | ||||||
|  |   --transition-slow: all 0.5s ease-in-out; | ||||||
|  |    | ||||||
|  |   // 字体 | ||||||
|   --font-family-base: 'Segoe UI', 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; |   --font-family-base: 'Segoe UI', 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| body { | body { | ||||||
|   font-family: $font-family-base; |   font-family: var(--font-family-base); | ||||||
|   color: $text-color; |   color: var(--text-color); | ||||||
|   background: $background-color; |   background: var(--background-color); | ||||||
|   margin: 0; |   margin: 0; | ||||||
|   padding: 0; |   padding: 0; | ||||||
|  |   transition: var(--transition-base); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| a { | a { | ||||||
|   color: $primary-color; |   color: var(--primary-color); | ||||||
|   text-decoration: none; |   text-decoration: none; | ||||||
| 
 | 
 | ||||||
|   &:hover { |   &:hover { | ||||||
|     text-decoration: underline; |     text-decoration: underline; | ||||||
|     color: $primary-color |     color: var(--primary-hover); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .btn { | .btn { | ||||||
|   display: inline-block; |   display: inline-block; | ||||||
|   padding: 0.5em 1.2em; |   padding: 0.5em 1.2em; | ||||||
|   border-radius: $border-radius; |   border-radius: var(--border-radius); | ||||||
|   background: $primary-color; |   background: var(--primary-color); | ||||||
|   color: #fff; |   color: var(--text-inverse); | ||||||
|   border: none; |   border: none; | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|   transition: background 0.2s; |   transition: var(--transition-fast); | ||||||
| 
 | 
 | ||||||
|   &:hover { |   &:hover { | ||||||
|     background: $primary-color |     background: var(--primary-hover); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   &:active { | ||||||
|  |     background: var(--primary-active); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &.secondary { |   &.secondary { | ||||||
|     background: $secondary-color; |     background: var(--secondary-color); | ||||||
| 
 | 
 | ||||||
|     &:hover { |     &:hover { | ||||||
|       background: $secondary-color |       background: var(--secondary-hover); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     &:active { | ||||||
|  |       background: var(--secondary-active); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   &.accent { | ||||||
|  |     background: var(--accent-color); | ||||||
|  |     color: var(--text-color); | ||||||
|  | 
 | ||||||
|  |     &:hover { | ||||||
|  |       background: var(--accent-hover); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // ========================================== | ||||||
|  | // 自定义滚动条样式 - 支持主题切换 | ||||||
|  | // ========================================== | ||||||
|  | 
 | ||||||
|  | // WebKit 浏览器 (Chrome, Edge, Safari) | ||||||
|  | ::-webkit-scrollbar { | ||||||
|  |   width: 8px; | ||||||
|  |   height: 8px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ::-webkit-scrollbar-track { | ||||||
|  |   background: var(--background-secondary); | ||||||
|  |   border-radius: var(--border-radius); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ::-webkit-scrollbar-thumb { | ||||||
|  |   background: var(--border-color-hover); | ||||||
|  |   border-radius: var(--border-radius); | ||||||
|  |   transition: var(--transition-fast); | ||||||
|  | 
 | ||||||
|  |   &:hover { | ||||||
|  |     background: var(--text-tertiary); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   &:active { | ||||||
|  |     background: var(--primary-color); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ::-webkit-scrollbar-corner { | ||||||
|  |   background: var(--background-secondary); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Firefox | ||||||
|  | * { | ||||||
|  |   scrollbar-width: thin; | ||||||
|  |   scrollbar-color: var(--border-color-hover) var(--background-secondary); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 可选:隐藏滚动条但保持滚动功能 | ||||||
|  | .scrollbar-hide { | ||||||
|  |   -ms-overflow-style: none;  /* IE and Edge */ | ||||||
|  |   scrollbar-width: none;  /* Firefox */ | ||||||
|  |    | ||||||
|  |   &::-webkit-scrollbar { | ||||||
|  |     display: none;  /* Chrome, Safari, Opera */ | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 平滑滚动 | ||||||
|  | html { | ||||||
|  |   scroll-behavior: smooth; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ========================================== | ||||||
|  | // Element Plus 主题覆盖 - 支持主题切换 | ||||||
|  | // ========================================== | ||||||
|  | 
 | ||||||
|  | // 亮色主题下的 Element Plus 组件 | ||||||
|  | :root[data-theme="light"], | ||||||
|  | :root:not([data-theme]) { | ||||||
|  |   --el-bg-color: #ffffff; | ||||||
|  |   --el-bg-color-page: #f2f3f5; | ||||||
|  |   --el-text-color-primary: #303133; | ||||||
|  |   --el-text-color-regular: #606266; | ||||||
|  |   --el-text-color-secondary: #909399; | ||||||
|  |   --el-text-color-placeholder: #c0c4cc; | ||||||
|  |   --el-text-color-disabled: #c0c4cc; | ||||||
|  |   --el-border-color: #dcdfe6; | ||||||
|  |   --el-border-color-light: #e4e7ed; | ||||||
|  |   --el-border-color-lighter: #ebeef5; | ||||||
|  |   --el-border-color-extra-light: #f2f6fc; | ||||||
|  |   --el-border-color-dark: #d4d7de; | ||||||
|  |   --el-border-color-darker: #cdd0d6; | ||||||
|  |    | ||||||
|  |   // 组件背景 | ||||||
|  |   --el-fill-color: #f0f2f5; | ||||||
|  |   --el-fill-color-light: #f5f7fa; | ||||||
|  |   --el-fill-color-lighter: #fafafa; | ||||||
|  |   --el-fill-color-extra-light: #fafcff; | ||||||
|  |   --el-fill-color-dark: #ebedf0; | ||||||
|  |   --el-fill-color-darker: #e6e8eb; | ||||||
|  |   --el-fill-color-blank: #ffffff; | ||||||
|  |    | ||||||
|  |   // 组件颜色 | ||||||
|  |   --el-color-primary: #1890ff; | ||||||
|  |   --el-color-success: #2ecc71; | ||||||
|  |   --el-color-warning: #ffcc00; | ||||||
|  |   --el-color-danger: #dc3545; | ||||||
|  |   --el-color-error: #dc3545; | ||||||
|  |   --el-color-info: #909399; | ||||||
|  |    | ||||||
|  |   // 阴影 | ||||||
|  |   --el-box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); | ||||||
|  |   --el-box-shadow-light: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04); | ||||||
|  |   --el-box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04); | ||||||
|  |   --el-box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12); | ||||||
|  |    | ||||||
|  |   // 覆盖 Element Plus 表格样式(亮色主题) | ||||||
|  |   --el-table-bg-color: #ffffff; | ||||||
|  |   --el-table-header-bg-color: #fafafa; | ||||||
|  |   --el-table-header-text-color: #909399; | ||||||
|  |   --el-table-row-hover-bg-color: #f5f7fa; | ||||||
|  |   --el-table-border-color: #ebeef5; | ||||||
|  |   --el-table-border: 1px solid #ebeef5; | ||||||
|  |    | ||||||
|  |   // 对话框 | ||||||
|  |   --el-overlay-color-light: rgba(0, 0, 0, 0.5); | ||||||
|  |   --el-overlay-color-lighter: rgba(0, 0, 0, 0.3); | ||||||
|  |   --el-mask-color: rgba(0, 0, 0, 0.5); | ||||||
|  |   --el-mask-color-extra-light: rgba(0, 0, 0, 0.2); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 暗色主题下的 Element Plus 组件 | ||||||
|  | :root[data-theme="dark"] { | ||||||
|  |   --el-bg-color: #2d2d2d; | ||||||
|  |   --el-bg-color-page: #1a1a1a; | ||||||
|  |   --el-text-color-primary: #e9ecef; | ||||||
|  |   --el-text-color-regular: #adb5bd; | ||||||
|  |   --el-text-color-secondary: #6c757d; | ||||||
|  |   --el-text-color-placeholder: #495057; | ||||||
|  |   --el-text-color-disabled: #495057; | ||||||
|  |   --el-border-color: #404040; | ||||||
|  |   --el-border-color-light: #404040; | ||||||
|  |   --el-border-color-lighter: #3d3d3d; | ||||||
|  |   --el-border-color-extra-light: #3d3d3d; | ||||||
|  |   --el-border-color-dark: #4a4a4a; | ||||||
|  |   --el-border-color-darker: #4a4a4a; | ||||||
|  |    | ||||||
|  |   // 组件背景 | ||||||
|  |   --el-fill-color: #3d3d3d; | ||||||
|  |   --el-fill-color-light: #3d3d3d; | ||||||
|  |   --el-fill-color-lighter: #3d3d3d; | ||||||
|  |   --el-fill-color-extra-light: #3d3d3d; | ||||||
|  |   --el-fill-color-dark: #4a4a4a; | ||||||
|  |   --el-fill-color-darker: #4a4a4a; | ||||||
|  |   --el-fill-color-blank: #2d2d2d; | ||||||
|  |    | ||||||
|  |   // 组件颜色 | ||||||
|  |   --el-color-primary: #409EFF; | ||||||
|  |   --el-color-success: #58d68d; | ||||||
|  |   --el-color-warning: #f7dc6f; | ||||||
|  |   --el-color-danger: #ec7063; | ||||||
|  |   --el-color-error: #ec7063; | ||||||
|  |   --el-color-info: #adb5bd; | ||||||
|  |    | ||||||
|  |   // 阴影(暗色主题使用更深的阴影) | ||||||
|  |   --el-box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.3); | ||||||
|  |   --el-box-shadow-light: 0 2px 4px rgba(0, 0, 0, 0.5); | ||||||
|  |   --el-box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.4); | ||||||
|  |   --el-box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.6); | ||||||
|  |    | ||||||
|  |   // 覆盖 Element Plus 表格样式 | ||||||
|  |   --el-table-bg-color: var(--card-bg); | ||||||
|  |   --el-table-header-bg-color: var(--header-bg); | ||||||
|  |   --el-table-header-text-color: var(--text-color); | ||||||
|  |   --el-table-row-hover-bg-color: var(--background-hover); | ||||||
|  |   --el-table-border-color: var(--border-color); | ||||||
|  |   --el-table-border: 1px solid var(--border-color); | ||||||
|  |    | ||||||
|  |   // 对话框 | ||||||
|  |   --el-overlay-color-light: rgba(0, 0, 0, 0.7); | ||||||
|  |   --el-overlay-color-lighter: rgba(0, 0, 0, 0.5); | ||||||
|  |   --el-mask-color: rgba(0, 0, 0, 0.8); | ||||||
|  |   --el-mask-color-extra-light: rgba(0, 0, 0, 0.3); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ========================================== | ||||||
|  | // Element Plus 组件强制样式覆盖 | ||||||
|  | // ========================================== | ||||||
|  | 
 | ||||||
|  | // 亮色和暗色主题统一样式 | ||||||
|  | .el-table { | ||||||
|  |   background-color: var(--card-bg); | ||||||
|  |   color: var(--text-color); | ||||||
|  |   transition: var(--transition-base); | ||||||
|  |    | ||||||
|  |   th.el-table__cell { | ||||||
|  |     background-color: var(--header-bg); | ||||||
|  |     color: var(--text-color); | ||||||
|  |     border-bottom-color: var(--border-color); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   td.el-table__cell { | ||||||
|  |     background-color: var(--card-bg); | ||||||
|  |     color: var(--text-color); | ||||||
|  |     border-bottom-color: var(--border-color); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .el-table__row:hover { | ||||||
|  |     background-color: var(--background-hover) !important; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .el-table__row.current-row { | ||||||
|  |     background-color: var(--background-hover) !important; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .el-table__border { | ||||||
|  |     border-color: var(--border-color); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .el-dialog { | ||||||
|  |   background-color: var(--card-bg); | ||||||
|  |   color: var(--text-color); | ||||||
|  |   transition: var(--transition-base); | ||||||
|  |    | ||||||
|  |   .el-dialog__header { | ||||||
|  |     border-bottom: 1px solid var(--border-color); | ||||||
|  |      | ||||||
|  |     .el-dialog__title { | ||||||
|  |       color: var(--text-color); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .el-dialog__body { | ||||||
|  |     color: var(--text-color); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .el-dialog__footer { | ||||||
|  |     border-top: 1px solid var(--border-color); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .el-descriptions { | ||||||
|  |   .el-descriptions__label { | ||||||
|  |     color: var(--text-secondary); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .el-descriptions__content { | ||||||
|  |     color: var(--text-color); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .el-descriptions__border { | ||||||
|  |     border-color: var(--border-color); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 表单 | ||||||
|  | .el-form { | ||||||
|  |   .el-form-item__label { | ||||||
|  |     color: var(--text-color); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .el-input__inner, | ||||||
|  |   .el-textarea__inner { | ||||||
|  |     background-color: var(--background-secondary); | ||||||
|  |     color: var(--text-color); | ||||||
|  |     border-color: var(--border-color); | ||||||
|  |      | ||||||
|  |     &::placeholder { | ||||||
|  |       color: var(--text-tertiary); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 按钮 | ||||||
|  | .el-button { | ||||||
|  |   background-color: var(--background-secondary); | ||||||
|  |   color: var(--text-color); | ||||||
|  |   border-color: var(--border-color); | ||||||
|  |   transition: var(--transition-fast); | ||||||
|  |    | ||||||
|  |   &:hover { | ||||||
|  |     border-color: var(--primary-color); | ||||||
|  |     color: var(--primary-color); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   &.el-button--primary { | ||||||
|  |     background-color: var(--primary-color); | ||||||
|  |     color: var(--text-inverse); | ||||||
|  |     border-color: var(--primary-color); | ||||||
|  |      | ||||||
|  |     &:hover { | ||||||
|  |       background-color: var(--primary-hover); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 标签页 | ||||||
|  | .el-tabs { | ||||||
|  |   .el-tabs__header { | ||||||
|  |     background-color: var(--card-bg); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .el-tabs__item { | ||||||
|  |     color: var(--text-secondary); | ||||||
|  |      | ||||||
|  |     &.is-active { | ||||||
|  |       color: var(--primary-color); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     &:hover { | ||||||
|  |       color: var(--primary-color); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .el-tabs__nav-wrap::after { | ||||||
|  |     background-color: var(--border-color); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | |||||||
| @ -2,11 +2,11 @@ | |||||||
|   display: flex; |   display: flex; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   justify-content: space-between; |   justify-content: space-between; | ||||||
|   margin: 10px 0; |   // margin: 10px 0; | ||||||
|   border-bottom: 1px solid #f2f3f5; |   border-bottom: 1px solid #f2f3f5; | ||||||
|   padding-bottom: 16px; |   padding-bottom: 20px; | ||||||
| 
 | 
 | ||||||
|   h2{ |   h2 { | ||||||
|     font-size: 1.4rem; |     font-size: 1.4rem; | ||||||
|     font-weight: 700; |     font-weight: 700; | ||||||
|   } |   } | ||||||
| @ -22,3 +22,8 @@ | |||||||
|   justify-content: flex-end; |   justify-content: flex-end; | ||||||
|   margin: 18px 0 0 0; |   margin: 18px 0 0 0; | ||||||
| } | } | ||||||
|  | .header-actions { | ||||||
|  |   display: flex; | ||||||
|  |   gap: 8px; | ||||||
|  |   align-items: center; | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										235
									
								
								front/src/assets/css/theme-usage.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								front/src/assets/css/theme-usage.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,235 @@ | |||||||
|  | # 主题系统使用指南 | ||||||
|  | 
 | ||||||
|  | ## 概述 | ||||||
|  | 
 | ||||||
|  | 项目已集成完整的亮色/暗色主题系统,支持一键切换并自动保存用户偏好。 | ||||||
|  | 
 | ||||||
|  | ## 主题标识 | ||||||
|  | 
 | ||||||
|  | - **亮色主题**: `data-theme="light"` 或不设置该属性 | ||||||
|  | - **暗色主题**: `data-theme="dark"` | ||||||
|  | - **主题模式标识**: 通过 CSS 变量 `--theme-mode` 可获取当前主题 | ||||||
|  | 
 | ||||||
|  | ## 使用方法 | ||||||
|  | 
 | ||||||
|  | ### 1. 在 Vue 组件中使用 | ||||||
|  | 
 | ||||||
|  | ```vue | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <button @click="toggleTheme"> | ||||||
|  |       切换到 {{ isDark ? '亮色' : '暗色' }} 主题 | ||||||
|  |     </button> | ||||||
|  |      | ||||||
|  |     <!-- 使用主题变量 --> | ||||||
|  |     <div class="card">内容</div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useTheme } from '@/utils/theme'; | ||||||
|  | 
 | ||||||
|  | const { currentTheme, toggle, isDark } = useTheme(); | ||||||
|  | 
 | ||||||
|  | const toggleTheme = () => { | ||||||
|  |   toggle(); | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | .card { | ||||||
|  |   background: var(--card-bg); | ||||||
|  |   color: var(--text-color); | ||||||
|  |   border: 1px solid var(--border-color); | ||||||
|  |   box-shadow: var(--card-shadow); | ||||||
|  |   border-radius: var(--border-radius); | ||||||
|  |   padding: 16px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .card:hover { | ||||||
|  |   border-color: var(--border-color-hover); | ||||||
|  |   background: var(--background-hover); | ||||||
|  |   transition: var(--transition-base); | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 2. 在 TypeScript 中使用 | ||||||
|  | 
 | ||||||
|  | ```typescript | ||||||
|  | import { themeManager, useTheme } from '@/utils/theme'; | ||||||
|  | 
 | ||||||
|  | // 方式1: 使用主题管理器 | ||||||
|  | themeManager.setTheme('dark'); | ||||||
|  | themeManager.toggle(); | ||||||
|  | console.log(themeManager.getCurrentTheme()); // 'dark' | ||||||
|  | 
 | ||||||
|  | // 方式2: 使用 useTheme hook | ||||||
|  | const { currentTheme, toggle, setTheme } = useTheme(); | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 3. 在 SCSS/CSS 中使用主题变量 | ||||||
|  | 
 | ||||||
|  | ```scss | ||||||
|  | .my-component { | ||||||
|  |   // 背景颜色 | ||||||
|  |   background: var(--background-color); | ||||||
|  |   background-secondary: var(--background-secondary); | ||||||
|  |    | ||||||
|  |   // 文本颜色 | ||||||
|  |   color: var(--text-color); | ||||||
|  |    | ||||||
|  |   // 主要颜色 | ||||||
|  |   border: 1px solid var(--primary-color); | ||||||
|  |    | ||||||
|  |   // 状态颜色 | ||||||
|  |   &.success { | ||||||
|  |     color: var(--success-color); | ||||||
|  |     background: var(--success-bg); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   &.error { | ||||||
|  |     color: var(--error-color); | ||||||
|  |     background: var(--error-bg); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // 阴影 | ||||||
|  |   box-shadow: var(--shadow-md); | ||||||
|  |    | ||||||
|  |   // 圆角 | ||||||
|  |   border-radius: var(--border-radius-lg); | ||||||
|  |    | ||||||
|  |   // 过渡 | ||||||
|  |   transition: var(--transition-base); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## 可用 CSS 变量 | ||||||
|  | 
 | ||||||
|  | ### 主题标识 | ||||||
|  | - `--theme-mode`: 'light' 或 'dark' | ||||||
|  | 
 | ||||||
|  | ### 主要颜色 | ||||||
|  | - `--primary-color`: 主色调 | ||||||
|  | - `--primary-hover`: 主色调悬停 | ||||||
|  | - `--primary-active`: 主色调激活 | ||||||
|  | - `--secondary-color`: 次要色调 | ||||||
|  | - `--secondary-hover`: 次要色调悬停 | ||||||
|  | - `--secondary-active`: 次要色调激活 | ||||||
|  | - `--accent-color`: 强调色 | ||||||
|  | - `--accent-hover`: 强调色悬停 | ||||||
|  | 
 | ||||||
|  | ### 背景颜色 | ||||||
|  | - `--background-color`: 主要背景 | ||||||
|  | - `--background-secondary`: 次要背景 | ||||||
|  | - `--background-tertiary`: 第三背景 | ||||||
|  | - `--background-hover`: 悬停背景 | ||||||
|  | 
 | ||||||
|  | ### 文本颜色 | ||||||
|  | - `--text-color`: 主要文本 | ||||||
|  | - `--text-secondary`: 次要文本 | ||||||
|  | - `--text-tertiary`: 第三文本 | ||||||
|  | - `--text-inverse`: 反转文本 | ||||||
|  | 
 | ||||||
|  | ### 边框颜色 | ||||||
|  | - `--border-color`: 边框颜色 | ||||||
|  | - `--border-color-hover`: 悬停边框 | ||||||
|  | - `--border-color-active`: 激活边框 | ||||||
|  | 
 | ||||||
|  | ### 状态颜色 | ||||||
|  | - `--success-color`: 成功色 | ||||||
|  | - `--success-bg`: 成功背景 | ||||||
|  | - `--warning-color`: 警告色 | ||||||
|  | - `--warning-bg`: 警告背景 | ||||||
|  | - `--error-color`: 错误色 | ||||||
|  | - `--error-bg`: 错误背景 | ||||||
|  | - `--info-color`: 信息色 | ||||||
|  | - `--info-bg`: 信息背景 | ||||||
|  | 
 | ||||||
|  | ### 组件颜色 | ||||||
|  | - `--card-bg`: 卡片背景 | ||||||
|  | - `--card-shadow`: 卡片阴影 | ||||||
|  | - `--sidebar-bg`: 侧边栏背景 | ||||||
|  | - `--sidebar-hover`: 侧边栏悬停 | ||||||
|  | - `--header-bg`: 头部背景 | ||||||
|  | 
 | ||||||
|  | ### 阴影 | ||||||
|  | - `--shadow-sm`: 小阴影 | ||||||
|  | - `--shadow-md`: 中等阴影 | ||||||
|  | - `--shadow-lg`: 大阴影 | ||||||
|  | - `--shadow-xl`: 超大阴影 | ||||||
|  | 
 | ||||||
|  | ### 圆角 | ||||||
|  | - `--border-radius`: 基础圆角 | ||||||
|  | - `--border-radius-sm`: 小圆角 | ||||||
|  | - `--border-radius-lg`: 大圆角 | ||||||
|  | - `--border-radius-xl`: 超大圆角 | ||||||
|  | - `--border-radius-full`: 完全圆角 | ||||||
|  | 
 | ||||||
|  | ### 过渡 | ||||||
|  | - `--transition-base`: 基础过渡 | ||||||
|  | - `--transition-fast`: 快速过渡 | ||||||
|  | - `--transition-slow`: 缓慢过渡 | ||||||
|  | 
 | ||||||
|  | ### 字体 | ||||||
|  | - `--font-family-base`: 基础字体族 | ||||||
|  | 
 | ||||||
|  | ## 特性 | ||||||
|  | 
 | ||||||
|  | 1. **自动保存**: 主题偏好自动保存到 `localStorage` | ||||||
|  | 2. **系统偏好**: 首次访问自动检测系统主题偏好 | ||||||
|  | 3. **系统监听**: 监听系统主题变化并自动更新 | ||||||
|  | 4. **平滑过渡**: 主题切换时使用 CSS 过渡效果 | ||||||
|  | 5. **完整变量**: 提供所有常见的 UI 变量 | ||||||
|  | 
 | ||||||
|  | ## 最佳实践 | ||||||
|  | 
 | ||||||
|  | 1. 始终使用 CSS 变量而不是固定颜色值 | ||||||
|  | 2. 在组件中使用 `useTheme()` 获取主题状态 | ||||||
|  | 3. 为交互元素添加悬停和激活状态 | ||||||
|  | 4. 使用阴影和边框来增强暗色主题的可读性 | ||||||
|  | 5. 测试两种主题下的所有页面和组件 | ||||||
|  | 
 | ||||||
|  | ## 示例组件 | ||||||
|  | 
 | ||||||
|  | 创建一个主题切换器组件: | ||||||
|  | 
 | ||||||
|  | ```vue | ||||||
|  | <template> | ||||||
|  |   <button class="theme-toggle" @click="handleToggle"> | ||||||
|  |     <i :class="isDark ? 'fa-sun' : 'fa-moon'"></i> | ||||||
|  |     <span>{{ isDark ? '切换到亮色' : '切换到暗色' }}</span> | ||||||
|  |   </button> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useTheme } from '@/utils/theme'; | ||||||
|  | 
 | ||||||
|  | const { isDark, toggle } = useTheme(); | ||||||
|  | 
 | ||||||
|  | const handleToggle = () => { | ||||||
|  |   toggle(); | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | .theme-toggle { | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  |   gap: 8px; | ||||||
|  |   padding: 8px 16px; | ||||||
|  |   background: var(--card-bg); | ||||||
|  |   color: var(--text-color); | ||||||
|  |   border: 1px solid var(--border-color); | ||||||
|  |   border-radius: var(--border-radius); | ||||||
|  |   cursor: pointer; | ||||||
|  |   transition: var(--transition-base); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-toggle:hover { | ||||||
|  |   background: var(--background-hover); | ||||||
|  |   border-color: var(--border-color-hover); | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
							
								
								
									
										57
									
								
								front/src/components/ThemeToggle.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								front/src/components/ThemeToggle.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | |||||||
|  | <template> | ||||||
|  |   <button class="theme-toggle" @click="toggleTheme" :title="isDark ? '切换到亮色模式' : '切换到暗色模式'"> | ||||||
|  |     <i class="fas" :class="isDark ? 'fa-sun' : 'fa-moon'"></i> | ||||||
|  |     <span class="theme-text">{{ isDark ? '亮色' : '暗色' }}</span> | ||||||
|  |   </button> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useTheme } from '@/utils/theme'; | ||||||
|  | 
 | ||||||
|  | const { toggle, isDark } = useTheme(); | ||||||
|  | 
 | ||||||
|  | const toggleTheme = () => { | ||||||
|  |   toggle(); | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped lang="scss"> | ||||||
|  | .theme-toggle { | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  |   gap: 8px; | ||||||
|  |   padding: 8px 16px; | ||||||
|  |   background: var(--card-bg); | ||||||
|  |   color: var(--text-color); | ||||||
|  |   border: 1px solid var(--border-color); | ||||||
|  |   border-radius: var(--border-radius); | ||||||
|  |   cursor: pointer; | ||||||
|  |   transition: var(--transition-base); | ||||||
|  |   font-size: 14px; | ||||||
|  | 
 | ||||||
|  |   &:hover { | ||||||
|  |     background: var(--background-hover); | ||||||
|  |     border-color: var(--border-color-hover); | ||||||
|  |     color: var(--primary-color); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   i { | ||||||
|  |     font-size: 16px; | ||||||
|  |     transition: transform 0.3s ease; | ||||||
|  | 
 | ||||||
|  |     .theme-toggle:hover & { | ||||||
|  |       transform: rotate(360deg); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .theme-text { | ||||||
|  |     font-weight: 500; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 暗色主题下的特殊样式 | ||||||
|  | :root[data-theme="dark"] .theme-toggle { | ||||||
|  |   box-shadow: var(--shadow-sm); | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | 
 | ||||||
							
								
								
									
										103
									
								
								front/src/utils/theme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								front/src/utils/theme.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,103 @@ | |||||||
|  | /** | ||||||
|  |  * 主题切换工具 | ||||||
|  |  * 支持亮色和暗色主题切换,并自动保存用户偏好 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | export type ThemeMode = 'light' | 'dark'; | ||||||
|  | 
 | ||||||
|  | class ThemeManager { | ||||||
|  |   private readonly STORAGE_KEY = 'app-theme'; | ||||||
|  |   private currentTheme: ThemeMode = 'light'; | ||||||
|  | 
 | ||||||
|  |   constructor() { | ||||||
|  |     this.init(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * 初始化主题 | ||||||
|  |    * 从本地存储读取或使用系统偏好 | ||||||
|  |    */ | ||||||
|  |   init() { | ||||||
|  |     // 先读取本地存储的主题
 | ||||||
|  |     const savedTheme = localStorage.getItem(this.STORAGE_KEY) as ThemeMode | null; | ||||||
|  |      | ||||||
|  |     if (savedTheme) { | ||||||
|  |       this.setTheme(savedTheme); | ||||||
|  |     } else { | ||||||
|  |       // 检测系统偏好
 | ||||||
|  |       const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; | ||||||
|  |       this.setTheme(prefersDark ? 'dark' : 'light'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 监听系统主题变化
 | ||||||
|  |     window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { | ||||||
|  |       if (!localStorage.getItem(this.STORAGE_KEY)) { | ||||||
|  |         this.setTheme(e.matches ? 'dark' : 'light', false); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * 设置主题 | ||||||
|  |    * @param theme 主题模式 | ||||||
|  |    * @param save 是否保存到本地存储,默认 true | ||||||
|  |    */ | ||||||
|  |   setTheme(theme: ThemeMode, save: boolean = true) { | ||||||
|  |     this.currentTheme = theme; | ||||||
|  |     document.documentElement.setAttribute('data-theme', theme); | ||||||
|  |      | ||||||
|  |     if (save) { | ||||||
|  |       localStorage.setItem(this.STORAGE_KEY, theme); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * 切换主题 | ||||||
|  |    */ | ||||||
|  |   toggle() { | ||||||
|  |     const newTheme = this.currentTheme === 'light' ? 'dark' : 'light'; | ||||||
|  |     this.setTheme(newTheme); | ||||||
|  |     return newTheme; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * 获取当前主题 | ||||||
|  |    */ | ||||||
|  |   getCurrentTheme(): ThemeMode { | ||||||
|  |     return this.currentTheme; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * 检查是否为暗色主题 | ||||||
|  |    */ | ||||||
|  |   isDark(): boolean { | ||||||
|  |     return this.currentTheme === 'dark'; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * 检查是否为亮色主题 | ||||||
|  |    */ | ||||||
|  |   isLight(): boolean { | ||||||
|  |     return this.currentTheme === 'light'; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 导出单例实例
 | ||||||
|  | export const themeManager = new ThemeManager(); | ||||||
|  | 
 | ||||||
|  | // 导出便捷方法
 | ||||||
|  | export const useTheme = () => { | ||||||
|  |   return { | ||||||
|  |     get currentTheme() { | ||||||
|  |       return themeManager.getCurrentTheme(); | ||||||
|  |     }, | ||||||
|  |     toggle: () => themeManager.toggle(), | ||||||
|  |     setTheme: (theme: ThemeMode) => themeManager.setTheme(theme), | ||||||
|  |     isDark: () => themeManager.isDark(), | ||||||
|  |     isLight: () => themeManager.isLight(), | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // 导出常量和类型
 | ||||||
|  | export default themeManager; | ||||||
|  | 
 | ||||||
| @ -337,33 +337,16 @@ export default { | |||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| // 全局变量定义 | // 使用主题变量,不需要本地定义了 | ||||||
| $primary-color: #409EFF; |  | ||||||
| $success-color: #67C23A; |  | ||||||
| $warning-color: #E6A23C; |  | ||||||
| $danger-color: #F56C6C; |  | ||||||
| $gray-100: #f5f7fa; |  | ||||||
| $gray-200: #e4e7ed; |  | ||||||
| $gray-300: #dcdfe6; |  | ||||||
| $gray-400: #c0c4cc; |  | ||||||
| $gray-500: #909399; |  | ||||||
| $gray-600: #606266; |  | ||||||
| $gray-700: #303133; |  | ||||||
| $shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08); |  | ||||||
| $shadow-md: 0 4px 12px rgba(0, 0, 0, 0.05); |  | ||||||
| $shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.1); |  | ||||||
| $radius-sm: 4px; |  | ||||||
| $radius-md: 8px; |  | ||||||
| $radius-lg: 12px; |  | ||||||
| $transition-base: all 0.3s ease; |  | ||||||
| 
 | 
 | ||||||
| // 顶部横幅样式 | // 顶部横幅样式 | ||||||
| .hero.new-style { | .hero.new-style { | ||||||
|   background: linear-gradient(135deg, #3b82f6, #0284c7); |   background: linear-gradient(135deg, var(--primary-color), var(--info-color)); | ||||||
|   padding: 60px 20px; |   padding: 60px 20px; | ||||||
|   margin-bottom: 30px; |   margin-bottom: 30px; | ||||||
|   position: relative; |   position: relative; | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|  |   transition: var(--transition-base); | ||||||
|    |    | ||||||
|   // 背景装饰 |   // 背景装饰 | ||||||
|   &::after { |   &::after { | ||||||
| @ -406,18 +389,19 @@ $transition-base: all 0.3s ease; | |||||||
|     align-items: stretch; |     align-items: stretch; | ||||||
|     max-width: 900px; |     max-width: 900px; | ||||||
|     margin: 0 auto 30px; |     margin: 0 auto 30px; | ||||||
|     background: white; |     background: var(--card-bg); | ||||||
|     border-radius: $radius-md; |     border-radius: var(--border-radius-lg); | ||||||
|     overflow: hidden; |     overflow: hidden; | ||||||
|     box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); |     box-shadow: var(--shadow-xl); | ||||||
|  |     border: 1px solid var(--border-color); | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   .search-select { |   .search-select { | ||||||
|     border-right: none; |     border-right: none; | ||||||
|     border-radius: $radius-md 0 0 $radius-md; |     border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg); | ||||||
|      |      | ||||||
|     ::v-deep .el-input__inner { |     ::v-deep .el-input__inner { | ||||||
|       border-radius: $radius-md 0 0 $radius-md; |       border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    |    | ||||||
| @ -433,15 +417,16 @@ $transition-base: all 0.3s ease; | |||||||
|   } |   } | ||||||
|    |    | ||||||
|   .search-button { |   .search-button { | ||||||
|     background-color: #f97316; |     background-color: var(--accent-color); | ||||||
|     border-color: #f97316; |     border-color: var(--accent-color); | ||||||
|     border-radius: 0 $radius-md $radius-md 0; |     border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0; | ||||||
|     padding: 0 32px; |     padding: 0 32px; | ||||||
|     font-size: 16px; |     font-size: 16px; | ||||||
|  |     transition: var(--transition-fast); | ||||||
|      |      | ||||||
|     &:hover { |     &:hover { | ||||||
|       background-color: #ea580c; |       background-color: var(--accent-hover); | ||||||
|       border-color: #ea580c; |       border-color: var(--accent-hover); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    |    | ||||||
| @ -479,24 +464,25 @@ $transition-base: all 0.3s ease; | |||||||
|   } |   } | ||||||
|    |    | ||||||
|   .stat-card { |   .stat-card { | ||||||
|     background: #fff; |     background: var(--card-bg); | ||||||
|     border-radius: $radius-lg; |     border-radius: var(--border-radius-lg); | ||||||
|     padding: 20px; |     padding: 20px; | ||||||
|     box-shadow: $shadow-md; |     box-shadow: var(--card-shadow); | ||||||
|     display: flex; |     display: flex; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     transition: $transition-base; |     transition: var(--transition-base); | ||||||
|  |     border: 1px solid var(--border-color); | ||||||
|      |      | ||||||
|     &:hover { |     &:hover { | ||||||
|       transform: translateY(-5px); |       transform: translateY(-5px); | ||||||
|       box-shadow: $shadow-lg; |       box-shadow: var(--shadow-lg); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     .stat-icon { |     .stat-icon { | ||||||
|       width: 48px; |       width: 48px; | ||||||
|       height: 48px; |       height: 48px; | ||||||
|       border-radius: $radius-md; |       border-radius: var(--border-radius-lg); | ||||||
|       background-color: rgba(64, 158, 255, 0.1); |       background: var(--info-bg); | ||||||
|       display: flex; |       display: flex; | ||||||
|       align-items: center; |       align-items: center; | ||||||
|       justify-content: center; |       justify-content: center; | ||||||
| @ -504,7 +490,7 @@ $transition-base: all 0.3s ease; | |||||||
|        |        | ||||||
|       i { |       i { | ||||||
|         font-size: 24px; |         font-size: 24px; | ||||||
|         color: #409EFF; |         color: var(--info-color); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|      |      | ||||||
| @ -513,14 +499,14 @@ $transition-base: all 0.3s ease; | |||||||
|        |        | ||||||
|       .stat-label { |       .stat-label { | ||||||
|         font-size: 14px; |         font-size: 14px; | ||||||
|         color: $gray-600; |         color: var(--text-secondary); | ||||||
|         margin-bottom: 4px; |         margin-bottom: 4px; | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       .stat-value { |       .stat-value { | ||||||
|         font-size: 24px; |         font-size: 24px; | ||||||
|         font-weight: 600; |         font-weight: 600; | ||||||
|         color: $gray-700; |         color: var(--text-color); | ||||||
|         margin-bottom: 4px; |         margin-bottom: 4px; | ||||||
|         line-height: 1; |         line-height: 1; | ||||||
|       } |       } | ||||||
| @ -536,11 +522,11 @@ $transition-base: all 0.3s ease; | |||||||
|         } |         } | ||||||
|          |          | ||||||
|         &.positive { |         &.positive { | ||||||
|           color: $success-color; |           color: var(--success-color); | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         &.negative { |         &.negative { | ||||||
|           color: $danger-color; |           color: var(--error-color); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -560,12 +546,17 @@ $transition-base: all 0.3s ease; | |||||||
|     h2 { |     h2 { | ||||||
|       font-size: 18px; |       font-size: 18px; | ||||||
|       font-weight: 600; |       font-weight: 600; | ||||||
|       color: $gray-700; |       color: var(--text-color); | ||||||
|       margin: 0; |       margin: 0; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     .create-btn { |     .create-btn { | ||||||
|       box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3); |       box-shadow: var(--shadow-md); | ||||||
|  |       transition: var(--transition-fast); | ||||||
|  |        | ||||||
|  |       &:hover { | ||||||
|  |         box-shadow: var(--shadow-lg); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    |    | ||||||
| @ -577,29 +568,29 @@ $transition-base: all 0.3s ease; | |||||||
|   } |   } | ||||||
|    |    | ||||||
|   .repo-card { |   .repo-card { | ||||||
|     background: #fff; |     background: var(--card-bg); | ||||||
|     border-radius: $radius-lg; |     border-radius: var(--border-radius-lg); | ||||||
|     overflow: hidden; |     overflow: hidden; | ||||||
|     box-shadow: $shadow-md; |     box-shadow: var(--card-shadow); | ||||||
|     transition: $transition-base; |     transition: var(--transition-base); | ||||||
|     border: 1px solid $gray-200; |     border: 1px solid var(--border-color); | ||||||
|      |      | ||||||
|     &:hover { |     &:hover { | ||||||
|       transform: translateY(-5px); |       transform: translateY(-5px); | ||||||
|       box-shadow: $shadow-lg; |       box-shadow: var(--shadow-lg); | ||||||
|       border-color: transparent; |       border-color: var(--primary-color); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     .repo-header { |     .repo-header { | ||||||
|       padding: 20px; |       padding: 20px; | ||||||
|       border-bottom: 1px solid $gray-100; |       border-bottom: 1px solid var(--border-color); | ||||||
|       display: flex; |       display: flex; | ||||||
|        |        | ||||||
|       .repo-icon { |       .repo-icon { | ||||||
|         width: 40px; |         width: 40px; | ||||||
|         height: 40px; |         height: 40px; | ||||||
|         border-radius: 8px; |         border-radius: var(--border-radius); | ||||||
|         background-color: rgba(64, 158, 255, 0.1); |         background: var(--info-bg); | ||||||
|         display: flex; |         display: flex; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         justify-content: center; |         justify-content: center; | ||||||
| @ -608,7 +599,7 @@ $transition-base: all 0.3s ease; | |||||||
|          |          | ||||||
|         i { |         i { | ||||||
|           font-size: 20px; |           font-size: 20px; | ||||||
|           color: #409EFF; |           color: var(--info-color); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|        |        | ||||||
| @ -618,7 +609,7 @@ $transition-base: all 0.3s ease; | |||||||
|         .repo-name { |         .repo-name { | ||||||
|           font-size: 16px; |           font-size: 16px; | ||||||
|           font-weight: 600; |           font-weight: 600; | ||||||
|           color: $gray-700; |           color: var(--text-color); | ||||||
|           margin: 0 0 8px 0; |           margin: 0 0 8px 0; | ||||||
|           white-space: nowrap; |           white-space: nowrap; | ||||||
|           overflow: hidden; |           overflow: hidden; | ||||||
| @ -631,14 +622,14 @@ $transition-base: all 0.3s ease; | |||||||
|           gap: 8px; |           gap: 8px; | ||||||
|            |            | ||||||
|           .private-tag { |           .private-tag { | ||||||
|             background-color: #f0f9ff; |             background: var(--info-bg); | ||||||
|             color: #1677ff; |             color: var(--info-color); | ||||||
|             border-color: #91d5ff; |             border-color: var(--info-color); | ||||||
|           } |           } | ||||||
|            |            | ||||||
|           .repo-owner { |           .repo-owner { | ||||||
|             font-size: 12px; |             font-size: 12px; | ||||||
|             color: $gray-600; |             color: var(--text-secondary); | ||||||
|             display: flex; |             display: flex; | ||||||
|             align-items: center; |             align-items: center; | ||||||
|             gap: 4px; |             gap: 4px; | ||||||
| @ -646,7 +637,7 @@ $transition-base: all 0.3s ease; | |||||||
|             .owner-avatar { |             .owner-avatar { | ||||||
|               width: 16px; |               width: 16px; | ||||||
|               height: 16px; |               height: 16px; | ||||||
|               border-radius: 50%; |               border-radius: var(--border-radius-full); | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| @ -658,11 +649,12 @@ $transition-base: all 0.3s ease; | |||||||
|        |        | ||||||
|       .repo-description { |       .repo-description { | ||||||
|         font-size: 14px; |         font-size: 14px; | ||||||
|         color: $gray-600; |         color: var(--text-secondary); | ||||||
|         line-height: 1.5; |         line-height: 1.5; | ||||||
|         margin: 0 0 16px 0; |         margin: 0 0 16px 0; | ||||||
|         display: -webkit-box; |         display: -webkit-box; | ||||||
|         -webkit-line-clamp: 2; |         -webkit-line-clamp: 2; | ||||||
|  |         line-clamp: 2; | ||||||
|         -webkit-box-orient: vertical; |         -webkit-box-orient: vertical; | ||||||
|         overflow: hidden; |         overflow: hidden; | ||||||
|         height: 42px; |         height: 42px; | ||||||
| @ -673,7 +665,7 @@ $transition-base: all 0.3s ease; | |||||||
|         align-items: center; |         align-items: center; | ||||||
|         justify-content: space-between; |         justify-content: space-between; | ||||||
|         font-size: 12px; |         font-size: 12px; | ||||||
|         color: $gray-500; |         color: var(--text-tertiary); | ||||||
|          |          | ||||||
|         .stat-item { |         .stat-item { | ||||||
|           display: flex; |           display: flex; | ||||||
| @ -689,7 +681,7 @@ $transition-base: all 0.3s ease; | |||||||
|      |      | ||||||
|     .repo-actions { |     .repo-actions { | ||||||
|       padding: 12px 20px; |       padding: 12px 20px; | ||||||
|       border-top: 1px solid $gray-100; |       border-top: 1px solid var(--border-color); | ||||||
|       display: flex; |       display: flex; | ||||||
|       justify-content: space-between; |       justify-content: space-between; | ||||||
|        |        | ||||||
| @ -702,28 +694,28 @@ $transition-base: all 0.3s ease; | |||||||
|         display: flex; |         display: flex; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         gap: 4px; |         gap: 4px; | ||||||
|         border-radius: $radius-sm; |         border-radius: var(--border-radius); | ||||||
|         transition: all 0.2s ease; |         transition: var(--transition-fast); | ||||||
|          |          | ||||||
|         i { |         i { | ||||||
|           font-size: 14px; |           font-size: 14px; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         &:hover { |         &:hover { | ||||||
|           background-color: $gray-100; |           background-color: var(--background-hover); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       .view-btn { |       .view-btn { | ||||||
|         color: $primary-color; |         color: var(--primary-color); | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       .edit-btn { |       .edit-btn { | ||||||
|         color: $success-color; |         color: var(--success-color); | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       .delete-btn { |       .delete-btn { | ||||||
|         color: $danger-color; |         color: var(--error-color); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -732,15 +724,16 @@ $transition-base: all 0.3s ease; | |||||||
|   .empty-state { |   .empty-state { | ||||||
|     text-align: center; |     text-align: center; | ||||||
|     padding: 60px 20px; |     padding: 60px 20px; | ||||||
|     background-color: #fff; |     background: var(--card-bg); | ||||||
|     border-radius: $radius-lg; |     border-radius: var(--border-radius-lg); | ||||||
|     border: 1px dashed $gray-300; |     border: 1px dashed var(--border-color); | ||||||
|  |     transition: var(--transition-base); | ||||||
|      |      | ||||||
|     .empty-icon { |     .empty-icon { | ||||||
|       width: 64px; |       width: 64px; | ||||||
|       height: 64px; |       height: 64px; | ||||||
|       border-radius: 50%; |       border-radius: var(--border-radius-full); | ||||||
|       background-color: $gray-100; |       background: var(--background-secondary); | ||||||
|       display: flex; |       display: flex; | ||||||
|       align-items: center; |       align-items: center; | ||||||
|       justify-content: center; |       justify-content: center; | ||||||
| @ -748,19 +741,19 @@ $transition-base: all 0.3s ease; | |||||||
|        |        | ||||||
|       i { |       i { | ||||||
|         font-size: 32px; |         font-size: 32px; | ||||||
|         color: $gray-400; |         color: var(--text-tertiary); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     h3 { |     h3 { | ||||||
|       font-size: 16px; |       font-size: 16px; | ||||||
|       color: $gray-700; |       color: var(--text-color); | ||||||
|       margin: 0 0 8px 0; |       margin: 0 0 8px 0; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     p { |     p { | ||||||
|       font-size: 14px; |       font-size: 14px; | ||||||
|       color: $gray-500; |       color: var(--text-secondary); | ||||||
|       margin: 0 0 20px 0; |       margin: 0 0 20px 0; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -791,8 +784,8 @@ $transition-base: all 0.3s ease; | |||||||
|       .search-select, |       .search-select, | ||||||
|       .search-input { |       .search-input { | ||||||
|         border-radius: 0; |         border-radius: 0; | ||||||
|         border-right: 1px solid $gray-300; |         border-right: 1px solid var(--border-color); | ||||||
|         border-bottom: 1px solid $gray-300; |         border-bottom: 1px solid var(--border-color); | ||||||
|          |          | ||||||
|         ::v-deep .el-input__inner { |         ::v-deep .el-input__inner { | ||||||
|           border-radius: 0; |           border-radius: 0; | ||||||
| @ -800,11 +793,11 @@ $transition-base: all 0.3s ease; | |||||||
|       } |       } | ||||||
|        |        | ||||||
|       .search-select { |       .search-select { | ||||||
|         border-radius: $radius-md $radius-md 0 0; |         border-radius: var(--border-radius-lg) var(--border-radius-lg) 0 0; | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       .search-button { |       .search-button { | ||||||
|         border-radius: 0 0 $radius-md $radius-md; |         border-radius: 0 0 var(--border-radius-lg) var(--border-radius-lg); | ||||||
|         padding: 12px; |         padding: 12px; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,178 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <header class="common-header"> |  | ||||||
|     <div class="logo" @click="goHome" style="cursor: pointer;"> |  | ||||||
| 
 |  | ||||||
|       <slot name="logo"> |  | ||||||
|         <img src="@/assets/imgs/logo.webp" alt="Logo" /> |  | ||||||
|       </slot> |  | ||||||
|     </div> |  | ||||||
|     <nav class="nav"> |  | ||||||
|       <slot name="nav"></slot> |  | ||||||
|     </nav> |  | ||||||
|     <div class="actions"> |  | ||||||
|       <slot name="actions"> |  | ||||||
|         <el-dropdown @command="handleCommand" placement="bottom-end"> |  | ||||||
|           <div class="user-info cursor-pointer"> |  | ||||||
|             <img |  | ||||||
|               :src="getAvatarUrl()" |  | ||||||
|               alt="User Avatar" |  | ||||||
|               style=" |  | ||||||
|                 height: 32px; |  | ||||||
|                 width: 32px; |  | ||||||
|                 border-radius: 50%; |  | ||||||
|                 margin-right: 8px; |  | ||||||
|               " |  | ||||||
|             /> |  | ||||||
|             <span>{{ getUserName() }}</span> |  | ||||||
|           </div> |  | ||||||
|           <template #dropdown> |  | ||||||
|             <el-dropdown-menu> |  | ||||||
|               <el-dropdown-item command="profile"> |  | ||||||
|                 <!-- <el-icon><user /></el-icon> --> |  | ||||||
|                 <i class="fas fa-user"></i> |  | ||||||
|                 <span>个人中心</span> |  | ||||||
|               </el-dropdown-item> |  | ||||||
|               <el-dropdown-item command="logout" divided> |  | ||||||
|                 <!-- <el-icon><arrow-right /></el-icon> --> |  | ||||||
|                 <i class="fas fa-sign-out-alt"></i> |  | ||||||
|                 <span>退出登录</span> |  | ||||||
|               </el-dropdown-item> |  | ||||||
|             </el-dropdown-menu> |  | ||||||
|           </template> |  | ||||||
|         </el-dropdown> |  | ||||||
|       </slot> |  | ||||||
|     </div> |  | ||||||
|   </header> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| // 安全获取头像URL |  | ||||||
| defineOptions({ name: "CommonHeader" }); |  | ||||||
| import { useRouter } from "vue-router"; |  | ||||||
| import { User, ArrowRight } from "@element-plus/icons-vue"; |  | ||||||
| import { ElMessage } from "element-plus"; |  | ||||||
| import { authAPI } from "@/services/api"; |  | ||||||
| 
 |  | ||||||
| const router = useRouter(); |  | ||||||
| 
 |  | ||||||
| // 创建一个函数来安全地访问 localStorage |  | ||||||
| const getAvatarUrl = () => { |  | ||||||
|   try { |  | ||||||
|     // 检查 window 和 localStorage 是否存在 |  | ||||||
|     if (typeof window !== "undefined" && window.localStorage) { |  | ||||||
|       try { |  | ||||||
|         const userItem = window.localStorage.getItem("user"); |  | ||||||
|         const userInfo = userItem ? JSON.parse(userItem) : {}; |  | ||||||
|         return userInfo.avatar || "/src/assets/imgs/default_avatar.png"; |  | ||||||
|       } catch (error) { |  | ||||||
|         console.warn("解析用户信息失败:", error); |  | ||||||
|         return "/src/assets/imgs/default_avatar.png"; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     // 如果 localStorage 不可用,返回默认头像 |  | ||||||
|     return "/src/assets/imgs/default_avatar.png"; |  | ||||||
|   } catch (error) { |  | ||||||
|     console.warn("获取头像失败:", error); |  | ||||||
|     return "/src/assets/imgs/default_avatar.png"; |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // 创建一个函数来安全地访问 localStorage 中的用户名 |  | ||||||
| const getUserName = () => { |  | ||||||
|   try { |  | ||||||
|     // 检查 window 和 localStorage 是否存在 |  | ||||||
|     if (typeof window !== "undefined" && window.localStorage) { |  | ||||||
|       return JSON.parse(window.localStorage.getItem("user") || "{}").nickname; |  | ||||||
|     } |  | ||||||
|     // 如果 localStorage 不可用,返回默认用户名 |  | ||||||
|     return "用户"; |  | ||||||
|   } catch (error) { |  | ||||||
|     console.warn("获取用户名失败:", error); |  | ||||||
|     return "用户"; |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // 点击logo返回首页 |  | ||||||
| const goHome = () => { |  | ||||||
|   router.push('/'); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // 处理下拉菜单命令 |  | ||||||
| const handleCommand = (command: string) => { |  | ||||||
|   switch (command) { |  | ||||||
|     case "profile": |  | ||||||
|       handleProfile(); |  | ||||||
|       break; |  | ||||||
|     case "logout": |  | ||||||
|       handleLogout(); |  | ||||||
|       break; |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // 个人中心处理函数 |  | ||||||
| const handleProfile = () => { |  | ||||||
|   // 这里可以根据实际项目情况跳转到个人中心页面 |  | ||||||
|   ElMessage.success("跳转到个人中心"); |  | ||||||
|   // 示例:router.push('/profile'); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // 登出处理函数 |  | ||||||
| const handleLogout = async () => { |  | ||||||
|   try { |  | ||||||
|     // 调用登出API |  | ||||||
|     await authAPI.logout(); |  | ||||||
| 
 |  | ||||||
|     // 清除本地登录状态 |  | ||||||
|     if (typeof window !== "undefined" && window.localStorage) { |  | ||||||
|       window.localStorage.removeItem("user"); |  | ||||||
|       window.localStorage.removeItem("token"); |  | ||||||
|       window.localStorage.removeItem("isAuthenticated"); |  | ||||||
|       window.localStorage.removeItem("token"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // 跳转到登录页面 |  | ||||||
|     router.push("/login"); |  | ||||||
| 
 |  | ||||||
|     ElMessage.success("退出登录成功"); |  | ||||||
|   } catch (error) { |  | ||||||
|     console.error("退出登录失败:", error); |  | ||||||
|     ElMessage.error("退出登录失败"); |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .common-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   height: 64px; |  | ||||||
|   padding: 0 24px; |  | ||||||
|   background: #fff; |  | ||||||
|   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |  | ||||||
| } |  | ||||||
| .logo img { |  | ||||||
|   height: 40px; |  | ||||||
| } |  | ||||||
| .nav { |  | ||||||
|   flex: 1; |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
|   align-items: center; |  | ||||||
| } |  | ||||||
| .actions { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 16px; |  | ||||||
| 
 |  | ||||||
|   .user-info { |  | ||||||
|     display: flex; |  | ||||||
|     align-items: center; |  | ||||||
|     gap: 8px; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   .cursor-pointer { |  | ||||||
|     cursor: pointer; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @ -11,13 +11,13 @@ | |||||||
|         <!-- 头部 --> |         <!-- 头部 --> | ||||||
|         <el-header class="header"> |         <el-header class="header"> | ||||||
|           <div class="header-content"> |           <div class="header-content"> | ||||||
|             <el-button |  | ||||||
|               :icon="isCollapse ? Expand : Fold" |  | ||||||
|               @click="toggleCollapse" |  | ||||||
|               circle |  | ||||||
|               size="small" |  | ||||||
|             /> |  | ||||||
|             <div class="header-left"> |             <div class="header-left"> | ||||||
|  |               <el-button | ||||||
|  |                 :icon="isCollapse ? Expand : Fold" | ||||||
|  |                 @click="toggleCollapse" | ||||||
|  |                 circle | ||||||
|  |                 size="small" | ||||||
|  |               /> | ||||||
|               <button |               <button | ||||||
|                 class="menu-toggle" |                 class="menu-toggle" | ||||||
|                 @click="toggleSidebar" |                 @click="toggleSidebar" | ||||||
| @ -39,19 +39,17 @@ | |||||||
|               </div> |               </div> | ||||||
| 
 | 
 | ||||||
|               <div class="header-actions"> |               <div class="header-actions"> | ||||||
|  |                 <button | ||||||
|  |                   class="action-btn theme-toggle-btn" | ||||||
|  |                   @click="toggleTheme" | ||||||
|  |                   aria-label="Toggle theme" | ||||||
|  |                 > | ||||||
|  |                   <i class="fas" :class="isDark ? 'fa-sun' : 'fa-moon'"></i> | ||||||
|  |                 </button> | ||||||
|                 <button class="action-btn" aria-label="Notifications"> |                 <button class="action-btn" aria-label="Notifications"> | ||||||
|                   <svg-icon name="bell" /> |                   <i class="fas fa-bell"></i> | ||||||
|                   <span class="notification-badge">3</span> |                   <span class="notification-badge">3</span> | ||||||
|                 </button> |                 </button> | ||||||
|                 <button class="action-btn" aria-label="Settings"> |  | ||||||
|                   <svg-icon name="settings" /> |  | ||||||
|                 </button> |  | ||||||
|                 <div class="mobile-user"> |  | ||||||
|                   <img |  | ||||||
|                     src="https://picsum.photos/id/1005/32/32" |  | ||||||
|                     alt="User avatar" |  | ||||||
|                   /> |  | ||||||
|                 </div> |  | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
| @ -70,21 +68,38 @@ | |||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { ref } from "vue"; | import { ref, computed } from "vue"; | ||||||
| import { Expand, Fold } from "@element-plus/icons-vue"; | import { Expand, Fold } from "@element-plus/icons-vue"; | ||||||
| import Sidebar from "./sidebar.vue"; | import Sidebar from "./sidebar.vue"; | ||||||
|  | import { useTheme } from "@/utils/theme"; | ||||||
| 
 | 
 | ||||||
| const isCollapse = ref(false); | const isCollapse = ref(false); | ||||||
| 
 | 
 | ||||||
| const toggleCollapse = () => { | const toggleCollapse = () => { | ||||||
|   isCollapse.value = !isCollapse.value; |   isCollapse.value = !isCollapse.value; | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | const toggleSidebar = () => { | ||||||
|  |   // 侧边栏切换逻辑(可以根据需要实现) | ||||||
|  |   console.log("Toggle sidebar"); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // 主题切换 | ||||||
|  | const theme = useTheme(); | ||||||
|  | 
 | ||||||
|  | const toggleTheme = () => { | ||||||
|  |   theme.toggle(); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const isDark = computed(() => theme.isDark()); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .layout-container { | .layout-container { | ||||||
|   height: 100vh; |   height: 100vh; | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|  |   background: var(--background-color); | ||||||
|  |   transition: var(--transition-base); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .full-height { | .full-height { | ||||||
| @ -92,28 +107,31 @@ const toggleCollapse = () => { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .main-sidebar { | .main-sidebar { | ||||||
|   background-color: #f5f5f5; |   background: var(--sidebar-bg); | ||||||
|   border-right: 1px solid #e0e0e0; |   border-right: 1px solid var(--border-color); | ||||||
|   transition: width 0.3s ease; |   transition: var(--transition-base); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .header { | .header { | ||||||
|   background: var(--el-bg-color); |   background: var(--header-bg); | ||||||
|   border-bottom: 1px solid var(--el-border-color-light); |   border-bottom: 1px solid var(--border-color); | ||||||
|   padding: 0 20px; |   padding: 0 20px; | ||||||
|   height: 81px; |   height: 81px; | ||||||
|   display: flex; |   display: flex; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|  |   box-shadow: var(--shadow-sm); | ||||||
|  |   transition: var(--transition-base); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .header-content { | .header-content { | ||||||
|   display: flex; |   display: flex; | ||||||
|  |   justify-content: space-between; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   width: 100%; |   width: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .main-content { | .main-content { | ||||||
|   background: var(--el-bg-color-page); |   background: var(--background-color); | ||||||
|   padding: 0; |   padding: 0; | ||||||
|   height: calc(100vh - 60px - 60px); /* 减去 header 和 footer 的高度 */ |   height: calc(100vh - 60px - 60px); /* 减去 header 和 footer 的高度 */ | ||||||
|   overflow-y: auto; |   overflow-y: auto; | ||||||
| @ -126,8 +144,8 @@ const toggleCollapse = () => { | |||||||
| 
 | 
 | ||||||
| .sub-sidebar-wrapper { | .sub-sidebar-wrapper { | ||||||
|   width: 150px; |   width: 150px; | ||||||
|   background-color: #f9f9f9; |   background: var(--background-secondary); | ||||||
|   border-right: 1px solid #e0e0e0; |   border-right: 1px solid var(--border-color); | ||||||
|   flex-shrink: 0; |   flex-shrink: 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -135,16 +153,16 @@ const toggleCollapse = () => { | |||||||
|   flex: 1; |   flex: 1; | ||||||
|   /* padding: 20px; */ |   /* padding: 20px; */ | ||||||
|   overflow-y: auto; |   overflow-y: auto; | ||||||
|   background-color: #ffffff; |   background: var(--background-color); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .footer { | .footer { | ||||||
|   display: flex; |   display: flex; | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   color: var(--el-text-color-disabled); |   color: var(--text-secondary); | ||||||
|   background: var(--el-bg-color); |   background: var(--header-bg); | ||||||
|   border-top: 1px solid var(--el-border-color-light); |   border-top: 1px solid var(--border-color); | ||||||
|   padding: 15px 0; |   padding: 15px 0; | ||||||
|   height: 81px; |   height: 81px; | ||||||
| } | } | ||||||
| @ -158,11 +176,11 @@ const toggleCollapse = () => { | |||||||
|     background: none; |     background: none; | ||||||
|     border: none; |     border: none; | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|     // color: $dark-gray; |     color: var(--text-color); | ||||||
|     // @include transition(color); |     transition: var(--transition-fast); | ||||||
| 
 | 
 | ||||||
|     &:hover { |     &:hover { | ||||||
|       // color: $primary-color; |       color: var(--primary-color); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -170,6 +188,7 @@ const toggleCollapse = () => { | |||||||
|     margin: 0; |     margin: 0; | ||||||
|     font-size: 1.25rem; |     font-size: 1.25rem; | ||||||
|     font-weight: 600; |     font-weight: 600; | ||||||
|  |     color: var(--text-color); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -182,13 +201,23 @@ const toggleCollapse = () => { | |||||||
|     position: relative; |     position: relative; | ||||||
|     display: flex; |     display: flex; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     // background-color: $light-gray; |     background: var(--background-secondary); | ||||||
|     border-radius: 6px; |     border: 1px solid var(--border-color); | ||||||
|  |     border-radius: var(--border-radius); | ||||||
|     padding: 0.5rem 1rem; |     padding: 0.5rem 1rem; | ||||||
|     width: 240px; |     width: 240px; | ||||||
|  |     transition: var(--transition-fast); | ||||||
|  | 
 | ||||||
|  |     &:hover { | ||||||
|  |       border-color: var(--border-color-hover); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     &:focus-within { | ||||||
|  |       border-color: var(--primary-color); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     .search-icon { |     .search-icon { | ||||||
|       // color: $medium-gray; |       color: var(--text-secondary); | ||||||
|       margin-right: 0.5rem; |       margin-right: 0.5rem; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -198,9 +227,10 @@ const toggleCollapse = () => { | |||||||
|       outline: none; |       outline: none; | ||||||
|       flex: 1; |       flex: 1; | ||||||
|       font-size: 0.875rem; |       font-size: 0.875rem; | ||||||
|  |       color: var(--text-color); | ||||||
| 
 | 
 | ||||||
|       &::placeholder { |       &::placeholder { | ||||||
|         // color: $medium-gray; |         color: var(--text-tertiary); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -213,28 +243,42 @@ const toggleCollapse = () => { | |||||||
|     .action-btn { |     .action-btn { | ||||||
|       background: none; |       background: none; | ||||||
|       border: none; |       border: none; | ||||||
|       // color: $dark-gray; |       color: var(--text-color); | ||||||
|       cursor: pointer; |       cursor: pointer; | ||||||
|       position: relative; |       position: relative; | ||||||
|       // @include transition(color); |       width: 36px; | ||||||
|  |       height: 36px; | ||||||
|  |       border-radius: var(--border-radius); | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: center; | ||||||
|  |       transition: var(--transition-fast); | ||||||
| 
 | 
 | ||||||
|       &:hover { |       &:hover { | ||||||
|         // color: $primary-color; |         background: var(--background-hover); | ||||||
|  |         color: var(--primary-color); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       &.theme-toggle-btn { | ||||||
|  |         i { | ||||||
|  |           font-size: 18px; | ||||||
|  |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       .notification-badge { |       .notification-badge { | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         top: -5px; |         top: -5px; | ||||||
|         right: -5px; |         right: -5px; | ||||||
|         // background-color: $danger-color; |         background: var(--error-color); | ||||||
|         color: white; |         color: var(--text-inverse); | ||||||
|         border-radius: 50%; |         border-radius: var(--border-radius-full); | ||||||
|         width: 16px; |         width: 16px; | ||||||
|         height: 16px; |         height: 16px; | ||||||
|         font-size: 0.7rem; |         font-size: 0.7rem; | ||||||
|         display: flex; |         display: flex; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         justify-content: center; |         justify-content: center; | ||||||
|  |         font-weight: 600; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -244,8 +288,14 @@ const toggleCollapse = () => { | |||||||
|       img { |       img { | ||||||
|         width: 36px; |         width: 36px; | ||||||
|         height: 36px; |         height: 36px; | ||||||
|         border-radius: 50%; |         border-radius: var(--border-radius-full); | ||||||
|         object-fit: cover; |         object-fit: cover; | ||||||
|  |         border: 2px solid var(--border-color); | ||||||
|  |         transition: var(--transition-fast); | ||||||
|  | 
 | ||||||
|  |         &:hover { | ||||||
|  |           border-color: var(--primary-color); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -17,9 +17,9 @@ | |||||||
|     <el-menu |     <el-menu | ||||||
|       :default-active="activeMenu" |       :default-active="activeMenu" | ||||||
|       class="sidebar-menu" |       class="sidebar-menu" | ||||||
|       background-color="#f5f5f5" |       background-color="var(--sidebar-bg)" | ||||||
|       text-color="#333" |       text-color="var(--text-color)" | ||||||
|       active-text-color="#409eff" |       active-text-color="var(--primary-color)" | ||||||
|       :collapse="isCollapse" |       :collapse="isCollapse" | ||||||
|       :collapse-transition="false" |       :collapse-transition="false" | ||||||
|       :router="false" |       :router="false" | ||||||
| @ -270,13 +270,16 @@ onMounted(() => { | |||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|  |   background: var(--sidebar-bg); | ||||||
|  |   transition: var(--transition-base); | ||||||
| 
 | 
 | ||||||
|   .logo { |   .logo { | ||||||
|     display: flex; |     display: flex; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     justify-content: center; |     justify-content: center; | ||||||
|     padding: 20px 0; |     padding: 20px 0; | ||||||
|     border-bottom: 1px solid #e0e0e0; |     border-bottom: 1px solid var(--border-color); | ||||||
|  |     transition: var(--transition-base); | ||||||
| 
 | 
 | ||||||
|     .logo-img { |     .logo-img { | ||||||
|       height: 40px; |       height: 40px; | ||||||
| @ -285,7 +288,7 @@ onMounted(() => { | |||||||
|     .logo-text { |     .logo-text { | ||||||
|       font-size: 18px; |       font-size: 18px; | ||||||
|       font-weight: bold; |       font-weight: bold; | ||||||
|       color: #333; |       color: var(--text-color); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -296,20 +299,26 @@ onMounted(() => { | |||||||
| 
 | 
 | ||||||
|   .bottom-settings { |   .bottom-settings { | ||||||
|     // margin-bottom: 20px; |     // margin-bottom: 20px; | ||||||
|     border-bottom: 1px solid #e0e0e0; |     border-bottom: 1px solid var(--border-color); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .user-dropdown-trigger { |   .user-dropdown-trigger { | ||||||
|     width: 40px; |     width: 40px; | ||||||
|     height: 40px; |     height: 40px; | ||||||
|     border-radius: 50%; |     border-radius: var(--border-radius-full); | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|     transition: transform 0.2s; |     transition: var(--transition-base); | ||||||
|     margin: 20px 0; |     margin: 20px 0; | ||||||
|  |     border: 2px solid var(--border-color); | ||||||
|  | 
 | ||||||
|  |     &:hover { | ||||||
|  |       border-color: var(--primary-color); | ||||||
|  |       transform: scale(1.05); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     img { |     img { | ||||||
|       object-fit: cover; |       object-fit: cover; | ||||||
|       border-radius: 50%; |       border-radius: var(--border-radius-full); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -334,26 +343,43 @@ onMounted(() => { | |||||||
|   flex: 1; |   flex: 1; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // 菜单项样式 - 统一管理 | ||||||
| .sidebar-menu :deep(.el-menu-item) { | .sidebar-menu :deep(.el-menu-item) { | ||||||
|   height: 50px; |   height: 50px; | ||||||
|   line-height: 50px; |   line-height: 50px; | ||||||
|   display: flex !important; |   display: flex !important; | ||||||
|   align-items: center !important; |   align-items: center !important; | ||||||
|   justify-content: flex-start !important; |   justify-content: flex-start !important; | ||||||
| } |   transition: var(--transition-fast); | ||||||
|    |    | ||||||
| .sidebar-menu :deep(.el-menu-item) i { |   &:hover { | ||||||
|   margin-right: 8px; |     background-color: var(--background-hover) !important; | ||||||
|   font-size: 16px; |     color: var(--primary-color) !important; | ||||||
|  |      | ||||||
|  |     i { | ||||||
|  |       color: var(--primary-color) !important; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   i { | ||||||
|  |     margin-right: 8px; | ||||||
|  |     font-size: 16px; | ||||||
|  |     transition: var(--transition-fast); | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .sidebar-menu :deep(.el-menu-item.is-active) { | .sidebar-menu :deep(.el-menu-item.is-active) { | ||||||
|   background-color: #e6f0ff !important; |   background-color: var(--background-hover) !important; | ||||||
|  |   color: var(--primary-color) !important; | ||||||
|  |    | ||||||
|  |   i { | ||||||
|  |     color: var(--primary-color) !important; | ||||||
|  |   } | ||||||
| } | } | ||||||
| .user-nickname { | .user-nickname { | ||||||
|   font-size: 14px; |   font-size: 14px; | ||||||
|   font-weight: bold; |   font-weight: bold; | ||||||
|   color: #333; |   color: var(--text-color); | ||||||
|   text-align: center; |   text-align: center; | ||||||
|   padding: 20px 40px; |   padding: 20px 40px; | ||||||
| } | } | ||||||
| @ -361,11 +387,11 @@ onMounted(() => { | |||||||
| .debug-info { | .debug-info { | ||||||
|   padding: 20px; |   padding: 20px; | ||||||
|   text-align: center; |   text-align: center; | ||||||
|   color: #666; |   color: var(--text-secondary); | ||||||
|   font-size: 12px; |   font-size: 12px; | ||||||
|   background: #f0f0f0; |   background: var(--warning-bg); | ||||||
|   border: 1px dashed #ccc; |   border: 1px dashed var(--warning-color); | ||||||
|   margin: 10px; |   margin: 10px; | ||||||
|   border-radius: 4px; |   border-radius: var(--border-radius); | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  | |||||||
| @ -3,9 +3,9 @@ | |||||||
|     <el-menu |     <el-menu | ||||||
|       :default-active="activeSubMenu" |       :default-active="activeSubMenu" | ||||||
|       class="sub-sidebar-menu" |       class="sub-sidebar-menu" | ||||||
|       background-color="#fafafa" |       background-color="var(--sidebar-bg)" | ||||||
|       text-color="#666" |       text-color="var(--text-secondary)" | ||||||
|       active-text-color="#409eff" |       active-text-color="var(--primary-color)" | ||||||
|       :router="false" |       :router="false" | ||||||
|     > |     > | ||||||
|       <el-menu-item  |       <el-menu-item  | ||||||
| @ -84,46 +84,47 @@ const handleSubMenuClick = (item: SubMenuItem) => { | |||||||
| .sub-sidebar-container { | .sub-sidebar-container { | ||||||
|   width: 150px; |   width: 150px; | ||||||
|   height: 100%; |   height: 100%; | ||||||
|   background: #fafafa; |   background: var(--sidebar-bg); | ||||||
|   border-right: 1px solid #e8e8e8; |   border-right: 1px solid var(--border-color); | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|  |   transition: var(--transition-base); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .back-section { | .back-section { | ||||||
|   display: flex; |   display: flex; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   padding: 16px 20px; |   padding: 16px 20px; | ||||||
|   border-bottom: 1px solid #e8e8e8; |   border-bottom: 1px solid var(--border-color); | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|   transition: background-color 0.2s; |   transition: var(--transition-fast); | ||||||
|    |    | ||||||
|   &:hover { |   &:hover { | ||||||
|     background-color: #f0f0f0; |     background-color: var(--background-hover); | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   .el-icon { |   .el-icon { | ||||||
|     margin-right: 8px; |     margin-right: 8px; | ||||||
|     color: #666; |     color: var(--text-secondary); | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   .back-text { |   .back-text { | ||||||
|     font-size: 14px; |     font-size: 14px; | ||||||
|     color: #666; |     color: var(--text-secondary); | ||||||
|     font-weight: 500; |     font-weight: 500; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .module-title { | .module-title { | ||||||
|   padding: 20px; |   padding: 20px; | ||||||
|   border-bottom: 1px solid #e8e8e8; |   border-bottom: 1px solid var(--border-color); | ||||||
|    |    | ||||||
|   h3 { |   h3 { | ||||||
|     margin: 0; |     margin: 0; | ||||||
|     font-size: 16px; |     font-size: 16px; | ||||||
|     font-weight: 600; |     font-weight: 600; | ||||||
|     color: #333; |     color: var(--text-color); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -135,28 +136,46 @@ const handleSubMenuClick = (item: SubMenuItem) => { | |||||||
|     height: 44px; |     height: 44px; | ||||||
|     line-height: 44px; |     line-height: 44px; | ||||||
|     margin: 4px 8px; |     margin: 4px 8px; | ||||||
|     border-radius: 6px; |     border-radius: var(--border-radius); | ||||||
|  |     transition: var(--transition-fast); | ||||||
|  |     display: flex !important; | ||||||
|  |     align-items: center !important; | ||||||
|  |      | ||||||
|  |     i { | ||||||
|  |       margin-right: 8px; | ||||||
|  |       font-size: 16px; | ||||||
|  |       transition: var(--transition-fast); | ||||||
|  |     } | ||||||
|      |      | ||||||
|     &:hover { |     &:hover { | ||||||
|       background-color: #f0f0f0; |       background-color: var(--background-hover) !important; | ||||||
|  |       color: var(--primary-color) !important; | ||||||
|  |        | ||||||
|  |       i { | ||||||
|  |         color: var(--primary-color) !important; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     &.is-active { |     &.is-active { | ||||||
|       background-color: #e6f0ff; |       background-color: var(--background-hover) !important; | ||||||
|       color: #409eff; |       color: var(--primary-color) !important; | ||||||
|  |        | ||||||
|  |       i { | ||||||
|  |         color: var(--primary-color) !important; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .module-description { | .module-description { | ||||||
|   padding: 16px 20px; |   padding: 16px 20px; | ||||||
|   border-top: 1px solid #e8e8e8; |   border-top: 1px solid var(--border-color); | ||||||
|   background-color: #f5f5f5; |   background-color: var(--background-secondary); | ||||||
|    |    | ||||||
|   p { |   p { | ||||||
|     margin: 0; |     margin: 0; | ||||||
|     font-size: 12px; |     font-size: 12px; | ||||||
|     color: #999; |     color: var(--text-tertiary); | ||||||
|     line-height: 1.5; |     line-height: 1.5; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -409,11 +409,12 @@ onActivated(async () => { | |||||||
| .universal-sub-menu { | .universal-sub-menu { | ||||||
|   display: flex; |   display: flex; | ||||||
|   /* height: 100%; */ |   /* height: 100%; */ | ||||||
|   background: #f5f5f5; |   background: var(--background-secondary); | ||||||
|  |   transition: var(--transition-base); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .sub-sidebar-wrapper { | .sub-sidebar-wrapper { | ||||||
|   border-right: 1px solid #e4e7ed; |   border-right: 1px solid var(--border-color); | ||||||
|   overflow-y: none; |   overflow-y: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -422,6 +423,7 @@ onActivated(async () => { | |||||||
|   padding: 20px; |   padding: 20px; | ||||||
|   overflow-y: auto; |   overflow-y: auto; | ||||||
|   height: calc(100vh - 162px); |   height: calc(100vh - 162px); | ||||||
|  |   background: var(--background-color); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* 加载状态样式 */ | /* 加载状态样式 */ | ||||||
| @ -432,17 +434,18 @@ onActivated(async () => { | |||||||
|   align-items: center; |   align-items: center; | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
|   height: 200px; |   height: 200px; | ||||||
|   background: #ffffff; |   background: var(--card-bg); | ||||||
|   border-radius: 8px; |   border-radius: var(--border-radius-lg); | ||||||
|   box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |   box-shadow: var(--card-shadow); | ||||||
|  |   border: 1px solid var(--border-color); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .loading-spinner { | .loading-spinner { | ||||||
|   width: 32px; |   width: 32px; | ||||||
|   height: 32px; |   height: 32px; | ||||||
|   border: 2px solid rgba(64, 158, 255, 0.2); |   border: 2px solid var(--info-bg); | ||||||
|   border-radius: 50%; |   border-radius: var(--border-radius-full); | ||||||
|   border-top-color: #409eff; |   border-top-color: var(--info-color); | ||||||
|   animation: spin 1s ease-in-out infinite; |   animation: spin 1s ease-in-out infinite; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -455,7 +458,7 @@ onActivated(async () => { | |||||||
| .loading-state p, | .loading-state p, | ||||||
| .error-state p { | .error-state p { | ||||||
|   margin-top: 12px; |   margin-top: 12px; | ||||||
|   color: #606266; |   color: var(--text-secondary); | ||||||
|   font-size: 14px; |   font-size: 14px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -465,11 +468,11 @@ onActivated(async () => { | |||||||
| 
 | 
 | ||||||
| .error-icon i { | .error-icon i { | ||||||
|   font-size: 48px; |   font-size: 48px; | ||||||
|   color: #f56c6c; |   color: var(--error-color); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .error-state h3 { | .error-state h3 { | ||||||
|   color: #f56c6c; |   color: var(--error-color); | ||||||
|   margin-bottom: 8px; |   margin-bottom: 8px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -490,15 +493,15 @@ onActivated(async () => { | |||||||
| .component-loading .loading-spinner { | .component-loading .loading-spinner { | ||||||
|   width: 32px; |   width: 32px; | ||||||
|   height: 32px; |   height: 32px; | ||||||
|   border: 2px solid rgba(64, 158, 255, 0.2); |   border: 2px solid var(--info-bg); | ||||||
|   border-radius: 50%; |   border-radius: var(--border-radius-full); | ||||||
|   border-top-color: #409eff; |   border-top-color: var(--info-color); | ||||||
|   animation: spin 1s ease-in-out infinite; |   animation: spin 1s ease-in-out infinite; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .component-loading p { | .component-loading p { | ||||||
|   margin-top: 12px; |   margin-top: 12px; | ||||||
|   color: #606266; |   color: var(--text-secondary); | ||||||
|   font-size: 14px; |   font-size: 14px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -512,10 +515,11 @@ onActivated(async () => { | |||||||
| .development-component { | .development-component { | ||||||
|   padding: 60px 40px; |   padding: 60px 40px; | ||||||
|   text-align: center; |   text-align: center; | ||||||
|   background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |   background: var(--background-tertiary); | ||||||
|   border-radius: 12px; |   border-radius: var(--border-radius-xl); | ||||||
|   box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.1); |   box-shadow: var(--shadow-lg); | ||||||
|   border: 1px solid #e4e7ed; |   border: 1px solid var(--border-color); | ||||||
|  |   transition: var(--transition-base); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .development-icon { | .development-icon { | ||||||
| @ -524,46 +528,47 @@ onActivated(async () => { | |||||||
| 
 | 
 | ||||||
| .development-icon i { | .development-icon i { | ||||||
|   font-size: 64px; |   font-size: 64px; | ||||||
|   color: #409eff; |   color: var(--info-color); | ||||||
|   opacity: 0.8; |   opacity: 0.8; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .development-component h3 { | .development-component h3 { | ||||||
|   color: #303133; |   color: var(--text-color); | ||||||
|   margin-bottom: 16px; |   margin-bottom: 16px; | ||||||
|   font-size: 24px; |   font-size: 24px; | ||||||
|   font-weight: 600; |   font-weight: 600; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .development-path { | .development-path { | ||||||
|   color: #606266; |   color: var(--text-secondary); | ||||||
|   margin: 12px 0; |   margin: 12px 0; | ||||||
|   font-size: 16px; |   font-size: 16px; | ||||||
|   font-family: 'Courier New', monospace; |   font-family: 'Courier New', monospace; | ||||||
|   background: rgba(64, 158, 255, 0.1); |   background: var(--info-bg); | ||||||
|   padding: 8px 16px; |   padding: 8px 16px; | ||||||
|   border-radius: 6px; |   border-radius: var(--border-radius); | ||||||
|   display: inline-block; |   display: inline-block; | ||||||
|  |   border: 1px solid var(--info-color); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .development-desc { | .development-desc { | ||||||
|   color: #909399; |   color: var(--text-tertiary); | ||||||
|   margin: 16px 0 24px 0; |   margin: 16px 0 24px 0; | ||||||
|   font-size: 16px; |   font-size: 16px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .development-tips { | .development-tips { | ||||||
|   background: rgba(255, 255, 255, 0.8); |   background: var(--info-bg); | ||||||
|   padding: 20px; |   padding: 20px; | ||||||
|   border-radius: 8px; |   border-radius: var(--border-radius-lg); | ||||||
|   border-left: 4px solid #409eff; |   border-left: 4px solid var(--info-color); | ||||||
|   text-align: left; |   text-align: left; | ||||||
|   max-width: 500px; |   max-width: 500px; | ||||||
|   margin: 0 auto; |   margin: 0 auto; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .development-tips p { | .development-tips p { | ||||||
|   color: #606266; |   color: var(--text-secondary); | ||||||
|   margin: 8px 0; |   margin: 8px 0; | ||||||
|   font-size: 14px; |   font-size: 14px; | ||||||
|   line-height: 1.6; |   line-height: 1.6; | ||||||
| @ -572,11 +577,11 @@ onActivated(async () => { | |||||||
| .debug-info { | .debug-info { | ||||||
|   padding: 20px; |   padding: 20px; | ||||||
|   text-align: center; |   text-align: center; | ||||||
|   color: #666; |   color: var(--text-secondary); | ||||||
|   font-size: 12px; |   font-size: 12px; | ||||||
|   background: #f0f0f0; |   background: var(--warning-bg); | ||||||
|   border: 1px dashed #ccc; |   border: 1px dashed var(--warning-color); | ||||||
|   margin: 10px; |   margin: 10px; | ||||||
|   border-radius: 4px; |   border-radius: var(--border-radius); | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  | |||||||
| @ -393,6 +393,7 @@ $warning-color: #f59e0b; | |||||||
| $danger-color: #ef4444; | $danger-color: #ef4444; | ||||||
| $purple-color: #8b5cf6; | $purple-color: #8b5cf6; | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| // 工具类 | // 工具类 | ||||||
| @mixin transition($property: all, $duration: 0.3s, $easing: ease) { | @mixin transition($property: all, $duration: 0.3s, $easing: ease) { | ||||||
|   transition: $property $duration $easing; |   transition: $property $duration $easing; | ||||||
| @ -404,14 +405,15 @@ $purple-color: #8b5cf6; | |||||||
|     "medium": 0 4px 6px rgba(0, 0, 0, 0.05), |     "medium": 0 4px 6px rgba(0, 0, 0, 0.05), | ||||||
|     "large": 0 10px 15px rgba(0, 0, 0, 0.07), |     "large": 0 10px 15px rgba(0, 0, 0, 0.07), | ||||||
|   ); |   ); | ||||||
|   box-shadow: map-get($shadows, $size); |   // box-shadow: map-get($shadows, $size); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 主容器 | // 主容器 | ||||||
| .dashboard-container { | .dashboard-container { | ||||||
|   display: flex; |   display: flex; | ||||||
|   min-height: 100vh; |   min-height: 100vh; | ||||||
|   background-color: #f9fafb; |   background: var(--background-color); | ||||||
|  |   transition: var(--transition-base); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 侧边栏 | // 侧边栏 | ||||||
| @ -570,8 +572,8 @@ $purple-color: #8b5cf6; | |||||||
| 
 | 
 | ||||||
|   .main-header { |   .main-header { | ||||||
|     height: $header-height; |     height: $header-height; | ||||||
|     background-color: white; |     background: var(--header-bg); | ||||||
|     @include shadow; |     box-shadow: var(--shadow-sm); | ||||||
|     padding: 0 2rem; |     padding: 0 2rem; | ||||||
|     display: flex; |     display: flex; | ||||||
|     align-items: center; |     align-items: center; | ||||||
| @ -589,11 +591,11 @@ $purple-color: #8b5cf6; | |||||||
|         background: none; |         background: none; | ||||||
|         border: none; |         border: none; | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
|         color: $dark-gray; |         color: var(--text-color); | ||||||
|         @include transition(color); |         @include transition(color); | ||||||
| 
 | 
 | ||||||
|         &:hover { |         &:hover { | ||||||
|           color: $primary-color; |           color: var(--primary-color); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
| @ -613,13 +615,19 @@ $purple-color: #8b5cf6; | |||||||
|         position: relative; |         position: relative; | ||||||
|         display: flex; |         display: flex; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         background-color: $light-gray; |         background: var(--background-secondary); | ||||||
|         border-radius: 6px; |         border: 1px solid var(--border-color); | ||||||
|  |         border-radius: var(--border-radius); | ||||||
|         padding: 0.5rem 1rem; |         padding: 0.5rem 1rem; | ||||||
|         width: 240px; |         width: 240px; | ||||||
|  |         transition: var(--transition-fast); | ||||||
|  | 
 | ||||||
|  |         &:hover { | ||||||
|  |           border-color: var(--border-color-hover); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         .search-icon { |         .search-icon { | ||||||
|           color: $medium-gray; |           color: var(--text-secondary); | ||||||
|           margin-right: 0.5rem; |           margin-right: 0.5rem; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -629,9 +637,10 @@ $purple-color: #8b5cf6; | |||||||
|           outline: none; |           outline: none; | ||||||
|           flex: 1; |           flex: 1; | ||||||
|           font-size: 0.875rem; |           font-size: 0.875rem; | ||||||
|  |           color: var(--text-color); | ||||||
| 
 | 
 | ||||||
|           &::placeholder { |           &::placeholder { | ||||||
|             color: $medium-gray; |             color: var(--text-tertiary); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| @ -644,22 +653,22 @@ $purple-color: #8b5cf6; | |||||||
|         .action-btn { |         .action-btn { | ||||||
|           background: none; |           background: none; | ||||||
|           border: none; |           border: none; | ||||||
|           color: $dark-gray; |           color: var(--text-color); | ||||||
|           cursor: pointer; |           cursor: pointer; | ||||||
|           position: relative; |           position: relative; | ||||||
|           @include transition(color); |           transition: var(--transition-fast); | ||||||
| 
 | 
 | ||||||
|           &:hover { |           &:hover { | ||||||
|             color: $primary-color; |             color: var(--primary-color); | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           .notification-badge { |           .notification-badge { | ||||||
|             position: absolute; |             position: absolute; | ||||||
|             top: -5px; |             top: -5px; | ||||||
|             right: -5px; |             right: -5px; | ||||||
|             background-color: $danger-color; |             background: var(--error-color); | ||||||
|             color: white; |             color: var(--text-inverse); | ||||||
|             border-radius: 50%; |             border-radius: var(--border-radius-full); | ||||||
|             width: 16px; |             width: 16px; | ||||||
|             height: 16px; |             height: 16px; | ||||||
|             font-size: 0.7rem; |             font-size: 0.7rem; | ||||||
| @ -693,23 +702,25 @@ $purple-color: #8b5cf6; | |||||||
|       margin-bottom: 2rem; |       margin-bottom: 2rem; | ||||||
| 
 | 
 | ||||||
|       .stat-card { |       .stat-card { | ||||||
|         background-color: white; |         background: var(--card-bg); | ||||||
|         @include shadow; |         box-shadow: var(--card-shadow); | ||||||
|         border-radius: 8px; |         border-radius: var(--border-radius-lg); | ||||||
|         padding: 1.25rem; |         padding: 1.25rem; | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: space-between; |         justify-content: space-between; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         border-top: 4px solid $secondary-color; |         border-top: 4px solid var(--primary-color); | ||||||
|         @include transition(transform, 0.3s); |         transition: var(--transition-base); | ||||||
|  |         border: 1px solid var(--border-color); | ||||||
| 
 | 
 | ||||||
|         &:hover { |         &:hover { | ||||||
|           transform: translateY(-5px); |           transform: translateY(-5px); | ||||||
|  |           box-shadow: var(--shadow-lg); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         .stat-info { |         .stat-info { | ||||||
|           .stat-label { |           .stat-label { | ||||||
|             color: $medium-gray; |             color: var(--text-secondary); | ||||||
|             font-size: 0.875rem; |             font-size: 0.875rem; | ||||||
|             margin: 0 0 0.5rem 0; |             margin: 0 0 0.5rem 0; | ||||||
|           } |           } | ||||||
| @ -718,6 +729,7 @@ $purple-color: #8b5cf6; | |||||||
|             font-size: 1.5rem; |             font-size: 1.5rem; | ||||||
|             font-weight: 600; |             font-weight: 600; | ||||||
|             margin: 0 0 0.25rem 0; |             margin: 0 0 0.25rem 0; | ||||||
|  |             color: var(--text-color); | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           .stat-change { |           .stat-change { | ||||||
| @ -728,28 +740,28 @@ $purple-color: #8b5cf6; | |||||||
|             gap: 0.5rem; |             gap: 0.5rem; | ||||||
| 
 | 
 | ||||||
|             .change-period { |             .change-period { | ||||||
|               color: $medium-gray; |               color: var(--text-tertiary); | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           .positive { |           .positive { | ||||||
|             color: $success-color; |             color: var(--success-color); | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           .negative { |           .negative { | ||||||
|             color: $danger-color; |             color: var(--error-color); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         .stat-icon { |         .stat-icon { | ||||||
|           width: 48px; |           width: 48px; | ||||||
|           height: 48px; |           height: 48px; | ||||||
|           border-radius: 12px; |           border-radius: var(--border-radius-lg); | ||||||
|           background-color: rgba($secondary-color, 0.1); |           background: var(--info-bg); | ||||||
|           display: flex; |           display: flex; | ||||||
|           align-items: center; |           align-items: center; | ||||||
|           justify-content: center; |           justify-content: center; | ||||||
|           color: $secondary-color; |           color: var(--info-color); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -761,22 +773,30 @@ $purple-color: #8b5cf6; | |||||||
|       margin-bottom: 2rem; |       margin-bottom: 2rem; | ||||||
| 
 | 
 | ||||||
|       .chart-card { |       .chart-card { | ||||||
|         background-color: white; |         background: var(--card-bg); | ||||||
|         @include shadow; |         box-shadow: var(--card-shadow); | ||||||
|         border-radius: 8px; |         border-radius: var(--border-radius-lg); | ||||||
|         overflow: hidden; |         overflow: hidden; | ||||||
|  |         border: 1px solid var(--border-color); | ||||||
|  |         transition: var(--transition-base); | ||||||
|  | 
 | ||||||
|  |         &:hover { | ||||||
|  |           box-shadow: var(--shadow-lg); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         .chart-header { |         .chart-header { | ||||||
|           padding: 1.25rem 1.5rem; |           padding: 1.25rem 1.5rem; | ||||||
|           border-bottom: 1px solid $light-gray; |           border-bottom: 1px solid var(--border-color); | ||||||
|           display: flex; |           display: flex; | ||||||
|           align-items: center; |           align-items: center; | ||||||
|           justify-content: space-between; |           justify-content: space-between; | ||||||
|  |           background: var(--header-bg); | ||||||
| 
 | 
 | ||||||
|           h2 { |           h2 { | ||||||
|             margin: 0; |             margin: 0; | ||||||
|             font-size: 1.125rem; |             font-size: 1.125rem; | ||||||
|             font-weight: 600; |             font-weight: 600; | ||||||
|  |             color: var(--text-color); | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           .chart-filters { |           .chart-filters { | ||||||
| @ -785,22 +805,24 @@ $purple-color: #8b5cf6; | |||||||
| 
 | 
 | ||||||
|             .filter-btn { |             .filter-btn { | ||||||
|               background: none; |               background: none; | ||||||
|               border: 1px solid $light-gray; |               border: 1px solid var(--border-color); | ||||||
|               border-radius: 4px; |               border-radius: var(--border-radius); | ||||||
|               padding: 0.375rem 0.75rem; |               padding: 0.375rem 0.75rem; | ||||||
|               font-size: 0.75rem; |               font-size: 0.75rem; | ||||||
|               cursor: pointer; |               cursor: pointer; | ||||||
|               @include transition(all); |               color: var(--text-color); | ||||||
|  |               transition: var(--transition-fast); | ||||||
| 
 | 
 | ||||||
|               &:hover { |               &:hover { | ||||||
|                 border-color: $secondary-color; |                 border-color: var(--primary-color); | ||||||
|                 color: $secondary-color; |                 color: var(--primary-color); | ||||||
|  |                 background: var(--background-hover); | ||||||
|               } |               } | ||||||
| 
 | 
 | ||||||
|               &.active { |               &.active { | ||||||
|                 background-color: $secondary-color; |                 background: var(--primary-color); | ||||||
|                 color: white; |                 color: var(--text-inverse); | ||||||
|                 border-color: $secondary-color; |                 border-color: var(--primary-color); | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
| @ -809,6 +831,7 @@ $purple-color: #8b5cf6; | |||||||
|         .chart-container { |         .chart-container { | ||||||
|           padding: 1.5rem; |           padding: 1.5rem; | ||||||
|           width: 100%; |           width: 100%; | ||||||
|  |           background: var(--card-bg); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -820,34 +843,42 @@ $purple-color: #8b5cf6; | |||||||
| 
 | 
 | ||||||
|       .tasks-card, |       .tasks-card, | ||||||
|       .activities-card { |       .activities-card { | ||||||
|         background-color: white; |         background: var(--card-bg); | ||||||
|         @include shadow; |         box-shadow: var(--card-shadow); | ||||||
|         border-radius: 8px; |         border-radius: var(--border-radius-lg); | ||||||
|         overflow: hidden; |         overflow: hidden; | ||||||
|  |         border: 1px solid var(--border-color); | ||||||
|  |         transition: var(--transition-base); | ||||||
|  | 
 | ||||||
|  |         &:hover { | ||||||
|  |           box-shadow: var(--shadow-lg); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         .card-header { |         .card-header { | ||||||
|           padding: 1.25rem 1.5rem; |           padding: 1.25rem 1.5rem; | ||||||
|           border-bottom: 1px solid $light-gray; |           border-bottom: 1px solid var(--border-color); | ||||||
|           display: flex; |           display: flex; | ||||||
|           align-items: center; |           align-items: center; | ||||||
|           justify-content: space-between; |           justify-content: space-between; | ||||||
|  |           background: var(--header-bg); | ||||||
| 
 | 
 | ||||||
|           h2 { |           h2 { | ||||||
|             margin: 0; |             margin: 0; | ||||||
|             font-size: 1.125rem; |             font-size: 1.125rem; | ||||||
|             font-weight: 600; |             font-weight: 600; | ||||||
|  |             color: var(--text-color); | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           .view-all { |           .view-all { | ||||||
|             background: none; |             background: none; | ||||||
|             border: none; |             border: none; | ||||||
|             color: $secondary-color; |             color: var(--primary-color); | ||||||
|             font-size: 0.875rem; |             font-size: 0.875rem; | ||||||
|             cursor: pointer; |             cursor: pointer; | ||||||
|             @include transition(color); |             transition: var(--transition-fast); | ||||||
| 
 | 
 | ||||||
|             &:hover { |             &:hover { | ||||||
|               color: $primary-color; |               color: var(--primary-hover); | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| @ -861,15 +892,16 @@ $purple-color: #8b5cf6; | |||||||
|           .task-item, |           .task-item, | ||||||
|           .activity-item { |           .activity-item { | ||||||
|             padding: 1rem 1.5rem; |             padding: 1rem 1.5rem; | ||||||
|             border-bottom: 1px solid $light-gray; |             border-bottom: 1px solid var(--border-color); | ||||||
|             @include transition(background-color); |             background: var(--card-bg); | ||||||
|  |             transition: var(--transition-fast); | ||||||
| 
 | 
 | ||||||
|             &:last-child { |             &:last-child { | ||||||
|               border-bottom: none; |               border-bottom: none; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             &:hover { |             &:hover { | ||||||
|               background-color: $light-gray; |               background: var(--background-hover); | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| @ -894,10 +926,10 @@ $purple-color: #8b5cf6; | |||||||
|               left: 0; |               left: 0; | ||||||
|               width: 20px; |               width: 20px; | ||||||
|               height: 20px; |               height: 20px; | ||||||
|               border-radius: 4px; |               border-radius: var(--border-radius); | ||||||
|               border: 2px solid $medium-gray; |               border: 2px solid var(--border-color); | ||||||
|               cursor: pointer; |               cursor: pointer; | ||||||
|               @include transition(all); |               transition: var(--transition-fast); | ||||||
| 
 | 
 | ||||||
|               &::after { |               &::after { | ||||||
|                 content: ""; |                 content: ""; | ||||||
| @ -907,15 +939,15 @@ $purple-color: #8b5cf6; | |||||||
|                 top: 2px; |                 top: 2px; | ||||||
|                 width: 5px; |                 width: 5px; | ||||||
|                 height: 10px; |                 height: 10px; | ||||||
|                 border: solid white; |                 border: solid var(--text-inverse); | ||||||
|                 border-width: 0 2px 2px 0; |                 border-width: 0 2px 2px 0; | ||||||
|                 transform: rotate(45deg); |                 transform: rotate(45deg); | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             input:checked + label { |             input:checked + label { | ||||||
|               background-color: $success-color; |               background: var(--success-color); | ||||||
|               border-color: $success-color; |               border-color: var(--success-color); | ||||||
| 
 | 
 | ||||||
|               &::after { |               &::after { | ||||||
|                 display: block; |                 display: block; | ||||||
| @ -929,40 +961,41 @@ $purple-color: #8b5cf6; | |||||||
|             .task-title { |             .task-title { | ||||||
|               margin: 0 0 0.25rem 0; |               margin: 0 0 0.25rem 0; | ||||||
|               font-size: 0.875rem; |               font-size: 0.875rem; | ||||||
|               @include transition(text-decoration, color); |               color: var(--text-color); | ||||||
|  |               transition: var(--transition-base); | ||||||
| 
 | 
 | ||||||
|               &.completed { |               &.completed { | ||||||
|                 text-decoration: line-through; |                 text-decoration: line-through; | ||||||
|                 color: $medium-gray; |                 color: var(--text-tertiary); | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             .task-date { |             .task-date { | ||||||
|               margin: 0; |               margin: 0; | ||||||
|               font-size: 0.75rem; |               font-size: 0.75rem; | ||||||
|               color: $medium-gray; |               color: var(--text-secondary); | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           .task-priority { |           .task-priority { | ||||||
|             font-size: 0.75rem; |             font-size: 0.75rem; | ||||||
|             padding: 0.25rem 0.5rem; |             padding: 0.25rem 0.5rem; | ||||||
|             border-radius: 4px; |             border-radius: var(--border-radius); | ||||||
|             font-weight: 500; |             font-weight: 500; | ||||||
| 
 | 
 | ||||||
|             &.high { |             &.high { | ||||||
|               background-color: rgba(239, 68, 68, 0.1); |               background: var(--error-bg); | ||||||
|               color: $danger-color; |               color: var(--error-color); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             &.medium { |             &.medium { | ||||||
|               background-color: rgba(245, 158, 11, 0.1); |               background: var(--warning-bg); | ||||||
|               color: $warning-color; |               color: var(--warning-color); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             &.low { |             &.low { | ||||||
|               background-color: rgba(16, 185, 129, 0.1); |               background: var(--success-bg); | ||||||
|               color: $success-color; |               color: var(--success-color); | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| @ -998,7 +1031,7 @@ $purple-color: #8b5cf6; | |||||||
|             .activity-time { |             .activity-time { | ||||||
|               margin: 0; |               margin: 0; | ||||||
|               font-size: 0.75rem; |               font-size: 0.75rem; | ||||||
|               color: $medium-gray; |               color: var(--text-secondary); | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -2,10 +2,12 @@ | |||||||
|   <el-card class="box-card"> |   <el-card class="box-card"> | ||||||
|     <div class="header-bar"> |     <div class="header-bar"> | ||||||
|       <h2>角色管理</h2> |       <h2>角色管理</h2> | ||||||
|       <el-button type="primary" @click="showRoleDialog = true"> |       <div class="header-actions"> | ||||||
|         <el-icon><Plus /></el-icon> |         <el-button type="primary" @click="showRoleDialog = true"> | ||||||
|         添加角色 |           <el-icon><Plus /></el-icon> | ||||||
|       </el-button> |           添加角色 | ||||||
|  |         </el-button> | ||||||
|  |       </div> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <!-- 加载状态 --> |     <!-- 加载状态 --> | ||||||
| @ -226,15 +228,6 @@ onMounted(() => { | |||||||
|   box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.03); |   box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.03); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .header-bar { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   margin-bottom: 20px; |  | ||||||
|   padding-bottom: 6px; |  | ||||||
|   border-bottom: 1px solid #f0f1f2; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-bar h2 { | .header-bar h2 { | ||||||
|   font-size: 1.18rem; |   font-size: 1.18rem; | ||||||
|   font-weight: 600; |   font-weight: 600; | ||||||
|  | |||||||
| @ -526,15 +526,17 @@ onMounted(() => { | |||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .tenant-management-module { | .tenant-management-module { | ||||||
|   padding: 32px 30px 24px 20px; |   padding: 20px; | ||||||
|   min-height: 600px; |   min-height: 600px; | ||||||
|   background: #fff; |   background: var(--card-bg); | ||||||
|  |   border-radius: var(--border-radius-lg); | ||||||
|  |   box-shadow: var(--card-shadow); | ||||||
|  |   transition: var(--transition-base); | ||||||
| } | } | ||||||
| .header-bar { | .header-bar { | ||||||
|   display: flex; |   display: flex; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   justify-content: space-between; |   justify-content: space-between; | ||||||
|   margin-bottom: 18px; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .header-actions { | .header-actions { | ||||||
| @ -551,9 +553,9 @@ onMounted(() => { | |||||||
| .loading-spinner { | .loading-spinner { | ||||||
|   width: 36px; |   width: 36px; | ||||||
|   height: 36px; |   height: 36px; | ||||||
|   border: 4px solid #e5e5e5; |   border: 4px solid var(--border-color); | ||||||
|   border-left-color: #409eff; |   border-left-color: var(--primary-color); | ||||||
|   border-radius: 50%; |   border-radius: var(--border-radius-full); | ||||||
|   animation: spin 1s linear infinite; |   animation: spin 1s linear infinite; | ||||||
|   margin-bottom: 16px; |   margin-bottom: 16px; | ||||||
| } | } | ||||||
| @ -578,11 +580,11 @@ onMounted(() => { | |||||||
| 
 | 
 | ||||||
| .tenant-detail .el-descriptions-item__label { | .tenant-detail .el-descriptions-item__label { | ||||||
|   font-weight: 600; |   font-weight: 600; | ||||||
|   color: #606266; |   color: var(--text-secondary); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .tenant-detail .el-descriptions-item__content { | .tenant-detail .el-descriptions-item__content { | ||||||
|   color: #303133; |   color: var(--text-color); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .audit-content { | .audit-content { | ||||||
| @ -599,8 +601,8 @@ onMounted(() => { | |||||||
|   margin: 0 0 16px 0; |   margin: 0 0 16px 0; | ||||||
|   font-size: 16px; |   font-size: 16px; | ||||||
|   font-weight: 600; |   font-weight: 600; | ||||||
|   color: #303133; |   color: var(--text-color); | ||||||
|   border-bottom: 2px solid #409eff; |   border-bottom: 2px solid var(--primary-color); | ||||||
|   padding-bottom: 8px; |   padding-bottom: 8px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -9,27 +9,51 @@ | |||||||
|       </template> |       </template> | ||||||
| 
 | 
 | ||||||
|       <el-table :data="users" style="width: 100%"> |       <el-table :data="users" style="width: 100%"> | ||||||
|         <el-table-column prop="username" label="用户名" width="150" align="center" /> |         <el-table-column | ||||||
|         <el-table-column prop="email" label="邮箱" align="center" min-width="200" /> |           prop="username" | ||||||
|  |           label="用户名" | ||||||
|  |           width="150" | ||||||
|  |           align="center" | ||||||
|  |         /> | ||||||
|  |         <el-table-column | ||||||
|  |           prop="email" | ||||||
|  |           label="邮箱" | ||||||
|  |           align="center" | ||||||
|  |           min-width="200" | ||||||
|  |         /> | ||||||
|         <el-table-column prop="role" label="角色" width="120" align="center"> |         <el-table-column prop="role" label="角色" width="120" align="center"> | ||||||
|           <template #default="scope"> |           <template #default="scope"> | ||||||
|             <el-tag :type="scope.row.role === 'admin' ? 'danger' : 'primary'"> |             <el-tag :type="scope.row.role === 'admin' ? 'danger' : 'primary'"> | ||||||
|               {{ scope.row.role === 'admin' ? '管理员' : '普通用户' }} |               {{ scope.row.role === "admin" ? "管理员" : "普通用户" }} | ||||||
|             </el-tag> |             </el-tag> | ||||||
|           </template> |           </template> | ||||||
|         </el-table-column> |         </el-table-column> | ||||||
|         <el-table-column prop="status" label="状态" width="100"> |         <el-table-column prop="status" label="状态" width="100"> | ||||||
|           <template #default="scope"> |           <template #default="scope"> | ||||||
|             <el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'"> |             <el-tag | ||||||
|               {{ scope.row.status === 'active' ? '启用' : '禁用' }} |               :type="scope.row.status === 'active' ? 'success' : 'danger'" | ||||||
|  |             > | ||||||
|  |               {{ scope.row.status === "active" ? "启用" : "禁用" }} | ||||||
|             </el-tag> |             </el-tag> | ||||||
|           </template> |           </template> | ||||||
|         </el-table-column> |         </el-table-column> | ||||||
|         <el-table-column prop="lastLogin" label="最后登录" width="180" align="center" /> |         <el-table-column | ||||||
|  |           prop="lastLogin" | ||||||
|  |           label="最后登录" | ||||||
|  |           width="180" | ||||||
|  |           align="center" | ||||||
|  |         /> | ||||||
|         <el-table-column label="操作" width="240" align="center" fixed="right"> |         <el-table-column label="操作" width="240" align="center" fixed="right"> | ||||||
|           <template #default="scope"> |           <template #default="scope"> | ||||||
|             <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button> |             <el-button size="small" @click="handleEdit(scope.row)" | ||||||
|             <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button> |               >编辑</el-button | ||||||
|  |             > | ||||||
|  |             <el-button | ||||||
|  |               size="small" | ||||||
|  |               type="danger" | ||||||
|  |               @click="handleDelete(scope.row)" | ||||||
|  |               >删除</el-button | ||||||
|  |             > | ||||||
|           </template> |           </template> | ||||||
|         </el-table-column> |         </el-table-column> | ||||||
|       </el-table> |       </el-table> | ||||||
| @ -38,7 +62,7 @@ | |||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { ref } from 'vue'; | import { ref } from "vue"; | ||||||
| 
 | 
 | ||||||
| interface User { | interface User { | ||||||
|   id: number; |   id: number; | ||||||
| @ -52,51 +76,52 @@ interface User { | |||||||
| const users = ref<User[]>([ | const users = ref<User[]>([ | ||||||
|   { |   { | ||||||
|     id: 1, |     id: 1, | ||||||
|     username: 'admin', |     username: "admin", | ||||||
|     email: 'admin@example.com', |     email: "admin@example.com", | ||||||
|     role: 'admin', |     role: "admin", | ||||||
|     status: 'active', |     status: "active", | ||||||
|     lastLogin: '2025-01-20 10:30:00' |     lastLogin: "2025-01-20 10:30:00", | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     id: 2, |     id: 2, | ||||||
|     username: 'user1', |     username: "user1", | ||||||
|     email: 'user1@example.com', |     email: "user1@example.com", | ||||||
|     role: 'user', |     role: "user", | ||||||
|     status: 'active', |     status: "active", | ||||||
|     lastLogin: '2025-01-19 15:45:00' |     lastLogin: "2025-01-19 15:45:00", | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     id: 3, |     id: 3, | ||||||
|     username: 'user2', |     username: "user2", | ||||||
|     email: 'user2@example.com', |     email: "user2@example.com", | ||||||
|     role: 'user', |     role: "user", | ||||||
|     status: 'inactive', |     status: "inactive", | ||||||
|     lastLogin: '2025-01-18 09:20:00' |     lastLogin: "2025-01-18 09:20:00", | ||||||
|   } |   }, | ||||||
| ]); | ]); | ||||||
| 
 | 
 | ||||||
| const handleAddUser = () => { | const handleAddUser = () => { | ||||||
|   console.log('添加用户'); |   console.log("添加用户"); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const handleEdit = (user: User) => { | const handleEdit = (user: User) => { | ||||||
|   console.log('编辑用户:', user); |   console.log("编辑用户:", user); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const handleDelete = (user: User) => { | const handleDelete = (user: User) => { | ||||||
|   console.log('删除用户:', user); |   console.log("删除用户:", user); | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style lang="scss" scoped> | ||||||
| .users-container { |  | ||||||
|   padding: 20px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .card-header { | .card-header { | ||||||
|   display: flex; |   display: flex; | ||||||
|   justify-content: space-between; |   justify-content: space-between; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|  | 
 | ||||||
|  |   span { | ||||||
|  |     font-size: 1.4rem; | ||||||
|  |     font-weight: 700; | ||||||
|  |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user