优化样式,添加明暗主题

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">
import { onBeforeMount } from 'vue';
import themeManager from '@/utils/theme';
//
//
onBeforeMount(() => {
//
themeManager.init();
});
// 使
if (import.meta.env.DEV) {
(window as any).themeManager = themeManager;
}
</script>
<template>

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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