优化样式,添加明暗主题
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">
|
||||
import { onBeforeMount } from 'vue';
|
||||
import themeManager from '@/utils/theme';
|
||||
|
||||
// 在组件挂载前初始化全局主题系统
|
||||
// 这样确保在首次渲染时就能应用正确的主题,避免闪烁
|
||||
onBeforeMount(() => {
|
||||
// 初始化主题管理器,自动检测系统偏好或读取保存的主题
|
||||
themeManager.init();
|
||||
});
|
||||
|
||||
// 导出主题管理器供全局使用(可选,用于调试)
|
||||
if (import.meta.env.DEV) {
|
||||
(window as any).themeManager = themeManager;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// 主题色变量
|
||||
// 主题色变量(SCSS变量 - 默认为亮色主题)
|
||||
$primary-color: #3498db;
|
||||
$secondary-color: #2ecc71;
|
||||
$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);
|
||||
|
||||
// 主题色快捷类
|
||||
: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-hover: #27ae60;
|
||||
--secondary-active: #229954;
|
||||
--accent-color: #ffcc00;
|
||||
--background-color: #f8f9fa;
|
||||
--text-color: #333;
|
||||
--accent-hover: #e6b800;
|
||||
|
||||
// 背景颜色
|
||||
--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;
|
||||
--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-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;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: $font-family-base;
|
||||
color: $text-color;
|
||||
background: $background-color;
|
||||
font-family: var(--font-family-base);
|
||||
color: var(--text-color);
|
||||
background: var(--background-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
a {
|
||||
color: $primary-color;
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: $primary-color
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.5em 1.2em;
|
||||
border-radius: $border-radius;
|
||||
background: $primary-color;
|
||||
color: #fff;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--primary-color);
|
||||
color: var(--text-inverse);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
transition: var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: $primary-color
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--primary-active);
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: $secondary-color;
|
||||
background: var(--secondary-color);
|
||||
|
||||
&: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;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 10px 0;
|
||||
// margin: 10px 0;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
padding-bottom: 16px;
|
||||
padding-bottom: 20px;
|
||||
|
||||
h2{
|
||||
h2 {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
@ -21,4 +21,9 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
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>
|
||||
|
||||
<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 {
|
||||
background: linear-gradient(135deg, #3b82f6, #0284c7);
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--info-color));
|
||||
padding: 60px 20px;
|
||||
margin-bottom: 30px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--transition-base);
|
||||
|
||||
// 背景装饰
|
||||
&::after {
|
||||
@ -406,18 +389,19 @@ $transition-base: all 0.3s ease;
|
||||
align-items: stretch;
|
||||
max-width: 900px;
|
||||
margin: 0 auto 30px;
|
||||
background: white;
|
||||
border-radius: $radius-md;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
background-color: #f97316;
|
||||
border-color: #f97316;
|
||||
border-radius: 0 $radius-md $radius-md 0;
|
||||
background-color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
|
||||
padding: 0 32px;
|
||||
font-size: 16px;
|
||||
transition: var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background-color: #ea580c;
|
||||
border-color: #ea580c;
|
||||
background-color: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@ -479,24 +464,25 @@ $transition-base: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: $radius-lg;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 20px;
|
||||
box-shadow: $shadow-md;
|
||||
box-shadow: var(--card-shadow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: $transition-base;
|
||||
transition: var(--transition-base);
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: $shadow-lg;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: $radius-md;
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
border-radius: var(--border-radius-lg);
|
||||
background: var(--info-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -504,7 +490,7 @@ $transition-base: all 0.3s ease;
|
||||
|
||||
i {
|
||||
font-size: 24px;
|
||||
color: #409EFF;
|
||||
color: var(--info-color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -513,14 +499,14 @@ $transition-base: all 0.3s ease;
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: $gray-600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: $gray-700;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
@ -536,11 +522,11 @@ $transition-base: all 0.3s ease;
|
||||
}
|
||||
|
||||
&.positive {
|
||||
color: $success-color;
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.negative {
|
||||
color: $danger-color;
|
||||
color: var(--error-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -560,12 +546,17 @@ $transition-base: all 0.3s ease;
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: $gray-700;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: #fff;
|
||||
border-radius: $radius-lg;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: $shadow-md;
|
||||
transition: $transition-base;
|
||||
border: 1px solid $gray-200;
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: var(--transition-base);
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: $shadow-lg;
|
||||
border-color: transparent;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.repo-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid $gray-100;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
|
||||
.repo-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--info-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -608,7 +599,7 @@ $transition-base: all 0.3s ease;
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
color: #409EFF;
|
||||
color: var(--info-color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -618,7 +609,7 @@ $transition-base: all 0.3s ease;
|
||||
.repo-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: $gray-700;
|
||||
color: var(--text-color);
|
||||
margin: 0 0 8px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@ -631,14 +622,14 @@ $transition-base: all 0.3s ease;
|
||||
gap: 8px;
|
||||
|
||||
.private-tag {
|
||||
background-color: #f0f9ff;
|
||||
color: #1677ff;
|
||||
border-color: #91d5ff;
|
||||
background: var(--info-bg);
|
||||
color: var(--info-color);
|
||||
border-color: var(--info-color);
|
||||
}
|
||||
|
||||
.repo-owner {
|
||||
font-size: 12px;
|
||||
color: $gray-600;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
@ -646,7 +637,7 @@ $transition-base: all 0.3s ease;
|
||||
.owner-avatar {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border-radius: var(--border-radius-full);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -658,11 +649,12 @@ $transition-base: all 0.3s ease;
|
||||
|
||||
.repo-description {
|
||||
font-size: 14px;
|
||||
color: $gray-600;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
margin: 0 0 16px 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
height: 42px;
|
||||
@ -673,7 +665,7 @@ $transition-base: all 0.3s ease;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: $gray-500;
|
||||
color: var(--text-tertiary);
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
@ -689,7 +681,7 @@ $transition-base: all 0.3s ease;
|
||||
|
||||
.repo-actions {
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid $gray-100;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@ -702,28 +694,28 @@ $transition-base: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: $radius-sm;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: var(--border-radius);
|
||||
transition: var(--transition-fast);
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $gray-100;
|
||||
background-color: var(--background-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
color: $primary-color;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
color: $success-color;
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
color: $danger-color;
|
||||
color: var(--error-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -732,15 +724,16 @@ $transition-base: all 0.3s ease;
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background-color: #fff;
|
||||
border-radius: $radius-lg;
|
||||
border: 1px dashed $gray-300;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 1px dashed var(--border-color);
|
||||
transition: var(--transition-base);
|
||||
|
||||
.empty-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background-color: $gray-100;
|
||||
border-radius: var(--border-radius-full);
|
||||
background: var(--background-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -748,19 +741,19 @@ $transition-base: all 0.3s ease;
|
||||
|
||||
i {
|
||||
font-size: 32px;
|
||||
color: $gray-400;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
color: $gray-700;
|
||||
color: var(--text-color);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: $gray-500;
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
}
|
||||
@ -791,8 +784,8 @@ $transition-base: all 0.3s ease;
|
||||
.search-select,
|
||||
.search-input {
|
||||
border-radius: 0;
|
||||
border-right: 1px solid $gray-300;
|
||||
border-bottom: 1px solid $gray-300;
|
||||
border-right: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
::v-deep .el-input__inner {
|
||||
border-radius: 0;
|
||||
@ -800,11 +793,11 @@ $transition-base: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-select {
|
||||
border-radius: $radius-md $radius-md 0 0;
|
||||
border-radius: var(--border-radius-lg) var(--border-radius-lg) 0 0;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
border-radius: 0 0 $radius-md $radius-md;
|
||||
border-radius: 0 0 var(--border-radius-lg) var(--border-radius-lg);
|
||||
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">
|
||||
<div class="header-content">
|
||||
<el-button
|
||||
:icon="isCollapse ? Expand : Fold"
|
||||
@click="toggleCollapse"
|
||||
circle
|
||||
size="small"
|
||||
/>
|
||||
<div class="header-left">
|
||||
<el-button
|
||||
:icon="isCollapse ? Expand : Fold"
|
||||
@click="toggleCollapse"
|
||||
circle
|
||||
size="small"
|
||||
/>
|
||||
<button
|
||||
class="menu-toggle"
|
||||
@click="toggleSidebar"
|
||||
@ -39,19 +39,17 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<svg-icon name="bell" />
|
||||
<i class="fas fa-bell"></i>
|
||||
<span class="notification-badge">3</span>
|
||||
</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>
|
||||
@ -70,21 +68,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { Expand, Fold } from "@element-plus/icons-vue";
|
||||
import Sidebar from "./sidebar.vue";
|
||||
import { useTheme } from "@/utils/theme";
|
||||
|
||||
const isCollapse = ref(false);
|
||||
|
||||
const toggleCollapse = () => {
|
||||
isCollapse.value = !isCollapse.value;
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
// 侧边栏切换逻辑(可以根据需要实现)
|
||||
console.log("Toggle sidebar");
|
||||
};
|
||||
|
||||
// 主题切换
|
||||
const theme = useTheme();
|
||||
|
||||
const toggleTheme = () => {
|
||||
theme.toggle();
|
||||
};
|
||||
|
||||
const isDark = computed(() => theme.isDark());
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-container {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--background-color);
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.full-height {
|
||||
@ -92,28 +107,31 @@ const toggleCollapse = () => {
|
||||
}
|
||||
|
||||
.main-sidebar {
|
||||
background-color: #f5f5f5;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
transition: width 0.3s ease;
|
||||
background: var(--sidebar-bg);
|
||||
border-right: 1px solid var(--border-color);
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
background: var(--el-bg-color);
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
background: var(--header-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 0 20px;
|
||||
height: 81px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
background: var(--el-bg-color-page);
|
||||
background: var(--background-color);
|
||||
padding: 0;
|
||||
height: calc(100vh - 60px - 60px); /* 减去 header 和 footer 的高度 */
|
||||
overflow-y: auto;
|
||||
@ -126,8 +144,8 @@ const toggleCollapse = () => {
|
||||
|
||||
.sub-sidebar-wrapper {
|
||||
width: 150px;
|
||||
background-color: #f9f9f9;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
background: var(--background-secondary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@ -135,16 +153,16 @@ const toggleCollapse = () => {
|
||||
flex: 1;
|
||||
/* padding: 20px; */
|
||||
overflow-y: auto;
|
||||
background-color: #ffffff;
|
||||
background: var(--background-color);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--el-text-color-disabled);
|
||||
background: var(--el-bg-color);
|
||||
border-top: 1px solid var(--el-border-color-light);
|
||||
color: var(--text-secondary);
|
||||
background: var(--header-bg);
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 15px 0;
|
||||
height: 81px;
|
||||
}
|
||||
@ -158,11 +176,11 @@ const toggleCollapse = () => {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
// color: $dark-gray;
|
||||
// @include transition(color);
|
||||
color: var(--text-color);
|
||||
transition: var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
// color: $primary-color;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,6 +188,7 @@ const toggleCollapse = () => {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,13 +201,23 @@ const toggleCollapse = () => {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// background-color: $light-gray;
|
||||
border-radius: 6px;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.5rem 1rem;
|
||||
width: 240px;
|
||||
transition: var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-color-hover);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
// color: $medium-gray;
|
||||
color: var(--text-secondary);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
@ -198,9 +227,10 @@ const toggleCollapse = () => {
|
||||
outline: none;
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color);
|
||||
|
||||
&::placeholder {
|
||||
// color: $medium-gray;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -213,28 +243,42 @@ const toggleCollapse = () => {
|
||||
.action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
// color: $dark-gray;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
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 {
|
||||
// color: $primary-color;
|
||||
background: var(--background-hover);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
&.theme-toggle-btn {
|
||||
i {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
// background-color: $danger-color;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
background: var(--error-color);
|
||||
color: var(--text-inverse);
|
||||
border-radius: var(--border-radius-full);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 0.7rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,8 +288,14 @@ const toggleCollapse = () => {
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border-radius: var(--border-radius-full);
|
||||
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
|
||||
:default-active="activeMenu"
|
||||
class="sidebar-menu"
|
||||
background-color="#f5f5f5"
|
||||
text-color="#333"
|
||||
active-text-color="#409eff"
|
||||
background-color="var(--sidebar-bg)"
|
||||
text-color="var(--text-color)"
|
||||
active-text-color="var(--primary-color)"
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:router="false"
|
||||
@ -270,13 +270,16 @@ onMounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--sidebar-bg);
|
||||
transition: var(--transition-base);
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: var(--transition-base);
|
||||
|
||||
.logo-img {
|
||||
height: 40px;
|
||||
@ -285,7 +288,7 @@ onMounted(() => {
|
||||
.logo-text {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -296,20 +299,26 @@ onMounted(() => {
|
||||
|
||||
.bottom-settings {
|
||||
// margin-bottom: 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.user-dropdown-trigger {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border-radius: var(--border-radius-full);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
transition: var(--transition-base);
|
||||
margin: 20px 0;
|
||||
border: 2px solid var(--border-color);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
border-radius: var(--border-radius-full);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -334,26 +343,43 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// 菜单项样式 - 统一管理
|
||||
.sidebar-menu :deep(.el-menu-item) {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.sidebar-menu :deep(.el-menu-item) i {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
transition: var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-hover) !important;
|
||||
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) {
|
||||
background-color: #e6f0ff !important;
|
||||
background-color: var(--background-hover) !important;
|
||||
color: var(--primary-color) !important;
|
||||
|
||||
i {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
}
|
||||
.user-nickname {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
padding: 20px 40px;
|
||||
}
|
||||
@ -361,11 +387,11 @@ onMounted(() => {
|
||||
.debug-info {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
background: #f0f0f0;
|
||||
border: 1px dashed #ccc;
|
||||
background: var(--warning-bg);
|
||||
border: 1px dashed var(--warning-color);
|
||||
margin: 10px;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
<el-menu
|
||||
:default-active="activeSubMenu"
|
||||
class="sub-sidebar-menu"
|
||||
background-color="#fafafa"
|
||||
text-color="#666"
|
||||
active-text-color="#409eff"
|
||||
background-color="var(--sidebar-bg)"
|
||||
text-color="var(--text-secondary)"
|
||||
active-text-color="var(--primary-color)"
|
||||
:router="false"
|
||||
>
|
||||
<el-menu-item
|
||||
@ -84,46 +84,47 @@ const handleSubMenuClick = (item: SubMenuItem) => {
|
||||
.sub-sidebar-container {
|
||||
width: 150px;
|
||||
height: 100%;
|
||||
background: #fafafa;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
background: var(--sidebar-bg);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.back-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
transition: var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
background-color: var(--background-hover);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
color: #666;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.back-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.module-title {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,28 +136,46 @@ const handleSubMenuClick = (item: SubMenuItem) => {
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
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 {
|
||||
background-color: #f0f0f0;
|
||||
background-color: var(--background-hover) !important;
|
||||
color: var(--primary-color) !important;
|
||||
|
||||
i {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: #e6f0ff;
|
||||
color: #409eff;
|
||||
background-color: var(--background-hover) !important;
|
||||
color: var(--primary-color) !important;
|
||||
|
||||
i {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-description {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
background-color: #f5f5f5;
|
||||
border-top: 1px solid var(--border-color);
|
||||
background-color: var(--background-secondary);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
@ -409,11 +409,12 @@ onActivated(async () => {
|
||||
.universal-sub-menu {
|
||||
display: flex;
|
||||
/* height: 100%; */
|
||||
background: #f5f5f5;
|
||||
background: var(--background-secondary);
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.sub-sidebar-wrapper {
|
||||
border-right: 1px solid #e4e7ed;
|
||||
border-right: 1px solid var(--border-color);
|
||||
overflow-y: none;
|
||||
}
|
||||
|
||||
@ -422,6 +423,7 @@ onActivated(async () => {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 162px);
|
||||
background: var(--background-color);
|
||||
}
|
||||
|
||||
/* 加载状态样式 */
|
||||
@ -432,17 +434,18 @@ onActivated(async () => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--card-shadow);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 2px solid rgba(64, 158, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
border-top-color: #409eff;
|
||||
border: 2px solid var(--info-bg);
|
||||
border-radius: var(--border-radius-full);
|
||||
border-top-color: var(--info-color);
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@ -455,7 +458,7 @@ onActivated(async () => {
|
||||
.loading-state p,
|
||||
.error-state p {
|
||||
margin-top: 12px;
|
||||
color: #606266;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@ -465,11 +468,11 @@ onActivated(async () => {
|
||||
|
||||
.error-icon i {
|
||||
font-size: 48px;
|
||||
color: #f56c6c;
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.error-state h3 {
|
||||
color: #f56c6c;
|
||||
color: var(--error-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@ -490,15 +493,15 @@ onActivated(async () => {
|
||||
.component-loading .loading-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 2px solid rgba(64, 158, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
border-top-color: #409eff;
|
||||
border: 2px solid var(--info-bg);
|
||||
border-radius: var(--border-radius-full);
|
||||
border-top-color: var(--info-color);
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.component-loading p {
|
||||
margin-top: 12px;
|
||||
color: #606266;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@ -512,10 +515,11 @@ onActivated(async () => {
|
||||
.development-component {
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e4e7ed;
|
||||
background: var(--background-tertiary);
|
||||
border-radius: var(--border-radius-xl);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.development-icon {
|
||||
@ -524,46 +528,47 @@ onActivated(async () => {
|
||||
|
||||
.development-icon i {
|
||||
font-size: 64px;
|
||||
color: #409eff;
|
||||
color: var(--info-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.development-component h3 {
|
||||
color: #303133;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 16px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.development-path {
|
||||
color: #606266;
|
||||
color: var(--text-secondary);
|
||||
margin: 12px 0;
|
||||
font-size: 16px;
|
||||
font-family: 'Courier New', monospace;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
background: var(--info-bg);
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--border-radius);
|
||||
display: inline-block;
|
||||
border: 1px solid var(--info-color);
|
||||
}
|
||||
|
||||
.development-desc {
|
||||
color: #909399;
|
||||
color: var(--text-tertiary);
|
||||
margin: 16px 0 24px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.development-tips {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
background: var(--info-bg);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #409eff;
|
||||
border-radius: var(--border-radius-lg);
|
||||
border-left: 4px solid var(--info-color);
|
||||
text-align: left;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.development-tips p {
|
||||
color: #606266;
|
||||
color: var(--text-secondary);
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
@ -572,11 +577,11 @@ onActivated(async () => {
|
||||
.debug-info {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
background: #f0f0f0;
|
||||
border: 1px dashed #ccc;
|
||||
background: var(--warning-bg);
|
||||
border: 1px dashed var(--warning-color);
|
||||
margin: 10px;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -393,6 +393,7 @@ $warning-color: #f59e0b;
|
||||
$danger-color: #ef4444;
|
||||
$purple-color: #8b5cf6;
|
||||
|
||||
|
||||
// 工具类
|
||||
@mixin transition($property: all, $duration: 0.3s, $easing: ease) {
|
||||
transition: $property $duration $easing;
|
||||
@ -404,14 +405,15 @@ $purple-color: #8b5cf6;
|
||||
"medium": 0 4px 6px rgba(0, 0, 0, 0.05),
|
||||
"large": 0 10px 15px rgba(0, 0, 0, 0.07),
|
||||
);
|
||||
box-shadow: map-get($shadows, $size);
|
||||
// box-shadow: map-get($shadows, $size);
|
||||
}
|
||||
|
||||
// 主容器
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background-color: #f9fafb;
|
||||
background: var(--background-color);
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
// 侧边栏
|
||||
@ -570,8 +572,8 @@ $purple-color: #8b5cf6;
|
||||
|
||||
.main-header {
|
||||
height: $header-height;
|
||||
background-color: white;
|
||||
@include shadow;
|
||||
background: var(--header-bg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 0 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -589,11 +591,11 @@ $purple-color: #8b5cf6;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: $dark-gray;
|
||||
color: var(--text-color);
|
||||
@include transition(color);
|
||||
|
||||
&:hover {
|
||||
color: $primary-color;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -613,13 +615,19 @@ $purple-color: #8b5cf6;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: $light-gray;
|
||||
border-radius: 6px;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.5rem 1rem;
|
||||
width: 240px;
|
||||
transition: var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-color-hover);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: $medium-gray;
|
||||
color: var(--text-secondary);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
@ -629,9 +637,10 @@ $purple-color: #8b5cf6;
|
||||
outline: none;
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color);
|
||||
|
||||
&::placeholder {
|
||||
color: $medium-gray;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -644,22 +653,22 @@ $purple-color: #8b5cf6;
|
||||
.action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $dark-gray;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
@include transition(color);
|
||||
transition: var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
color: $primary-color;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background-color: $danger-color;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
background: var(--error-color);
|
||||
color: var(--text-inverse);
|
||||
border-radius: var(--border-radius-full);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 0.7rem;
|
||||
@ -693,23 +702,25 @@ $purple-color: #8b5cf6;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.stat-card {
|
||||
background-color: white;
|
||||
@include shadow;
|
||||
border-radius: 8px;
|
||||
background: var(--card-bg);
|
||||
box-shadow: var(--card-shadow);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: 4px solid $secondary-color;
|
||||
@include transition(transform, 0.3s);
|
||||
border-top: 4px solid var(--primary-color);
|
||||
transition: var(--transition-base);
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
.stat-label {
|
||||
color: $medium-gray;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
@ -718,6 +729,7 @@ $purple-color: #8b5cf6;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.25rem 0;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
@ -728,28 +740,28 @@ $purple-color: #8b5cf6;
|
||||
gap: 0.5rem;
|
||||
|
||||
.change-period {
|
||||
color: $medium-gray;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: $success-color;
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: $danger-color;
|
||||
color: var(--error-color);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background-color: rgba($secondary-color, 0.1);
|
||||
border-radius: var(--border-radius-lg);
|
||||
background: var(--info-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $secondary-color;
|
||||
color: var(--info-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -761,22 +773,30 @@ $purple-color: #8b5cf6;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.chart-card {
|
||||
background-color: white;
|
||||
@include shadow;
|
||||
border-radius: 8px;
|
||||
background: var(--card-bg);
|
||||
box-shadow: var(--card-shadow);
|
||||
border-radius: var(--border-radius-lg);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-color);
|
||||
transition: var(--transition-base);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid $light-gray;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--header-bg);
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.chart-filters {
|
||||
@ -785,22 +805,24 @@ $purple-color: #8b5cf6;
|
||||
|
||||
.filter-btn {
|
||||
background: none;
|
||||
border: 1px solid $light-gray;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
@include transition(all);
|
||||
color: var(--text-color);
|
||||
transition: var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: $secondary-color;
|
||||
color: $secondary-color;
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
background: var(--background-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $secondary-color;
|
||||
color: white;
|
||||
border-color: $secondary-color;
|
||||
background: var(--primary-color);
|
||||
color: var(--text-inverse);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -809,6 +831,7 @@ $purple-color: #8b5cf6;
|
||||
.chart-container {
|
||||
padding: 1.5rem;
|
||||
width: 100%;
|
||||
background: var(--card-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -820,34 +843,42 @@ $purple-color: #8b5cf6;
|
||||
|
||||
.tasks-card,
|
||||
.activities-card {
|
||||
background-color: white;
|
||||
@include shadow;
|
||||
border-radius: 8px;
|
||||
background: var(--card-bg);
|
||||
box-shadow: var(--card-shadow);
|
||||
border-radius: var(--border-radius-lg);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-color);
|
||||
transition: var(--transition-base);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid $light-gray;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--header-bg);
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.view-all {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $secondary-color;
|
||||
color: var(--primary-color);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
@include transition(color);
|
||||
transition: var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
color: $primary-color;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -861,15 +892,16 @@ $purple-color: #8b5cf6;
|
||||
.task-item,
|
||||
.activity-item {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid $light-gray;
|
||||
@include transition(background-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--card-bg);
|
||||
transition: var(--transition-fast);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $light-gray;
|
||||
background: var(--background-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -894,10 +926,10 @@ $purple-color: #8b5cf6;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
border: 2px solid $medium-gray;
|
||||
border-radius: var(--border-radius);
|
||||
border: 2px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
@include transition(all);
|
||||
transition: var(--transition-fast);
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
@ -907,15 +939,15 @@ $purple-color: #8b5cf6;
|
||||
top: 2px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border: solid var(--text-inverse);
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
input:checked + label {
|
||||
background-color: $success-color;
|
||||
border-color: $success-color;
|
||||
background: var(--success-color);
|
||||
border-color: var(--success-color);
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
@ -929,40 +961,41 @@ $purple-color: #8b5cf6;
|
||||
.task-title {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 0.875rem;
|
||||
@include transition(text-decoration, color);
|
||||
color: var(--text-color);
|
||||
transition: var(--transition-base);
|
||||
|
||||
&.completed {
|
||||
text-decoration: line-through;
|
||||
color: $medium-gray;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.task-date {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
color: $medium-gray;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.task-priority {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--border-radius);
|
||||
font-weight: 500;
|
||||
|
||||
&.high {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: $danger-color;
|
||||
background: var(--error-bg);
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
&.medium {
|
||||
background-color: rgba(245, 158, 11, 0.1);
|
||||
color: $warning-color;
|
||||
background: var(--warning-bg);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
&.low {
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
color: $success-color;
|
||||
background: var(--success-bg);
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -998,7 +1031,7 @@ $purple-color: #8b5cf6;
|
||||
.activity-time {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
color: $medium-gray;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,10 +2,12 @@
|
||||
<el-card class="box-card">
|
||||
<div class="header-bar">
|
||||
<h2>角色管理</h2>
|
||||
<el-button type="primary" @click="showRoleDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加角色
|
||||
</el-button>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="showRoleDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加角色
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
@ -226,15 +228,6 @@ onMounted(() => {
|
||||
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 {
|
||||
font-size: 1.18rem;
|
||||
font-weight: 600;
|
||||
|
||||
@ -526,15 +526,17 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.tenant-management-module {
|
||||
padding: 32px 30px 24px 20px;
|
||||
padding: 20px;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
@ -551,9 +553,9 @@ onMounted(() => {
|
||||
.loading-spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 4px solid #e5e5e5;
|
||||
border-left-color: #409eff;
|
||||
border-radius: 50%;
|
||||
border: 4px solid var(--border-color);
|
||||
border-left-color: var(--primary-color);
|
||||
border-radius: var(--border-radius-full);
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@ -578,11 +580,11 @@ onMounted(() => {
|
||||
|
||||
.tenant-detail .el-descriptions-item__label {
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.tenant-detail .el-descriptions-item__content {
|
||||
color: #303133;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.audit-content {
|
||||
@ -599,8 +601,8 @@ onMounted(() => {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
border-bottom: 2px solid #409eff;
|
||||
color: var(--text-color);
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
|
||||
@ -7,29 +7,53 @@
|
||||
<el-button type="primary" @click="handleAddUser">添加用户</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<el-table :data="users" style="width: 100%">
|
||||
<el-table-column prop="username" label="用户名" width="150" align="center" />
|
||||
<el-table-column prop="email" label="邮箱" align="center" min-width="200" />
|
||||
<el-table-column
|
||||
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">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.role === 'admin' ? 'danger' : 'primary'">
|
||||
{{ scope.row.role === 'admin' ? '管理员' : '普通用户' }}
|
||||
{{ scope.row.role === "admin" ? "管理员" : "普通用户" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
|
||||
<el-tag
|
||||
:type="scope.row.status === 'active' ? 'success' : 'danger'"
|
||||
>
|
||||
{{ scope.row.status === "active" ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</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">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
<el-button size="small" @click="handleEdit(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -38,7 +62,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ref } from "vue";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
@ -52,51 +76,52 @@ interface User {
|
||||
const users = ref<User[]>([
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
role: 'admin',
|
||||
status: 'active',
|
||||
lastLogin: '2025-01-20 10:30:00'
|
||||
username: "admin",
|
||||
email: "admin@example.com",
|
||||
role: "admin",
|
||||
status: "active",
|
||||
lastLogin: "2025-01-20 10:30:00",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'user1',
|
||||
email: 'user1@example.com',
|
||||
role: 'user',
|
||||
status: 'active',
|
||||
lastLogin: '2025-01-19 15:45:00'
|
||||
username: "user1",
|
||||
email: "user1@example.com",
|
||||
role: "user",
|
||||
status: "active",
|
||||
lastLogin: "2025-01-19 15:45:00",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'user2',
|
||||
email: 'user2@example.com',
|
||||
role: 'user',
|
||||
status: 'inactive',
|
||||
lastLogin: '2025-01-18 09:20:00'
|
||||
}
|
||||
username: "user2",
|
||||
email: "user2@example.com",
|
||||
role: "user",
|
||||
status: "inactive",
|
||||
lastLogin: "2025-01-18 09:20:00",
|
||||
},
|
||||
]);
|
||||
|
||||
const handleAddUser = () => {
|
||||
console.log('添加用户');
|
||||
console.log("添加用户");
|
||||
};
|
||||
|
||||
const handleEdit = (user: User) => {
|
||||
console.log('编辑用户:', user);
|
||||
console.log("编辑用户:", user);
|
||||
};
|
||||
|
||||
const handleDelete = (user: User) => {
|
||||
console.log('删除用户:', user);
|
||||
console.log("删除用户:", user);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.users-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user