优化样式,添加明暗主题

This commit is contained in:
李志强 2025-10-28 10:59:23 +08:00
parent d1ae365964
commit 6cd942fb29
17 changed files with 1782 additions and 523 deletions

414
front/THEME_GUIDE.md Normal file
View 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` - 组件示例

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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;
} }
@ -21,4 +21,9 @@
display: flex; display: flex;
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;
}

View 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>
```

View 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
View 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;

View File

@ -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;
} }
} }

View File

@ -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>

View File

@ -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);
}
} }
} }
} }

View File

@ -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>

View File

@ -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;
} }
} }

View File

@ -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>

View File

@ -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);
} }
} }
} }

View File

@ -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;

View File

@ -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;
} }

View File

@ -7,29 +7,53 @@
<el-button type="primary" @click="handleAddUser">添加用户</el-button> <el-button type="primary" @click="handleAddUser">添加用户</el-button>
</div> </div>
</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>