소스 검색

feat: implement theme customization system with dark mode support

- Add ThemeSettings and ThemeSwitch components for user theme preferences
- Introduce a theme store to manage theme configurations, including color schemes and font sizes
- Implement CSS variables for dynamic theming and Element Plus style overrides
- Enhance layout with responsive theme switching and improved styling for dark mode
- Create a new theme system with presets for various color themes
yb 2 주 전
부모
커밋
63174b753a

+ 137 - 13
src/assets/styles/index.scss

@@ -1,5 +1,8 @@
 @use './variables.scss' as *;
 
+// 导入主题系统
+@import './theme/index.scss';
+
 * {
   margin: 0;
   padding: 0;
@@ -11,15 +14,21 @@ body,
 #app {
   width: 100%;
   height: 100%;
-  font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
-  font-size: 14px;
-  color: $text-color;
-  background-color: $bg-color;
+  font-family: var(--font-family);
+  font-size: var(--font-size-base);
+  color: var(--text-primary);
+  background-color: var(--bg-page);
+  transition: background-color var(--transition-base) var(--transition-timing),
+    color var(--transition-base) var(--transition-timing);
 }
 
 a {
   text-decoration: none;
   color: inherit;
+
+  &:hover {
+    color: var(--color-primary);
+  }
 }
 
 ul,
@@ -34,12 +43,17 @@ ol {
 }
 
 ::-webkit-scrollbar-thumb {
-  background-color: #c1c1c1;
-  border-radius: 3px;
+  background-color: var(--scrollbar-thumb);
+  border-radius: var(--radius-full);
+  transition: background-color var(--transition-base);
+
+  &:hover {
+    background-color: var(--scrollbar-thumb-hover);
+  }
 }
 
 ::-webkit-scrollbar-track {
-  background-color: #f1f1f1;
+  background-color: var(--scrollbar-track);
 }
 
 // 通用类
@@ -59,12 +73,93 @@ ol {
   justify-content: space-between;
 }
 
+.flex-col {
+  display: flex;
+  flex-direction: column;
+}
+
+.flex-1 {
+  flex: 1;
+}
+
+.flex-wrap {
+  flex-wrap: wrap;
+}
+
+.items-center {
+  align-items: center;
+}
+
+.justify-center {
+  justify-content: center;
+}
+
+.justify-between {
+  justify-content: space-between;
+}
+
+.justify-end {
+  justify-content: flex-end;
+}
+
+.gap-sm {
+  gap: var(--spacing-sm);
+}
+
+.gap-md {
+  gap: var(--spacing-md);
+}
+
+.gap-lg {
+  gap: var(--spacing-lg);
+}
+
 .text-ellipsis {
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
 
+.text-center {
+  text-align: center;
+}
+
+.text-right {
+  text-align: right;
+}
+
+.text-primary {
+  color: var(--text-primary);
+}
+
+.text-regular {
+  color: var(--text-regular);
+}
+
+.text-secondary {
+  color: var(--text-secondary);
+}
+
+.text-success {
+  color: var(--color-success);
+}
+
+.text-warning {
+  color: var(--color-warning);
+}
+
+.text-danger {
+  color: var(--color-danger);
+}
+
+.font-medium {
+  font-weight: var(--font-weight-medium);
+}
+
+.font-semibold {
+  font-weight: var(--font-weight-semibold);
+}
+
 .clearfix::after {
   content: '';
   display: table;
@@ -73,18 +168,47 @@ ol {
 
 // 页面容器
 .page-container {
-  //padding: 20px;
-  background-color: #fff;
-  border-radius: 4px;
+  padding: var(--content-padding);
+  background-color: var(--bg-container);
+  border-radius: var(--radius-lg);
+  box-shadow: var(--shadow-sm);
 }
 
 // 搜索表单
 .search-form {
-  background-color: #fff;
-  border-radius: 4px;
+  padding: var(--spacing-base);
+  background-color: var(--bg-container);
+  border-radius: var(--radius-base);
+  margin-bottom: var(--spacing-base);
 }
 
 // 表格操作按钮
 .table-actions {
-  margin-bottom: 15px;
+  margin-bottom: var(--spacing-base);
+  display: flex;
+  align-items: center;
+  gap: var(--spacing-sm);
+}
+
+// 分页
+.pagination {
+  margin-top: var(--spacing-lg);
+  display: flex;
+  justify-content: flex-end;
+}
+
+// 卡片
+.card {
+  background-color: var(--bg-container);
+  border-radius: var(--radius-lg);
+  box-shadow: var(--shadow-base);
+  padding: var(--spacing-lg);
+}
+
+// 主题切换过渡
+.theme-transition {
+  transition: background-color var(--transition-base) var(--transition-timing),
+    border-color var(--transition-base) var(--transition-timing),
+    color var(--transition-base) var(--transition-timing),
+    box-shadow var(--transition-base) var(--transition-timing);
 }

+ 192 - 0
src/assets/styles/theme/css-variables.scss

@@ -0,0 +1,192 @@
+/**
+ * CSS 变量定义 - 现代风格
+ */
+
+:root {
+  // ========== 主题色(默认 Indigo)==========
+  --color-primary: #6366f1;
+  --color-primary-light-3: #818cf8;
+  --color-primary-light-5: #a5b4fc;
+  --color-primary-light-7: #c7d2fe;
+  --color-primary-light-9: #e0e7ff;
+  --color-primary-dark-2: #4f46e5;
+
+  // 语义色
+  --color-success: #10b981;
+  --color-success-light: #34d399;
+  --color-warning: #f59e0b;
+  --color-warning-light: #fbbf24;
+  --color-danger: #ef4444;
+  --color-danger-light: #f87171;
+  --color-info: #6b7280;
+  --color-info-light: #9ca3af;
+
+  // ========== 背景色(浅色模式)==========
+  --bg-page: #f8fafc;
+  --bg-container: #ffffff;
+  --bg-header: #ffffff;
+  --bg-sidebar: #1e293b;
+  --bg-sidebar-logo: #0f172a;
+  --bg-hover: #f1f5f9;
+  --bg-active: var(--color-primary-light-9);
+  --bg-mask: rgba(0, 0, 0, 0.5);
+  --bg-tooltip: #1e293b;
+
+  // ========== 文字颜色 ==========
+  --text-primary: #0f172a;
+  --text-regular: #334155;
+  --text-secondary: #64748b;
+  --text-placeholder: #94a3b8;
+  --text-disabled: #cbd5e1;
+  --text-inverse: #ffffff;
+  --text-link: var(--color-primary);
+  --text-link-hover: var(--color-primary-dark-2);
+
+  // ========== 边框颜色 ==========
+  --border-color: #e2e8f0;
+  --border-color-light: #f1f5f9;
+  --border-color-lighter: #f8fafc;
+  --border-color-dark: #cbd5e1;
+
+  // ========== 阴影 ==========
+  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  --shadow-base: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
+  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
+  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
+  --shadow-header: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
+
+  // ========== 布局尺寸 ==========
+  --sidebar-width: 220px;
+  --sidebar-width-collapsed: 64px;
+  --header-height: 60px;
+  --content-padding: 20px;
+
+  // ========== 圆角 ==========
+  --radius-sm: 4px;
+  --radius-base: 8px;
+  --radius-lg: 12px;
+  --radius-xl: 16px;
+  --radius-full: 9999px;
+
+  // ========== 字体 ==========
+  --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
+    'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
+  --font-size-xs: 12px;
+  --font-size-sm: 13px;
+  --font-size-base: 14px;
+  --font-size-lg: 16px;
+  --font-size-xl: 18px;
+  --font-size-2xl: 20px;
+  --font-weight-normal: 400;
+  --font-weight-medium: 500;
+  --font-weight-semibold: 600;
+  --font-weight-bold: 700;
+  --line-height-tight: 1.25;
+  --line-height-base: 1.5;
+  --line-height-relaxed: 1.75;
+
+  // ========== 间距 ==========
+  --spacing-xs: 4px;
+  --spacing-sm: 8px;
+  --spacing-md: 12px;
+  --spacing-base: 16px;
+  --spacing-lg: 20px;
+  --spacing-xl: 24px;
+  --spacing-2xl: 32px;
+
+  // ========== 过渡动画 ==========
+  --transition-fast: 150ms;
+  --transition-base: 200ms;
+  --transition-slow: 300ms;
+  --transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
+
+  // ========== 侧边栏专用 ==========
+  --sidebar-text: #94a3b8;
+  --sidebar-text-active: #ffffff;
+  --sidebar-hover-bg: rgba(255, 255, 255, 0.05);
+  --sidebar-active-bg: var(--color-primary);
+
+  // ========== 滚动条 ==========
+  --scrollbar-thumb: #cbd5e1;
+  --scrollbar-track: #f1f5f9;
+  --scrollbar-thumb-hover: #94a3b8;
+}
+
+// ==========================================
+// 深色模式
+// ==========================================
+html.dark {
+  // ========== 主题色(深色模式稍亮)==========
+  --color-primary: #818cf8;
+  --color-primary-light-3: #6366f1;
+  --color-primary-light-5: #4f46e5;
+  --color-primary-light-7: #4338ca;
+  --color-primary-light-9: #3730a3;
+  --color-primary-dark-2: #a5b4fc;
+
+  // 语义色
+  --color-success: #34d399;
+  --color-success-light: #10b981;
+  --color-warning: #fbbf24;
+  --color-warning-light: #f59e0b;
+  --color-danger: #f87171;
+  --color-danger-light: #ef4444;
+  --color-info: #9ca3af;
+  --color-info-light: #6b7280;
+
+  // ========== 背景色(深色模式)==========
+  --bg-page: #09090b;
+  --bg-container: #18181b;
+  --bg-header: #18181b;
+  --bg-sidebar: #18181b;
+  --bg-sidebar-logo: #09090b;
+  --bg-hover: #27272a;
+  --bg-active: rgba(99, 102, 241, 0.15);
+  --bg-mask: rgba(0, 0, 0, 0.7);
+  --bg-tooltip: #f4f4f5;
+
+  // ========== 文字颜色 ==========
+  --text-primary: #fafafa;
+  --text-regular: #e4e4e7;
+  --text-secondary: #a1a1aa;
+  --text-placeholder: #71717a;
+  --text-disabled: #52525b;
+  --text-inverse: #ffffff;
+  --text-link: var(--color-primary);
+  --text-link-hover: var(--color-primary-dark-2);
+
+  // ========== 边框颜色 ==========
+  --border-color: #27272a;
+  --border-color-light: #3f3f46;
+  --border-color-lighter: #52525b;
+  --border-color-dark: #18181b;
+
+  // ========== 阴影 ==========
+  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
+  --shadow-base: 0 1px 3px 0 rgba(0, 0, 0, 0.4), 0 1px 2px -1px rgba(0, 0, 0, 0.4);
+  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.4);
+  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
+  --shadow-header: 0 1px 3px 0 rgba(0, 0, 0, 0.3);
+
+  // ========== 侧边栏专用 ==========
+  --sidebar-text: #a1a1aa;
+  --sidebar-text-active: #ffffff;
+  --sidebar-hover-bg: rgba(255, 255, 255, 0.08);
+  --sidebar-active-bg: var(--color-primary);
+
+  // ========== 滚动条 ==========
+  --scrollbar-thumb: #3f3f46;
+  --scrollbar-track: #27272a;
+  --scrollbar-thumb-hover: #52525b;
+}
+
+// ==========================================
+// 紧凑模式
+// ==========================================
+.compact-mode {
+  --font-size-base: 13px;
+  --spacing-base: 12px;
+  --spacing-lg: 16px;
+  --content-padding: 16px;
+  --header-height: 52px;
+}

+ 258 - 0
src/assets/styles/theme/element-override.scss

@@ -0,0 +1,258 @@
+/**
+ * Element Plus 样式覆盖
+ * 遵循 Element Plus 官方主题定制方式
+ * 深色模式使用 html.dark 类名
+ */
+
+// 全局主题色覆盖 (确保自定义主题色应用到 Element Plus)
+:root {
+  --el-color-primary: var(--color-primary);
+  --el-color-primary-light-3: var(--color-primary-light-3);
+  --el-color-primary-light-5: var(--color-primary-light-5);
+  --el-color-primary-light-7: var(--color-primary-light-7);
+  --el-color-primary-light-8: var(--color-primary-light-7);
+  --el-color-primary-light-9: var(--color-primary-light-9);
+  --el-color-primary-dark-2: var(--color-primary-dark-2);
+}
+
+// 按钮
+.el-button--primary {
+  --el-button-bg-color: var(--color-primary);
+  --el-button-border-color: var(--color-primary);
+  --el-button-hover-bg-color: var(--color-primary-light-3);
+  --el-button-hover-border-color: var(--color-primary-light-3);
+  --el-button-active-bg-color: var(--color-primary-dark-2);
+  --el-button-active-border-color: var(--color-primary-dark-2);
+}
+
+// 链接
+.el-link--primary {
+  --el-link-text-color: var(--color-primary);
+  --el-link-hover-text-color: var(--color-primary-light-3);
+}
+
+// 输入框
+.el-input {
+  --el-input-border-radius: var(--radius-base);
+}
+
+// 选择框
+.el-select {
+  --el-select-border-radius: var(--radius-base);
+}
+
+// 卡片
+.el-card {
+  --el-card-border-radius: var(--radius-lg);
+  border: 1px solid var(--border-color);
+  background-color: var(--bg-container);
+}
+
+// 表格
+.el-table {
+  --el-table-border-color: var(--border-color);
+  --el-table-header-bg-color: var(--bg-hover);
+  --el-table-row-hover-bg-color: var(--bg-hover);
+  --el-table-bg-color: var(--bg-container);
+  --el-table-tr-bg-color: var(--bg-container);
+  --el-table-text-color: var(--text-primary);
+  --el-table-header-text-color: var(--text-primary);
+
+  border-radius: var(--radius-base);
+  overflow: hidden;
+
+  th.el-table__cell {
+    font-weight: var(--font-weight-semibold);
+  }
+}
+
+// 分页
+.el-pagination {
+  --el-pagination-button-bg-color: var(--bg-container);
+  --el-pagination-hover-color: var(--color-primary);
+
+  .el-pager li {
+    border-radius: var(--radius-sm);
+  }
+}
+
+// 对话框
+.el-dialog {
+  --el-dialog-border-radius: var(--radius-lg);
+  --el-dialog-bg-color: var(--bg-container);
+}
+
+// 抽屉
+.el-drawer {
+  --el-drawer-bg-color: var(--bg-container);
+}
+
+// 消息
+.el-message {
+  --el-message-border-radius: var(--radius-base);
+}
+
+// 标签
+.el-tag {
+  --el-tag-border-radius: var(--radius-sm);
+}
+
+// 下拉菜单
+.el-dropdown-menu {
+  --el-dropdown-menu-border-radius: var(--radius-base);
+  border: 1px solid var(--border-color);
+}
+
+// 菜单
+.el-menu {
+  --el-menu-border-color: transparent;
+}
+
+// 表单
+.el-form-item__label {
+  color: var(--text-regular);
+  font-weight: var(--font-weight-medium);
+}
+
+// 深色模式下的特殊处理
+html.dark {
+  // Element Plus 核心变量覆盖
+  --el-color-primary: var(--color-primary);
+  --el-color-primary-light-3: var(--color-primary-light-3);
+  --el-color-primary-light-5: var(--color-primary-light-5);
+  --el-color-primary-light-7: var(--color-primary-light-7);
+  --el-color-primary-light-8: var(--color-primary-light-7);
+  --el-color-primary-light-9: var(--color-primary-light-9);
+  --el-color-primary-dark-2: var(--color-primary-dark-2);
+
+  --el-bg-color: var(--bg-container);
+  --el-bg-color-page: var(--bg-page);
+  --el-bg-color-overlay: var(--bg-container);
+
+  --el-text-color-primary: var(--text-primary);
+  --el-text-color-regular: var(--text-regular);
+  --el-text-color-secondary: var(--text-secondary);
+  --el-text-color-placeholder: var(--text-placeholder);
+  --el-text-color-disabled: var(--text-disabled);
+
+  --el-border-color: var(--border-color);
+  --el-border-color-light: var(--border-color-light);
+  --el-border-color-lighter: var(--border-color-lighter);
+  --el-border-color-dark: var(--border-color-dark);
+
+  --el-fill-color: var(--bg-hover);
+  --el-fill-color-light: var(--bg-hover);
+  --el-fill-color-lighter: var(--bg-page);
+  --el-fill-color-blank: var(--bg-container);
+
+  // 表格深色模式
+  .el-table {
+    --el-table-bg-color: var(--bg-container);
+    --el-table-tr-bg-color: var(--bg-container);
+    --el-table-header-bg-color: var(--bg-hover);
+    --el-table-row-hover-bg-color: var(--bg-hover);
+    --el-table-border-color: var(--border-color);
+    --el-table-text-color: var(--text-primary);
+    --el-table-header-text-color: var(--text-primary);
+    --el-table-current-row-bg-color: var(--bg-active);
+
+    background-color: var(--bg-container) !important;
+
+    th.el-table__cell,
+    td.el-table__cell {
+      background-color: transparent !important;
+    }
+
+    .el-table__inner-wrapper::before {
+      background-color: var(--border-color);
+    }
+  }
+
+  // 输入框
+  .el-input__wrapper {
+    background-color: var(--bg-container);
+    box-shadow: 0 0 0 1px var(--border-color) inset;
+
+    &:hover {
+      box-shadow: 0 0 0 1px var(--border-color-light) inset;
+    }
+
+    &.is-focus {
+      box-shadow: 0 0 0 1px var(--color-primary) inset;
+    }
+  }
+
+  // 选择框
+  .el-select__wrapper {
+    background-color: var(--bg-container);
+    box-shadow: 0 0 0 1px var(--border-color) inset;
+  }
+
+  // 日期选择器弹出
+  .el-picker__popper {
+    background-color: var(--bg-container);
+    border-color: var(--border-color);
+  }
+
+  // 下拉菜单弹出
+  .el-select-dropdown,
+  .el-dropdown-menu {
+    background-color: var(--bg-container);
+    border-color: var(--border-color);
+  }
+
+  // 弹出层
+  .el-popper {
+    background-color: var(--bg-container);
+    border-color: var(--border-color);
+    border: 1px solid var(--border-color) !important;
+
+    &.el-popper--light {
+      background-color: var(--bg-container);
+      border: 1px solid var(--border-color);
+
+      .el-popper__arrow::before {
+        background-color: var(--bg-container);
+        border-color: var(--border-color);
+      }
+    }
+  }
+
+  // 下拉菜单
+  .el-dropdown__popper {
+    background-color: var(--bg-container) !important;
+    border: 1px solid var(--border-color) !important;
+    box-shadow: var(--shadow-lg) !important;
+
+    .el-dropdown-menu {
+      background-color: var(--bg-container);
+      border: none;
+
+      .el-dropdown-menu__item {
+        color: var(--text-regular);
+
+        &:hover,
+        &:focus {
+          background-color: var(--bg-hover);
+          color: var(--color-primary);
+        }
+      }
+    }
+
+    .el-popper__arrow::before {
+      background-color: var(--bg-container) !important;
+      border-color: var(--border-color) !important;
+    }
+  }
+
+  // 对话框
+  .el-dialog {
+    background-color: var(--bg-container);
+  }
+
+  // 消息框
+  .el-message-box {
+    background-color: var(--bg-container);
+    border-color: var(--border-color);
+  }
+}

+ 9 - 0
src/assets/styles/theme/index.scss

@@ -0,0 +1,9 @@
+/**
+ * 主题系统入口
+ */
+
+// CSS 变量定义
+@import './css-variables.scss';
+
+// Element Plus 覆盖
+@import './element-override.scss';

+ 74 - 0
src/assets/styles/theme/presets.ts

@@ -0,0 +1,74 @@
+/**
+ * 现代风格主题色预设
+ */
+
+export interface ThemeColorSet {
+  name: string
+  primary: string
+  primaryLight3: string
+  primaryLight5: string
+  primaryLight7: string
+  primaryLight9: string
+  primaryDark2: string
+}
+
+export const themeColorPresets: Record<string, ThemeColorSet> = {
+  indigo: {
+    name: '靛蓝',
+    primary: '#6366f1',
+    primaryLight3: '#818cf8',
+    primaryLight5: '#a5b4fc',
+    primaryLight7: '#c7d2fe',
+    primaryLight9: '#e0e7ff',
+    primaryDark2: '#4f46e5'
+  },
+  violet: {
+    name: '紫罗兰',
+    primary: '#8b5cf6',
+    primaryLight3: '#a78bfa',
+    primaryLight5: '#c4b5fd',
+    primaryLight7: '#ddd6fe',
+    primaryLight9: '#ede9fe',
+    primaryDark2: '#7c3aed'
+  },
+  cyan: {
+    name: '青色',
+    primary: '#06b6d4',
+    primaryLight3: '#22d3ee',
+    primaryLight5: '#67e8f9',
+    primaryLight7: '#a5f3fc',
+    primaryLight9: '#cffafe',
+    primaryDark2: '#0891b2'
+  },
+  rose: {
+    name: '玫红',
+    primary: '#f43f5e',
+    primaryLight3: '#fb7185',
+    primaryLight5: '#fda4af',
+    primaryLight7: '#fecdd3',
+    primaryLight9: '#ffe4e6',
+    primaryDark2: '#e11d48'
+  },
+  emerald: {
+    name: '翠绿',
+    primary: '#10b981',
+    primaryLight3: '#34d399',
+    primaryLight5: '#6ee7b7',
+    primaryLight7: '#a7f3d0',
+    primaryLight9: '#d1fae5',
+    primaryDark2: '#059669'
+  },
+  amber: {
+    name: '琥珀',
+    primary: '#f59e0b',
+    primaryLight3: '#fbbf24',
+    primaryLight5: '#fcd34d',
+    primaryLight7: '#fde68a',
+    primaryLight9: '#fef3c7',
+    primaryDark2: '#d97706'
+  }
+}
+
+export type ThemeColorKey = keyof typeof themeColorPresets
+
+export const defaultThemeColor: ThemeColorKey = 'indigo'

+ 6 - 0
src/components.d.ts

@@ -24,6 +24,7 @@ declare module 'vue' {
     ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
+    ElDrawer: typeof import('element-plus/es')['ElDrawer']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
@@ -43,6 +44,8 @@ declare module 'vue' {
     ElOptionGroup: typeof import('element-plus/es')['ElOptionGroup']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElProgress: typeof import('element-plus/es')['ElProgress']
+    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
+    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElResult: typeof import('element-plus/es')['ElResult']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElSelect: typeof import('element-plus/es')['ElSelect']
@@ -56,11 +59,14 @@ declare module 'vue' {
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
     ElText: typeof import('element-plus/es')['ElText']
+    ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElUpload: typeof import('element-plus/es')['ElUpload']
     HelloWorld: typeof import('./components/HelloWorld.vue')['default']
     LangDropdown: typeof import('./components/LangDropdown.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    ThemeSettings: typeof import('./components/ThemeSettings.vue')['default']
+    ThemeSwitch: typeof import('./components/ThemeSwitch.vue')['default']
     VideoPlayer: typeof import('./components/VideoPlayer.vue')['default']
   }
   export interface ComponentCustomProperties {

+ 245 - 0
src/components/ThemeSettings.vue

@@ -0,0 +1,245 @@
+<template>
+  <el-drawer v-model="visible" title="主题设置" direction="rtl" size="320px">
+    <div class="theme-settings">
+      <!-- 主题模式 -->
+      <div class="setting-section">
+        <div class="section-title">主题模式</div>
+        <div class="mode-options">
+          <div
+            v-for="mode in modeOptions"
+            :key="mode.value"
+            class="mode-item"
+            :class="{ active: themeStore.config.mode === mode.value }"
+            @click="themeStore.setMode(mode.value)"
+          >
+            <el-icon :size="20">
+              <component :is="mode.icon" />
+            </el-icon>
+            <span>{{ mode.label }}</span>
+          </div>
+        </div>
+      </div>
+
+      <!-- 主题色 -->
+      <div class="setting-section">
+        <div class="section-title">主题色</div>
+        <div class="color-presets">
+          <el-tooltip
+            v-for="(colors, key) in themeColorPresets"
+            :key="key"
+            :content="colors.name"
+            placement="top"
+          >
+            <div
+              class="color-item"
+              :class="{ active: themeStore.config.colorScheme === key }"
+              :style="{ backgroundColor: colors.primary }"
+              @click="themeStore.setColorScheme(key as ThemeColorKey)"
+            >
+              <el-icon v-if="themeStore.config.colorScheme === key" :size="14">
+                <Check />
+              </el-icon>
+            </div>
+          </el-tooltip>
+        </div>
+      </div>
+
+      <!-- 字体大小 -->
+      <div class="setting-section">
+        <div class="section-title">字体大小</div>
+        <el-radio-group v-model="fontSize" class="font-size-group">
+          <el-radio-button value="small">小</el-radio-button>
+          <el-radio-button value="default">中</el-radio-button>
+          <el-radio-button value="large">大</el-radio-button>
+        </el-radio-group>
+      </div>
+
+      <!-- 其他选项 -->
+      <div class="setting-section">
+        <div class="section-title">其他设置</div>
+        <div class="setting-item">
+          <span>紧凑模式</span>
+          <el-switch v-model="compactMode" />
+        </div>
+      </div>
+
+      <!-- 重置按钮 -->
+      <div class="setting-section">
+        <el-button class="reset-btn" @click="themeStore.resetToDefault">
+          <el-icon><RefreshRight /></el-icon>
+          恢复默认设置
+        </el-button>
+      </div>
+    </div>
+  </el-drawer>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { Sunny, Moon, Monitor, Check, RefreshRight } from '@element-plus/icons-vue'
+import { useThemeStore, type ThemeMode } from '@/store/theme'
+import { themeColorPresets, type ThemeColorKey } from '@/assets/styles/theme/presets'
+
+interface ModeOption {
+  value: ThemeMode
+  label: string
+  icon: typeof Sunny
+}
+
+const modeOptions: ModeOption[] = [
+  { value: 'light', label: '浅色', icon: Sunny },
+  { value: 'dark', label: '深色', icon: Moon },
+  { value: 'system', label: '跟随系统', icon: Monitor }
+]
+
+const props = defineProps<{
+  modelValue: boolean
+}>()
+
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean]
+}>()
+
+const themeStore = useThemeStore()
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+
+const fontSize = computed({
+  get: () => themeStore.config.fontSize,
+  set: (val) => themeStore.setFontSize(val)
+})
+
+const compactMode = computed({
+  get: () => themeStore.config.compactMode,
+  set: () => themeStore.toggleCompactMode()
+})
+</script>
+
+<style lang="scss" scoped>
+.theme-settings {
+  padding: 0 4px;
+}
+
+.setting-section {
+  margin-bottom: 28px;
+
+  .section-title {
+    font-size: 13px;
+    font-weight: var(--font-weight-semibold);
+    color: var(--text-secondary);
+    margin-bottom: 14px;
+    text-transform: uppercase;
+    letter-spacing: 0.5px;
+  }
+}
+
+.mode-options {
+  display: flex;
+  gap: 10px;
+
+  .mode-item {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 8px;
+    padding: 14px 8px;
+    border-radius: var(--radius-lg);
+    background-color: var(--bg-hover);
+    cursor: pointer;
+    transition: all var(--transition-base) var(--transition-timing);
+    border: 2px solid transparent;
+
+    span {
+      font-size: 12px;
+      color: var(--text-secondary);
+    }
+
+    &:hover {
+      background-color: var(--color-primary-light-9);
+    }
+
+    &.active {
+      background-color: var(--color-primary-light-9);
+      border-color: var(--color-primary);
+
+      .el-icon {
+        color: var(--color-primary);
+      }
+
+      span {
+        color: var(--color-primary);
+        font-weight: var(--font-weight-medium);
+      }
+    }
+  }
+}
+
+.color-presets {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+
+  .color-item {
+    width: 40px;
+    height: 40px;
+    border-radius: var(--radius-base);
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #fff;
+    transition: all var(--transition-base) var(--transition-timing);
+    border: 2px solid transparent;
+
+    &:hover {
+      transform: scale(1.08);
+      box-shadow: var(--shadow-md);
+    }
+
+    &.active {
+      border-color: var(--text-primary);
+      box-shadow: 0 0 0 2px var(--bg-container);
+    }
+  }
+}
+
+.font-size-group {
+  width: 100%;
+
+  :deep(.el-radio-button) {
+    flex: 1;
+
+    .el-radio-button__inner {
+      width: 100%;
+      border-radius: var(--radius-base);
+    }
+  }
+}
+
+.setting-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 14px 0;
+  border-bottom: 1px solid var(--border-color);
+
+  span {
+    color: var(--text-regular);
+    font-size: 14px;
+  }
+
+  &:last-child {
+    border-bottom: none;
+  }
+}
+
+.reset-btn {
+  width: 100%;
+  height: 40px;
+  border-radius: var(--radius-base);
+}
+</style>

+ 51 - 0
src/components/ThemeSwitch.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="theme-switch">
+    <el-tooltip :content="isDark ? '切换到浅色模式' : '切换到深色模式'" placement="bottom">
+      <el-button class="switch-btn" circle text @click="themeStore.toggleDarkMode">
+        <el-icon :size="18">
+          <Moon v-if="isDark" />
+          <Sunny v-else />
+        </el-icon>
+      </el-button>
+    </el-tooltip>
+
+    <el-tooltip content="主题设置" placement="bottom">
+      <el-button class="switch-btn" circle text @click="emit('openSettings')">
+        <el-icon :size="18"><Setting /></el-icon>
+      </el-button>
+    </el-tooltip>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { Moon, Sunny, Setting } from '@element-plus/icons-vue'
+import { useThemeStore } from '@/store/theme'
+
+const emit = defineEmits<{
+  openSettings: []
+}>()
+
+const themeStore = useThemeStore()
+const isDark = computed(() => themeStore.isDark)
+</script>
+
+<style lang="scss" scoped>
+.theme-switch {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+
+  .switch-btn {
+    width: 36px;
+    height: 36px;
+    color: var(--text-secondary);
+    transition: all var(--transition-base) var(--transition-timing);
+
+    &:hover {
+      color: var(--color-primary);
+      background-color: var(--bg-hover);
+    }
+  }
+}
+</style>

+ 37 - 0
src/composables/useTheme.ts

@@ -0,0 +1,37 @@
+import { computed } from 'vue'
+import { useThemeStore, type ThemeMode } from '@/store/theme'
+import type { ThemeColorKey } from '@/assets/styles/theme/presets'
+
+/**
+ * 主题 Composable - 简化主题相关操作
+ */
+export function useTheme() {
+  const themeStore = useThemeStore()
+
+  const isDark = computed(() => themeStore.isDark)
+  const primaryColor = computed(() => themeStore.currentColors.primary)
+  const themeMode = computed(() => themeStore.config.mode)
+  const colorScheme = computed(() => themeStore.config.colorScheme)
+
+  function toggleDark() {
+    themeStore.toggleDarkMode()
+  }
+
+  function setMode(mode: ThemeMode) {
+    themeStore.setMode(mode)
+  }
+
+  function setColorScheme(scheme: ThemeColorKey) {
+    themeStore.setColorScheme(scheme)
+  }
+
+  return {
+    isDark,
+    primaryColor,
+    themeMode,
+    colorScheme,
+    toggleDark,
+    setMode,
+    setColorScheme
+  }
+}

+ 36 - 18
src/layout/index.vue

@@ -12,9 +12,9 @@
         :default-active="activeMenu"
         :collapse="!sidebarOpened"
         :collapse-transition="false"
-        background-color="#304156"
-        text-color="#bfcbd9"
-        active-text-color="#409eff"
+        :background-color="isDark ? '#18181b' : '#1e293b'"
+        :text-color="isDark ? '#a1a1aa' : '#94a3b8'"
+        active-text-color="var(--color-primary)"
         router
       >
         <el-menu-item index="/machine">
@@ -93,6 +93,7 @@
           </el-breadcrumb>
         </div>
         <div class="header-right">
+          <ThemeSwitch @open-settings="themeSettingsVisible = true" />
           <LangDropdown />
 
           <el-dropdown @command="handleCommand">
@@ -143,6 +144,9 @@
         <el-button type="primary" :loading="passwordLoading" @click="handleChangePassword">{{ t('确定') }}</el-button>
       </template>
     </el-dialog>
+
+    <!-- 主题设置抽屉 -->
+    <ThemeSettings v-model="themeSettingsVisible" />
   </el-container>
 </template>
 
@@ -165,16 +169,22 @@ import {
   Connection
 } from '@element-plus/icons-vue'
 import LangDropdown from '@/components/LangDropdown.vue'
+import ThemeSwitch from '@/components/ThemeSwitch.vue'
+import ThemeSettings from '@/components/ThemeSettings.vue'
 import { useAppStore } from '@/store/app'
 import { useUserStore } from '@/store/user'
+import { useThemeStore } from '@/store/theme'
 import { changePassword } from '@/api/login'
 import { useI18n } from 'vue-i18n'
 const route = useRoute()
 const router = useRouter()
 const appStore = useAppStore()
 const userStore = useUserStore()
+const themeStore = useThemeStore()
 const { t } = useI18n()
 const sidebarOpened = computed(() => appStore.sidebarOpened)
+const themeSettingsVisible = ref(false)
+const isDark = computed(() => themeStore.isDark)
 const userInfo = computed(() => userStore.userInfo)
 
 const activeMenu = computed(() => {
@@ -277,16 +287,19 @@ onMounted(() => {
 }
 
 .sidebar-container {
-  background-color: #304156;
-  transition: width 0.3s;
+  background-color: var(--bg-sidebar);
+  transition:
+    width var(--transition-base) var(--transition-timing),
+    background-color var(--transition-base) var(--transition-timing);
   overflow: auto;
 
   .logo {
-    height: 60px;
+    height: var(--header-height);
     display: flex;
     align-items: center;
     padding: 0 15px;
-    background-color: #2b3a4a;
+    background-color: var(--bg-sidebar-logo);
+    transition: background-color var(--transition-base) var(--transition-timing);
 
     img {
       width: 32px;
@@ -295,9 +308,9 @@ onMounted(() => {
 
     h1 {
       margin-left: 12px;
-      font-size: 16px;
-      font-weight: 600;
-      color: #fff;
+      font-size: var(--font-size-lg);
+      font-weight: var(--font-weight-semibold);
+      color: var(--text-inverse);
       white-space: nowrap;
     }
   }
@@ -321,9 +334,12 @@ onMounted(() => {
   display: flex;
   align-items: center;
   justify-content: space-between;
-  height: 60px;
-  background-color: #fff;
-  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+  height: var(--header-height);
+  background-color: var(--bg-header);
+  box-shadow: var(--shadow-header);
+  transition:
+    background-color var(--transition-base) var(--transition-timing),
+    box-shadow var(--transition-base) var(--transition-timing);
 
   .header-left {
     display: flex;
@@ -333,10 +349,11 @@ onMounted(() => {
       font-size: 20px;
       cursor: pointer;
       margin-right: 15px;
-      color: #5a5e66;
+      color: var(--text-secondary);
+      transition: color var(--transition-fast) var(--transition-timing);
 
       &:hover {
-        color: #409eff;
+        color: var(--color-primary);
       }
     }
   }
@@ -353,20 +370,21 @@ onMounted(() => {
 
       .username {
         margin: 0 8px;
-        color: #5a5e66;
+        color: var(--text-secondary);
       }
     }
   }
 }
 
 .app-main {
-  background-color: #f5f7fa;
+  background-color: var(--bg-page);
   overflow: auto;
+  transition: background-color var(--transition-base) var(--transition-timing);
 }
 
 .fade-enter-active,
 .fade-leave-active {
-  transition: opacity 0.2s ease;
+  transition: opacity var(--transition-base) ease;
 }
 
 .fade-enter-from,

+ 7 - 0
src/main.ts

@@ -4,6 +4,7 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
 import en from 'element-plus/es/locale/lang/en'
 import zhCn from 'element-plus/es/locale/lang/zh-cn'
 import 'element-plus/dist/index.css'
+import 'element-plus/theme-chalk/dark/css-vars.css'
 
 import App from './App.vue'
 import router from './router'
@@ -11,6 +12,8 @@ import pinia from './store'
 import i18n from './locales'
 import '@/assets/styles/index.scss'
 
+import { useThemeStore } from './store/theme'
+
 const app = createApp(App)
 
 // 注册所有 Element Plus 图标
@@ -26,4 +29,8 @@ app.use(router)
 app.use(pinia)
 app.use(i18n)
 
+// 初始化主题(必须在 pinia 注册后)
+const themeStore = useThemeStore()
+themeStore.applyTheme()
+
 app.mount('#app')

+ 158 - 0
src/store/theme.ts

@@ -0,0 +1,158 @@
+import { defineStore } from 'pinia'
+import { computed, watch } from 'vue'
+import { useStorage, usePreferredDark } from '@vueuse/core'
+import {
+  themeColorPresets,
+  defaultThemeColor,
+  type ThemeColorKey,
+  type ThemeColorSet
+} from '@/assets/styles/theme/presets'
+
+export type ThemeMode = 'light' | 'dark' | 'system'
+
+export interface ThemeConfig {
+  mode: ThemeMode
+  colorScheme: ThemeColorKey
+  fontSize: 'small' | 'default' | 'large'
+  compactMode: boolean
+}
+
+const DEFAULT_CONFIG: ThemeConfig = {
+  mode: 'system',
+  colorScheme: defaultThemeColor,
+  fontSize: 'default',
+  compactMode: false
+}
+
+export const useThemeStore = defineStore('theme', () => {
+  // 持久化存储主题配置
+  const config = useStorage<ThemeConfig>('theme-config', DEFAULT_CONFIG, localStorage, {
+    mergeDefaults: true
+  })
+
+  // 系统深色模式偏好
+  const prefersDark = usePreferredDark()
+
+  // 计算实际主题模式
+  const isDark = computed(() => {
+    if (config.value.mode === 'system') {
+      return prefersDark.value
+    }
+    return config.value.mode === 'dark'
+  })
+
+  // 当前主题色配置
+  const currentColors = computed<ThemeColorSet>(() => {
+    return themeColorPresets[config.value.colorScheme]
+  })
+
+  // 切换深色/浅色模式
+  function toggleDarkMode() {
+    if (config.value.mode === 'system') {
+      config.value.mode = prefersDark.value ? 'light' : 'dark'
+    } else {
+      config.value.mode = config.value.mode === 'dark' ? 'light' : 'dark'
+    }
+  }
+
+  // 设置主题模式
+  function setMode(mode: ThemeMode) {
+    config.value.mode = mode
+  }
+
+  // 设置主题色
+  function setColorScheme(scheme: ThemeColorKey) {
+    config.value.colorScheme = scheme
+  }
+
+  // 设置字体大小
+  function setFontSize(size: 'small' | 'default' | 'large') {
+    config.value.fontSize = size
+  }
+
+  // 切换紧凑模式
+  function toggleCompactMode() {
+    config.value.compactMode = !config.value.compactMode
+  }
+
+  // 重置为默认配置
+  function resetToDefault() {
+    config.value = { ...DEFAULT_CONFIG }
+  }
+
+  // 应用主题到 DOM
+  function applyTheme() {
+    const root = document.documentElement
+
+    // 1. 设置主题模式 (使用 Element Plus 官方的 dark 类名)
+    if (isDark.value) {
+      root.classList.add('dark')
+    } else {
+      root.classList.remove('dark')
+    }
+
+    // 2. 设置主题色 CSS 变量
+    const colors = currentColors.value
+    root.style.setProperty('--color-primary', colors.primary)
+    root.style.setProperty('--color-primary-light-3', colors.primaryLight3)
+    root.style.setProperty('--color-primary-light-5', colors.primaryLight5)
+    root.style.setProperty('--color-primary-light-7', colors.primaryLight7)
+    root.style.setProperty('--color-primary-light-9', colors.primaryLight9)
+    root.style.setProperty('--color-primary-dark-2', colors.primaryDark2)
+
+    // 3. 同步 Element Plus 主题变量
+    root.style.setProperty('--el-color-primary', colors.primary)
+    root.style.setProperty('--el-color-primary-light-3', colors.primaryLight3)
+    root.style.setProperty('--el-color-primary-light-5', colors.primaryLight5)
+    root.style.setProperty('--el-color-primary-light-7', colors.primaryLight7)
+    root.style.setProperty('--el-color-primary-light-8', colors.primaryLight7)
+    root.style.setProperty('--el-color-primary-light-9', colors.primaryLight9)
+    root.style.setProperty('--el-color-primary-dark-2', colors.primaryDark2)
+
+    // 4. 设置字体大小
+    const fontSizeMap = {
+      small: '13px',
+      default: '14px',
+      large: '15px'
+    }
+    root.style.setProperty('--font-size-base', fontSizeMap[config.value.fontSize])
+
+    // 5. 设置紧凑模式
+    if (config.value.compactMode) {
+      root.classList.add('compact-mode')
+    } else {
+      root.classList.remove('compact-mode')
+    }
+  }
+
+  // 监听配置变化,自动应用
+  watch(
+    [() => config.value, isDark],
+    () => {
+      applyTheme()
+    },
+    { deep: true, immediate: true }
+  )
+
+  // 监听系统主题变化
+  watch(prefersDark, () => {
+    if (config.value.mode === 'system') {
+      applyTheme()
+    }
+  })
+
+  return {
+    config,
+    isDark,
+    currentColors,
+    prefersDark,
+    themeColorPresets,
+    toggleDarkMode,
+    setMode,
+    setColorScheme,
+    setFontSize,
+    toggleCompactMode,
+    resetToDefault,
+    applyTheme
+  }
+})