【Vue3】SCSS 进阶篇

1. SCSS高级语法

🌟**【青柠代码录】--- 青柠来相伴,代码更简单** 🌟

🔥**【全栈】博客合集:** https://www.yuque.com/u12587869/zplytb/ur5ohwqxd2axtiny 🔥

🎯**【Java】面试题:** https://www.yuque.com/u12587869/zplytb/eh7yqzitiab693og 🎯

1.1 条件判断(@if/@else):多场景样式适配

@if/@else 可根据传入参数、变量值生成不同样式,核心用于「多状态、多配置」的组件开发(如按钮尺寸、卡片阴影深度、状态切换样式),搭配参数校验,能有效避免开发中的非法参数错误,提升代码健壮性。

实战场景:通用按钮(多尺寸、多状态),结合上一篇的基础混合器,完善动态适配逻辑,支持small/base/large三种尺寸,以及默认/禁用/高亮三种状态。

复制代码
// 全局混合器:src/styles/_mixins.scss(承接第一篇,补充完善)
// 先引入全局变量(确保样式统一)
@import "@/styles/_variables.scss";
​
// 1. 按钮尺寸混合器(带参数校验)
@mixin btn-size($size: base) {
  // 校验参数合法性:仅支持small/base/large,非法参数抛出错误(便于调试)
  @if not index(('small', 'base', 'large'), $size) {
    @error "按钮尺寸仅支持 small/base/large,当前传入:#{$size}";
  }
​
  // 小尺寸按钮(适配列表操作、弹窗内按钮)
  @if $size == small {
    height: 32px;
    padding: 4px 12px;
    font-size: 12px;
    border-radius: $border-radius-sm; // 全局小圆角变量
  } 
  // 基础尺寸按钮(默认,适配大部分场景)
  @else if $size == base {
    height: 40px;
    padding: 8px 16px;
    font-size: $font-size-base; // 全局基础字体变量
    border-radius: $border-radius-base; // 全局基础圆角变量
  } 
  // 大尺寸按钮(适配表单提交、首页核心操作)
  @else if $size == large {
    height: 48px;
    padding: 12px 24px;
    font-size: 16px;
    border-radius: $border-radius-lg; // 全局大圆角变量
  }
}
​
// 2. 按钮状态混合器(结合条件判断)
@mixin btn-status($status: default) {
  @if $status == default {
    background-color: $primary-color; // 全局主色调
    color: #fff;
    border: 1px solid transparent;
    &:hover {
      background-color: darken($primary-color, 8%); // 内置函数:颜色加深
    }
  } @else if $status == disabled {
    background-color: $border-color; // 全局边框色
    color: #999;
    border: 1px solid $border-color;
    cursor: not-allowed;
    &:hover {
      background-color: $border-color; // 禁用状态取消hover效果
    }
  } @else if $status == highlight {
    background-color: #fff;
    color: $primary-color;
    border: 1px solid $primary-color;
    &:hover {
      background-color: lighten($primary-color, 15%); // 内置函数:颜色变浅
    }
  }
}
​
// 3. 整合按钮混合器(简化组件使用)
@mixin btn-common($size: base, $status: default) {
  @include btn-size($size);
  @include btn-status($status);
  outline: none;
  cursor: pointer;
  transition: background-color 0.2s ease, transform 0.1s ease;
  &:active {
    transform: scale(0.98); // 点击反馈效果
  }
}

组件中使用示例(Vue3单文件组件,承接第一篇的通用按钮组件,补充动态配置):

复制代码
<template>
  <button 
    class="btn"
    :class="{ 'btn--disabled': disabled }"
    :disabled="disabled"
    @click="handleClick"
  >
    <slot>按钮</slot>
  </button>
</template>
​
<script setup lang="ts">
import { defineProps, emit } from 'vue';
​
// 组件props(支持尺寸、状态自定义,与SCSS混合器参数对应)
const props = defineProps({
  size: {
    type: String,
    default: 'base',
    validator: (value: string) => ['small', 'base', 'large'].includes(value)
  },
  status: {
    type: String,
    default: 'default',
    validator: (value: string) => ['default', 'disabled', 'highlight'].includes(value)
  },
  disabled: {
    type: Boolean,
    default: false
  }
});
​
const emit = emit('click');
const handleClick = () => {
  if (!props.disabled) {
    emit('click');
  }
};
</script>
​
<style lang="scss">
// 直接使用全局混合器,传入props参数,实现动态样式
.btn {
  @include btn-common($size: #{props.size}, $status: #{props.status});
  // 补充额外样式,适配组件特殊需求
  &--disabled {
    opacity: 0.7;
  }
}
</style>

注意事项

  • 使用@if/@else时,建议添加参数校验(@error),避免传入非法参数导致样式异常,尤其适合多团队协作场景;

  • 条件判断中可结合SCSS内置函数(如darken/lighten),实现更灵活的样式动态调整;

  • Vue3组件中使用时,通过#{props.xxx}将props值传入SCSS,实现组件props与SCSS样式的联动。

1.2 循环语句(@for/@each):批量生成样式

项目中,经常会遇到「重复样式批量生成」的场景(如间距类、主题色类、响应式断点类),使用@for(数值循环)和@each(列表循环),可大幅减少冗余代码,提升开发效率,同时保证样式统一性。

1.2.1 @for循环:数值循环(适合连续数值场景)

实战场景 :批量生成全局间距类(通用规范),适配不同场景的内边距/外边距,避免重复书写.spacing-1.spacing-2等类名。

复制代码
// src/styles/_mixins.scss(新增间距循环)
// 批量生成间距类(内边距)
@for $i from 1 through 8 {
  // 定义步长:每级间距增加4px(规范,可自定义)
  $spacing-value: $i * 4px;
  // 内边距类:p-1 ~ p-8
  .p-#{$i} {
    padding: $spacing-value;
  }
  // 上下内边距类:py-1 ~ py-8
  .py-#{$i} {
    padding-top: $spacing-value;
    padding-bottom: $spacing-value;
  }
  // 左右内边距类:px-1 ~ px-8
  .px-#{$i} {
    padding-left: $spacing-value;
    padding-right: $spacing-value;
  }
}
​
// 批量生成间距类(外边距,与内边距对应)
@for $i from 1 through 8 {
  $spacing-value: $i * 4px;
  .m-#{$i} {
    margin: $spacing-value;
  }
  .my-#{$i} {
    margin-top: $spacing-value;
    margin-bottom: $spacing-value;
  }
  .mx-#{$i} {
    margin-left: $spacing-value;
    margin-right: $spacing-value;
  }
}

编译后效果(自动生成32个间距类,无需手动书写):

复制代码
.p-1 { padding: 4px; }
.py-1 { padding-top: 4px; padding-bottom: 4px; }
.px-1 { padding-left: 4px; padding-right: 4px; }
/* ... 依次生成p-2到p-8,m-1到m-8相关类名 */

使用场景:页面布局、组件内间距调整,直接添加类名即可,如<div class="px-4 py-3">内容</div>,无需重复书写padding样式。

1.2.2 @each循环:列表循环(适合非连续、多组数据场景)

实战场景1:批量生成主题色样式(品牌色规范),结合全局主题色列表,批量生成文本色、背景色、边框色类,适配不同模块的主题需求。

复制代码
// src/styles/_variables.scss(定义主题色列表)
// 品牌色列表(key为主题名,value为色值)
$theme-colors: (
  primary: #42b983,    // 主色调
  secondary: #64748b,  // 辅助色
  danger: #f56c6c,     // 危险色
  success: #67c23a,    // 成功色
  warning: #e6a600,    // 警告色
  info: #1890ff        // 信息色
);
​
// src/styles/_mixins.scss(批量生成主题色类)
@import "@/styles/_variables.scss";
​
// 批量生成主题色文本类(text-primary、text-danger等)
@each $name, $color in $theme-colors {
  .text-#{$name} {
    color: $color;
    // hover效果(统一规范)
    &:hover {
      color: darken($color, 10%);
      transition: color 0.2s ease;
    }
  }
}
​
// 批量生成主题色背景类(bg-primary、bg-danger等)
@each $name, $color in $theme-colors {
  .bg-#{$name} {
    background-color: $color;
    color: #fff; // 白色文本,保证可读性
    &:hover {
      background-color: darken($color, 8%);
      transition: background-color 0.2s ease;
    }
  }
}
​
// 批量生成主题色边框类(border-primary、border-danger等)
@each $name, $color in $theme-colors {
  .border-#{$name} {
    border: 1px solid $color;
  }
}

实战场景2:批量生成响应式断点类(适配PC/移动端),结合响应式断点列表,批量生成不同屏幕尺寸的适配类,简化响应式开发。

复制代码
// src/styles/_variables.scss(定义响应式断点)
$breakpoints: (
  mobile: 768px,    // 移动端(小于768px)
  pad: 768px,       // 平板(大于等于768px)
  pc: 1200px,       // 桌面端(大于等于1200px)
  large-pc: 1920px  // 大屏桌面(大于等于1920px)
);
​
// src/styles/_mixins.scss(批量生成响应式类)
@each $device, $width in $breakpoints {
  @if $device == mobile {
    // 移动端:小于指定宽度
    @media (max-width: $width) {
      .hidden-on-#{$device} { display: none; } // 移动端隐藏
      .show-on-#{$device} { display: block; }  // 移动端显示
    }
  } @else {
    // 其他设备:大于等于指定宽度
    @media (min-width: $width) {
      .hidden-on-#{$device} { display: none; } // 对应设备隐藏
      .show-on-#{$device} { display: block; }  // 对应设备显示
    }
  }
}

使用示例

复制代码
<!-- 移动端显示,PC端隐藏 -->
<div class="show-on-mobile hidden-on-pc">移动端专属内容</div>
​
<!-- PC端显示,移动端隐藏 -->
<div class="show-on-pc hidden-on-mobile">PC端专属内容</div>

注意事项

  • @for循环适合「连续数值」场景(如1-8的间距),@each适合「列表数据」场景(如主题色、断点);

  • 循环中使用#{$变量名}实现「动态类名/属性值」,注意语法格式,避免遗漏#{}

  • 批量生成的样式需遵循规范,统一步长、 hover效果等,避免样式混乱。

1.3 自定义函数(@function):复杂样式计算

SCSS内置函数(如darken/lighten/hex-to-rgb)可满足基础需求,但项目中经常需要「自定义样式计算逻辑」(如响应式尺寸、颜色转换、动态间距),此时使用@function封装自定义函数,可实现逻辑复用,提升代码可维护性。

自定义函数与混合器(@mixin)的区别:混合器用于「生成样式块」,函数用于「返回计算后的值」,可嵌套在样式属性中使用。

实战场景1:HEX转RGBA(高频需求)

项目中经常需要将十六进制颜色(HEX)转换为带透明度的RGBA颜色,封装自定义函数,避免重复计算,同时保证透明度统一。

复制代码
// src/styles/_functions.scss(新建自定义函数文件,模块化管理)
// HEX转RGBA函数:参数1=HEX色值,参数2=透明度(默认1,不透明)
@function hex-to-rgba($hex, $alpha: 1) {
  // 校验HEX色值合法性(简化版,避免传入非法色值)
  @if length($hex) != 7 and length($hex) != 4 {
    @error "HEX色值格式错误,需为#fff或#ffffff,当前传入:#{$hex}";
  }
  // 内置函数hex-to-rgb:将HEX转为RGB数组(如#42b983转为(66, 185, 131))
  $rgb: hex-to-rgb($hex);
  // 提取RGB各分量
  $red: nth($rgb, 1);
  $green: nth($rgb, 2);
  $blue: nth($rgb, 3);
  // 返回RGBA值
  @return rgba($red, $green, $blue, $alpha);
}
​
// 引入到全局混合器,方便使用
// src/styles/_mixins.scss
@import "@/styles/_functions.scss";

使用示例

复制代码
.card {
  // 主色调,透明度0.8
  background-color: hex-to-rgba($primary-color, 0.8);
  // 危险色,透明度0.6
  border: 1px solid hex-to-rgba($danger-color, 0.6);
}

实战场景2:响应式尺寸计算(适配不同屏幕)

大型项目中,响应式布局需要根据屏幕宽度动态计算元素尺寸(如字体、容器宽度),封装自定义函数,实现「按屏幕比例自适应」,避免写多个媒体查询。

复制代码
// src/styles/_functions.scss(新增响应式尺寸函数)
// 响应式尺寸计算:参数1=基础尺寸(1920px屏幕下的尺寸),参数2=基准屏幕宽度(默认1920px)
@function responsive-size($base-size, $base-screen: 1920) {
  // 校验参数:基础尺寸和基准屏幕宽度必须为正数
  @if $base-size <= 0 or $base-screen <= 0 {
    @error "基础尺寸和基准屏幕宽度必须为正数,当前传入:base-size=#{$base-size},base-screen=#{$base-screen}";
  }
  // 计算逻辑:(基础尺寸 / 基准屏幕宽度) * 100vw,实现按屏幕比例自适应
  @return ($base-size / $base-screen) * 100vw;
}
​
// 响应式字体大小(在响应式尺寸基础上,限制最小/最大值,避免字体过小/过大)
@function responsive-font($base-size, $min-size: 12px, $max-size: 24px) {
  $responsive: responsive-size($base-size);
  // 限制最小值
  @if $responsive < $min-size {
    @return $min-size;
  }
  // 限制最大值
  @else if $responsive > $max-size {
    @return $max-size;
  }
  // 正常返回响应式尺寸
  @else {
    @return $responsive;
  }
}

使用示例(首页标题响应式适配):

复制代码
.home-title {
  // 1920px屏幕下字体大小为28px,自适应调整,最小16px,最大32px
  font-size: responsive-font(28px, 16px, 32px);
  // 1920px屏幕下宽度为800px,自适应调整
  width: responsive-size(800px);
}

实战场景3:动态行高计算(字体适配)

UI规范中,行高通常与字体大小关联(如行高=字体大小+8px),封装自定义函数,实现行高自动适配,避免手动计算。

复制代码
// src/styles/_functions.scss(新增行高函数)
@function line-height($font-size, $offset: 8px) {
  // 行高=字体大小+偏移量(默认8px,可自定义)
  @return $font-size + $offset;
}
​
// 使用示例
.title {
  font-size: 18px;
  line-height: line-height(18px); // 行高=18+8=26px
}
​
.subtitle {
  font-size: 14px;
  line-height: line-height(14px, 6px); // 自定义偏移量,行高=20px
}

注意事项

  • 自定义函数需添加参数校验(@error),避免非法参数导致计算错误;

  • 函数返回值必须是「合法的样式值」(如px、vw、rgba),不能返回样式块;

  • 建议将自定义函数单独放在_functions.scss文件中,模块化管理,便于多团队协作查阅和修改。


2. SCSS主题切换:结合Vue3响应式实现全局亮色/暗色切换

主题切换是项目的高频需求(如后台管理系统、移动端APP),核心需求是「全局样式动态切换」+「用户选择持久化」,结合SCSS变量、CSS变量和Vue3响应式数据,可实现无闪屏、可扩展的主题切换功能。

2.1 核心思路

  1. 定义多套主题变量(亮色/暗色,可扩展更多主题);

  2. 将SCSS主题变量映射为CSS全局变量(实现动态切换);

  3. 通过Vue3响应式数据控制body类名(区分不同主题);

  4. 使用localStorage持久化用户主题选择(页面刷新后保留);

  5. 封装主题切换组件,实现无闪屏切换,适配全局使用。

2.2 步骤1:定义主题变量(模块化拆分)

按主题类型拆分SCSS文件,集中管理主题变量,便于后续扩展和维护,目录结构延续第一篇的工程化规范:

复制代码
src/styles/
├── themes/          // 主题目录(新增)
│   ├── _light.scss  // 亮色主题变量
│   ├── _dark.scss   // 暗色主题变量
│   └── _theme-map.scss // 主题映射(统一管理所有主题)
├── _variables.scss  // 全局公共变量(不随主题变化)
├── _mixins.scss     // 全局混合器
├── _functions.scss  // 自定义函数
└── main.scss        // 样式入口

编写主题变量文件,确保亮色/暗色变量一一对应,便于映射:

复制代码
// src/styles/themes/_light.scss(亮色主题,默认主题)
// 亮色主题标识(用于区分主题)
$theme-name: 'light' !default;
​
// 背景色(全局/卡片/表单)
$bg-color: #f8f9fa !default;          // 全局背景色
$card-bg-color: #fff !default;        // 卡片背景色
$form-bg-color: #fff !default;        // 表单背景色
​
// 文本色(标题/正文/辅助文本)
$text-color: #333 !default;           // 正文文本色
$title-color: #222 !default;          // 标题文本色
$aux-text-color: #666 !default;       // 辅助文本色
​
// 边框色/分割线色
$border-color: #e5e6eb !default;      // 基础边框色
$divider-color: #f0f0f0 !default;     // 分割线色
​
// 主题色(与全局公共变量一致,可单独覆盖)
$primary-color: #42b983 !default;     // 主色调
$danger-color: #f56c6c !default;      // 危险色
$success-color: #67c23a !default;     // 成功色
​
// 高亮色(hover/选中状态)
$hover-color: lighten($primary-color, 15%) !default; // 主色调hover
$active-color: darken($primary-color, 8%) !default;  // 主色调选中
// src/styles/themes/_dark.scss(暗色主题)
$theme-name: 'dark' !default;
​
// 背景色(深色模式,避免过暗伤眼)
$bg-color: #1e1e1e !default;          // 全局背景色
$card-bg-color: #2d2d2d !default;     // 卡片背景色
$form-bg-color: #2d2d2d !default;     // 表单背景色
​
// 文本色(深色模式,提高对比度)
$text-color: #fff !default;           // 正文文本色
$title-color: #f5f5f5 !default;       // 标题文本色
$aux-text-color: #aaa !default;       // 辅助文本色
​
// 边框色/分割线色(深色模式适配)
$border-color: #3d3d3d !default;      // 基础边框色
$divider-color: #333 !default;        // 分割线色
​
// 主题色(深色模式下微调,提高对比度)
$primary-color: #5fc39c !default;     // 主色调(变亮,适配深色背景)
$danger-color: #f78a8a !default;      // 危险色(变亮)
$success-color: #87d068 !default;     // 成功色(变亮)
​
// 高亮色(深色模式适配)
$hover-color: lighten($primary-color, 10%) !default;
$active-color: darken($primary-color, 5%) !default;

编写主题映射文件,统一管理所有主题,便于后续获取主题变量:

复制代码
// src/styles/themes/_theme-map.scss
// 引入所有主题变量
@use './light' as light;
@use './dark' as dark;
​
// 主题映射表:key=主题名,value=主题变量集合
$theme-map: (
  light: (
    bg-color: light.$bg-color,
    card-bg-color: light.$card-bg-color,
    form-bg-color: light.$form-bg-color,
    text-color: light.$text-color,
    title-color: light.$title-color,
    aux-text-color: light.$aux-text-color,
    border-color: light.$border-color,
    divider-color: light.$divider-color,
    primary-color: light.$primary-color,
    danger-color: light.$danger-color,
    success-color: light.$success-color,
    hover-color: light.$hover-color,
    active-color: light.$active-color
  ),
  dark: (
    bg-color: dark.$bg-color,
    card-bg-color: dark.$card-bg-color,
    form-bg-color: dark.$form-bg-color,
    text-color: dark.$text-color,
    title-color: dark.$title-color,
    aux-text-color: dark.$aux-text-color,
    border-color: dark.$border-color,
    divider-color: dark.$divider-color,
    primary-color: dark.$primary-color,
    danger-color: dark.$danger-color,
    success-color: dark.$success-color,
    hover-color: dark.$hover-color,
    active-color: dark.$active-color
  )
);
​
// 自定义函数:根据主题名和变量名,获取对应主题的变量值
@function get-theme-variable($theme, $key) {
  // 校验主题名合法性
  @if not map-has-key($theme-map, $theme) {
    @error "主题名不存在,仅支持light/dark,当前传入:#{$theme}";
  }
  // 校验变量名合法性
  @if not map-has-key(map-get($theme-map, $theme), $key) {
    @error "主题变量不存在,当前主题:#{$theme},变量名:#{$key}";
  }
  // 返回对应变量值
  @return map-get(map-get($theme-map, $theme), $key);
}

2.3 步骤2:全局样式入口配置(映射CSS变量)

在全局样式入口文件中,将SCSS主题变量映射为CSS全局变量(--变量名),通过body类名(.light/.dark)控制不同主题的CSS变量值,实现动态切换。

复制代码
// src/styles/main.scss(全局样式入口,承接第一篇)
// 引入主题映射、公共变量、混合器、函数
@use './themes/theme-map' as theme;
@import './_variables.scss';
@import './_mixins.scss';
@import './_functions.scss';

// 1. 基础样式重置(统一浏览器默认样式,不随主题变化)
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: 'Microsoft YaHei', sans-serif;
}

// 2. 亮色主题:将SCSS变量映射为CSS全局变量
body.light {
  --bg-color: #{theme.get-theme-variable(light, bg-color)};
  --card-bg-color: #{theme.get-theme-variable(light, card-bg-color)};
  --form-bg-color: #{theme.get-theme-variable(light, form-bg-color)};
  --text-color: #{theme.get-theme-variable(light, text-color)};
  --title-color: #{theme.get-theme-variable(light, title-color)};
  --aux-text-color: #{theme.get-theme-variable(light, aux-text-color)};
  --border-color: #{theme.get-theme-variable(light, border-color)};
  --divider-color: #{theme.get-theme-variable(light, divider-color)};
  --primary-color: #{theme.get-theme-variable(light, primary-color)};
  --danger-color: #{theme.get-theme-variable(light, danger-color)};
  --success-color: #{theme.get-theme-variable(light, success-color)};
  --hover-color: #{theme.get-theme-variable(light, hover-color)};
  --active-color: #{theme.get-theme-variable(light, active-color)};
}

// 3. 暗色主题:映射CSS全局变量(与亮色一一对应)
body.dark {
  --bg-color: #{theme.get-theme-variable(dark, bg-color)};
  --card-bg-color: #{theme.get-theme-variable(dark, card-bg-color)};
  --form-bg-color: #{theme.get-theme-variable(dark, form-bg-color)};
  --text-color: #{theme.get-theme-variable(dark, text-color)};
  --title-color: #{theme.get-theme-variable(dark, title-color)};
  --aux-text-color: #{theme.get-theme-variable(dark, aux-text-color)};
  --border-color: #{theme.get-theme-variable(dark, border-color)};
  --divider-color: #{theme.get-theme-variable(dark, divider-color)};
  --primary-color: #{theme.get-theme-variable(dark, primary-color)};
  --danger-color: #{theme.get-theme-variable(dark, danger-color)};
  --success-color: #{theme.get-theme-variable(dark, success-color)};
  --hover-color: #{theme.get-theme-variable(dark, hover-color)};
  --active-color: #{theme.get-theme-variable(dark, active-color)};
}

// 4. 全局样式:使用CSS变量(统一适配所有主题)
body {
  background-color: var(--bg-color);
  color: var(--text-color);
  font-size: $font-size-base;
  transition: background-color 0.3s ease, color 0.3s ease; // 过渡效果,避免闪屏
}

// 5. 通用组件样式:使用CSS变量(承接第一篇,适配主题切换)
.card {
  background-color: var(--card-bg-color);
  border: 1px solid var(--border-color);
  border-radius: $border-radius-base;
  padding: $spacing-md;
  box-shadow: $shadow-base;
  transition: background-color 0.3s ease;
}

.btn {
  background-color: var(--primary-color);
  color: #fff;
  border: 1px solid transparent;
  border-radius: $border-radius-base;
  padding: 8px 16px;
  cursor: pointer;
  &:hover {
    background-color: var(--hover-color);
  }
  &:active {
    background-color: var(--active-color);
  }
}

// 其他通用样式(表单、标题等),均使用CSS变量...

2.4 步骤3:Vue3响应式控制主题(封装工具函数)

封装主题控制工具函数,实现「主题切换」「初始化主题」「持久化存储」,便于全局调用,贴合Vue3组合式API开发。

复制代码
// src/utils/theme.ts(新增主题工具文件)
// 主题类型定义(限制主题名,避免非法值)
type ThemeType = 'light' | 'dark';

/**
 * 获取本地存储的主题
 * 优先读取localStorage,无值则返回默认主题(light)
 */
const getStoredTheme = (): ThemeType => {
  const storedTheme = localStorage.getItem('theme');
  // 校验本地存储的主题是否合法,非法则返回默认主题
  return (storedTheme as ThemeType) || 'light';
};

/**
 * 设置主题
 * @param theme 主题名(light/dark)
 */
export const setTheme = (theme: ThemeType) => {
  // 1. 校验主题合法性
  if (!['light', 'dark'].includes(theme)) {
    console.error('主题名非法,仅支持light/dark');
    return;
  }
  // 2. 保存主题到本地存储(持久化)
  localStorage.setItem('theme', theme);
  // 3. 设置body类名,切换主题样式
  document.body.className = theme;
  // 4. 可扩展:发送主题切换事件,供其他组件监听(如导航栏、设置面板)
  window.dispatchEvent(new CustomEvent('theme-change', { detail: theme }));
};

/**
 * 初始化主题
 * 页面加载时调用,恢复用户上次选择的主题
 */
export const initTheme = () => {
  const theme = getStoredTheme();
  setTheme(theme);
};

/**
 * 切换主题(亮色↔暗色)
 */
export const toggleTheme = () => {
  const currentTheme = getStoredTheme();
  const newTheme = currentTheme === 'light' ? 'dark' : 'light';
  setTheme(newTheme);
  return newTheme; // 返回新主题,便于组件更新状态
};

2.5 步骤4:封装主题切换组件(全局可用)

封装主题切换按钮组件,结合Vue3组合式API,实现主题切换交互,适配全局使用(如导航栏、设置面板)。

复制代码
<template>
  <button class="theme-switch" @click="handleToggle" :aria-label="`切换到${currentTheme === 'light' ? '暗色' : '亮色'}模式`">
    <!-- 图标可替换为自己的图标(如Element Plus图标) -->
    <span v-if="currentTheme === 'light'" class="icon">🌙</span>
    <span v-else class="icon">☀️</span>
    <span class="text">{{ currentTheme === 'light' ? '暗色模式' : '亮色模式' }}</span>
  </button>
</template>

<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { initTheme, toggleTheme, getStoredTheme } from '@/utils/theme';

// 响应式存储当前主题
const currentTheme = ref<'light' | 'dark'>('light');

// 页面挂载时初始化主题
onMounted(() => {
  initTheme();
  currentTheme.value = getStoredTheme();
  // 监听主题切换事件(其他地方切换主题时,同步更新组件状态)
  window.addEventListener('theme-change', (e: CustomEvent) => {
    currentTheme.value = e.detail;
  });
});

// 切换主题
const handleToggle = () => {
  currentTheme.value = toggleTheme();
};
</script>

<style lang="scss" scoped>
.theme-switch {
  display: flex;
  align-items: center;
  gap: 8px;
  background-color: var(--card-bg-color);
  color: var(--text-color);
  border: 1px solid var(--border-color);
  border-radius: $border-radius-base;
  padding: 6px 12px;
  cursor: pointer;
  transition: all 0.2s ease;

  &:hover {
    background-color: var(--hover-color);
    color: #fff;
  }

  .icon {
    font-size: 16px;
  }

  .text {
    font-size: 14px;
  }
}
</style>

2.6 步骤5:全局引入与使用

在Vue3入口文件中初始化主题,确保页面加载时就应用用户选择的主题:

复制代码
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import '@/styles/main.scss'; // 引入全局样式
import { initTheme } from '@/utils/theme';

// 初始化主题(必须在mount之前调用)
initTheme();

const app = createApp(App);
app.mount('#app');

在需要使用主题切换按钮的地方(如导航栏)引入组件:

复制代码
<template>
  <nav class="navbar">
    <div class="navbar-left">系统名称</div>
    <div class="navbar-right">
      <ThemeSwitch />
    </div>
  </nav>
</template>

<script setup lang="ts">
import ThemeSwitch from '@/components/ThemeSwitch.vue';
</script>

2.7 核心优势与扩展说明

  • 无闪屏切换:通过CSS过渡效果(transition),避免主题切换时的样式闪屏,提升用户体验;

  • 可扩展性强 :新增主题(如浅色模式、深色模式2),只需新增对应的_xxx.scss文件,修改_theme-map.scss即可,无需修改其他代码;

  • 持久化存储:通过localStorage保存用户主题选择,页面刷新后仍保留,提升用户体验;

  • 全局统一:所有组件样式均使用CSS变量,主题切换时全局样式自动同步,无需单独适配;

  • 扩展建议:可结合Vuex/Pinia管理主题状态,实现跨组件主题状态共享(适合大型项目)。


3. SCSS与组件库适配:自定义Element Plus样式

项目中,通常会使用Element Plus(Vue3)等组件库提升开发效率,但组件库默认样式往往不符合设计规范,此时通过SCSS覆盖组件库的默认变量,可实现「组件库样式自定义」,无需修改组件库源码,且升级组件库后仍能保留自定义样式。

3.1 适配Element Plus(Vue3首选组件库)

Element Plus提供了完整的SCSS变量体系,通过覆盖这些变量,可自定义组件库的主色调、字体、圆角、间距等,核心思路是「引入组件库SCSS源文件 + 覆盖默认变量」。

步骤1:安装Element Plus及相关依赖

复制代码
// 安装Element Plus
npm install element-plus
​
// 安装Element Plus图标(可选,按需引入)
npm install @element-plus/icons-vue

步骤2:创建Element Plus自定义变量文件

在SCSS工程化目录中,新增组件库自定义变量文件,集中覆盖Element Plus默认变量:

复制代码
// src/styles/element-plus/_variables.scss(新增目录和文件)
// 1. 覆盖Element Plus默认变量(完整变量列表参考Element Plus官方文档)
// 核心变量:主色调、辅助色、字体、圆角、间距等
$--color-primary: #42b983; // 主色调(与主题色一致)
$--color-success: #67c23a; // 成功色
$--color-warning: #e6a600; // 警告色
$--color-danger: #f56c6c;  // 危险色
$--color-info: #1890ff;    // 信息色
​
// 字体相关
$--font-size-base: 14px;       // 基础字体大小(与全局变量一致)
$--font-family: 'Microsoft YaHei', sans-serif; // 字体(与全局一致)
​
// 圆角相关
$--border-radius-base: 8px;    // 基础圆角(与全局变量一致)
$--border-radius-small: 4px;   // 小圆角
$--border-radius-large: 12px;  // 大圆角
​
// 按钮相关
$--button-height-base: 40px;   // 基础按钮高度
$--button-padding-horizontal-base: 16px; // 按钮水平内边距
$--button-font-size-base: 14px; // 按钮字体大小
​
// 输入框相关
$--input-height-base: 40px;    // 输入框高度
$--input-border-radius: 8px;   // 输入框圆角
​
// 卡片相关
$--card-border-radius: 8px;    // 卡片圆角
$--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); // 卡片阴影
​
// 2. 引入Element Plus的SCSS源文件(关键:必须在变量覆盖之后引入)
// 注意:引入的是src目录下的scss文件,不是编译后的css文件
@use 'element-plus/theme-chalk/src/common/var.scss' with (
  // 覆盖Element Plus的颜色变量(与上面的变量对应)
  $colors: (
    primary: (
      base: $--color-primary,
    ),
    success: (
      base: $--color-success,
    ),
    warning: (
      base: $--color-warning,
    ),
    danger: (
      base: $--color-danger,
    ),
    info: (
      base: $--color-info,
    ),
  ),
  // 覆盖字体变量
  $font-size: (
    base: $--font-size-base,
  ),
  // 覆盖圆角变量
  $border-radius: (
    base: $--border-radius-base,
    small: $--border-radius-small,
    large: $--border-radius-large,
  ),
  // 覆盖按钮变量
  $button: (
    height: (
      base: $--button-height-base,
    ),
    padding: (
      horizontal: (
        base: $--button-padding-horizontal-base,
      ),
    ),
    font-size: (
      base: $--button-font-size-base,
    ),
  ),
);
​
// 3. 额外自定义Element Plus组件样式(补充变量无法覆盖的样式)
// 示例:自定义按钮hover效果
.el-button {
  &:hover {
    transform: translateY(-1px);
    transition: transform 0.2s ease, background-color 0.2s ease;
  }
}
​
// 自定义输入框聚焦效果
.el-input__inner:focus {
  box-shadow: 0 0 0 2px rgba($--color-primary, 0.2);
  border-color: $--color-primary;
}
​
// 自定义卡片样式
.el-card {
  border: 1px solid var(--border-color); // 结合主题CSS变量
}

步骤3:Vite配置全局注入(避免重复引入)

修改Vite配置,将Element Plus自定义变量文件全局注入,确保所有组件都能使用自定义后的样式:

复制代码
// vite.config.ts(承接第一篇,补充配置)
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
// 按需引入Element Plus组件(可选,减少打包体积)
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';

export default defineConfig({
  plugins: [
    vue(),
    // 按需引入Element Plus(推荐,减少打包体积)
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `
          // 全局注入:Element Plus自定义变量、全局变量、混合器
          @import "@/styles/element-plus/_variables.scss";
          @import "@/styles/_variables.scss";
          @import "@/styles/_mixins.scss";
        `,
      },
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

步骤4:入口文件引入Element Plus

复制代码
// src/main.ts(补充Element Plus引入)
import { createApp } from 'vue';
import App from './App.vue';
import '@/styles/main.scss';
import { initTheme } from '@/utils/theme';
// 引入Element Plus样式(无需单独引入css,已通过SCSS注入)
import ElementPlus from 'element-plus';

initTheme();

const

app.use(ElementPlus); // 全局引入Element Plus(按需引入可省略此步) app.mount('#app');

注意事项

  • 若使用按需引入(推荐),无需在main.ts中全局引入ElementPlus,unplugin-auto-import和unplugin-vue-components会自动引入使用到的组件和样式;

  • 覆盖Element Plus变量时,需确保变量名与官方一致(参考Element Plus官方SCSS变量文档),避免覆盖失效;

  • 额外自定义组件样式时,建议使用组件库自带的类名(如.el-button、.el-input),避免使用深度选择器(::v-deep),提升样式优先级的同时保证兼容性。


4. SCSS优化技巧:压缩CSS、消除冗余、提升编译速度

大型项目中,SCSS样式文件往往体积较大,若不进行优化,会导致打包体积增大、页面加载变慢、编译速度下降,影响开发效率和用户体验。

4.1 消除样式冗余:精简代码,避免重复

样式冗余是项目开发中最常见的问题,尤其是多团队协作时,容易出现重复样式、无用样式,以下是针对性解决方案。

技巧1:复用混合器和自定义函数

将高频使用的样式逻辑(如按钮样式、卡片样式、间距控制)封装为混合器,将复杂计算逻辑封装为自定义函数,避免重复书写,同时保证样式统一。

示例:封装全局阴影混合器,适配不同场景的阴影需求:

复制代码
// src/styles/_mixins.scss(新增阴影混合器)
@mixin shadow($depth: 1) {
  @if $depth == 1 {
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); // 浅阴影(卡片默认)
  } @else if $depth == 2 {
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08); // 中阴影(弹窗、下拉菜单)
  } @else if $depth == 3 {
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12); // 深阴影(模态框)
  }
}

// 使用示例
.card {
  @include shadow(1); // 卡片浅阴影
}
.modal {
  @include shadow(3); // 模态框深阴影
}

技巧2:使用@extend继承样式

对于结构相似、仅少量样式不同的元素,使用@extend继承基础样式,减少重复代码。注意:@extend适合继承静态样式,不适合带参数的动态样式(动态样式用混合器)。

复制代码
// 基础文本样式(可继承)
.base-text {
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-color);
}

// 继承基础样式,补充特殊样式
.aux-text {
  @extend .base-text;
  color: var(--aux-text-color); // 仅修改文本色
}

.title-text {
  @extend .base-text;
  font-size: 16px;
  font-weight: 600; // 仅修改字体大小和粗细
}

技巧3:删除无用样式(使用工具检测)

项目迭代过程中,容易出现无用样式(如删除组件后未删除对应的样式),可使用工具检测并删除,减少样式体积:

  • 开发环境:使用purgecss(Vite插件:vite-plugin-purgecss),自动检测未使用的样式并删除;

  • 代码审查:定期使用stylelint检查冗余样式,规范开发习惯。

复制代码
// vite.config.ts(配置purgecss插件)
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import PurgeCSS from 'vite-plugin-purgecss';

export default defineConfig({
  plugins: [
    vue(),
    // 配置purgecss,自动删除无用样式
    PurgeCSS({
      content: ['./index.html', './src/**/*.vue'], // 检测的文件路径
      safelist: ['html', 'body'], // 保留的样式类名(避免误删全局样式)
    }),
  ],
});

4.2 压缩CSS:减小打包体积

SCSS编译为CSS后,可通过压缩工具减小文件体积,提升页面加载速度,Vite默认支持CSS压缩,可通过配置优化压缩效果。

复制代码
// vite.config.ts(配置CSS压缩)
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  build: {
    minify: 'terser', // 开启压缩(默认开启,生产环境生效)
    cssCodeSplit: true, // 拆分CSS文件(按组件拆分,避免单个CSS文件过大)
    rollupOptions: {
      output: {
        // 对CSS文件进行哈希命名,便于缓存
        assetFileNames: {
          css: 'css/[name].[hash].css',
        },
      },
    },
  },
  css: {
    minify: true, // 开启CSS压缩
  },
});

4.3 提升SCSS编译速度:优化工程化配置

大型项目中,SCSS文件较多时,编译速度会明显下降,可通过以下配置优化,提升开发效率。

技巧1:模块化拆分,避免大文件

将SCSS文件按功能拆分(如变量、混合器、函数、主题、组件库适配),避免单个文件过大,减少编译时的解析时间(参考前文的工程化目录结构)。

技巧2:合理使用@use和@import

SCSS中@use@import更高效(@use不会重复引入文件),建议优先使用@use引入文件,尤其是全局变量、混合器等高频使用的文件。

复制代码
// 推荐使用@use(不重复引入)
@use './_variables.scss' as vars;
@use './_mixins.scss' as mixins;
​
// 不推荐大量使用@import(可能重复引入)
// @import './_variables.scss';
// @import './_mixins.scss';

技巧3:缓存编译结果

使用Vite的缓存机制,缓存SCSS编译结果,避免每次开发时重新编译所有SCSS文件,Vite默认开启缓存,可通过配置优化缓存策略:

复制代码
// vite.config.ts(优化缓存)
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
​
export default defineConfig({
  plugins: [vue()],
  cacheDir: './node_modules/.vite/scss-cache', // 自定义SCSS缓存目录
  optimizeDeps: {
    include: ['@/styles/_variables.scss', '@/styles/_mixins.scss'], // 提前优化依赖
  },
});

5. SCSS最佳实践

5.1 目录结构规范化(统一标准)

延续前文的目录结构,进一步细化,适配多团队协作(如按业务模块拆分样式),确保每个团队都能清晰找到对应文件:

复制代码
src/styles/
├── core/                // 核心样式(全局通用,不随业务变化)
│   ├── _variables.scss  // 全局公共变量(字体、圆角、间距等)
│   ├── _mixins.scss     // 全局混合器(按钮、阴影、响应式等)
│   ├── _functions.scss  // 自定义函数(颜色转换、尺寸计算等)
│   └── _reset.scss      // 基础样式重置(统一浏览器默认样式)
├── themes/              // 主题相关(全局主题切换)
│   ├── _light.scss      // 亮色主题变量
│   ├── _dark.scss       // 暗色主题变量
│   └── _theme-map.scss  // 主题映射表
├── component-lib/       // 组件库适配(按组件库拆分)
│   ├── element-plus/    // Element Plus适配
│   │   └── _variables.scss
│   └── ant-design-vue/  // Ant Design Vue适配
│       └── _variables.scss
├── business/            // 业务模块样式(按团队/业务拆分)
│   ├── user/            // 用户模块(用户团队维护)
│   │   └── _user.scss
│   ├── order/           // 订单模块(订单团队维护)
│   │   └── _order.scss
│   └── common/          // 业务公共样式(多团队共用)
│       └── _business-common.scss
└── main.scss            // 样式入口(统一引入所有核心样式)

5.2 命名规范(避免冲突)

多团队协作时,样式类名容易冲突,需制定统一的命名规范,推荐使用「BEM命名规范」(Block-Element-Modifier),清晰区分组件、元素和状态。

复制代码
// BEM命名规范:Block__Element--Modifier
// Block:组件名(如user-card)
// Element:组件内元素(如title、content)
// Modifier:组件状态(如disabled、active)
​
// 示例:用户卡片组件样式
.user-card { // Block:用户卡片
  @include mixins.shadow(1);
  background-color: var(--card-bg-color);
  border-radius: vars.$border-radius-base;
  padding: vars.$spacing-md;
​
  &__title { // Element:卡片标题
    font-size: 16px;
    font-weight: 600;
    color: var(--title-color);
  }
​
  &__content { // Element:卡片内容
    @include mixins.base-text;
    margin-top: vars.$spacing-sm;
  }
​
  &--disabled { // Modifier:禁用状态
    opacity: 0.7;
    cursor: not-allowed;
  }
​
  &--active { // Modifier:选中状态
    border: 1px solid var(--primary-color);
  }
}

5.3 变量管理规范化(统一配置)

  • 全局公共变量(如字体、圆角、间距)统一放在core/_variables.scss,由架构团队维护,不允许各团队随意修改;

  • 业务模块变量(如业务专属颜色)放在对应业务目录下,命名需加业务前缀(如$order-primary-color),避免与全局变量冲突;

  • 主题变量统一放在themes目录,由UI团队维护,确保主题色、样式统一。

5.4 代码审查与规范校验(强制约束)

通过工具强制约束SCSS代码规范,避免不规范代码提交,提升代码质量:

  1. 使用stylelint配置SCSS规范(如禁止使用!important、强制使用BEM命名、禁止重复样式);

  2. 配置pre-commit钩子(如husky),提交代码时自动校验SCSS规范,不通过则无法提交;

  3. 定期开展代码审查,检查样式冗余、命名不规范等问题,形成迭代优化机制。

复制代码
// .stylelintrc.js(SCSS规范配置)
module.exports = {
  root: true,
  plugins: ['stylelint-scss'],
  rules: {
    // 禁止使用!important
    'declaration-no-important': true,
    // 强制使用BEM命名规范(简化版)
    'selector-class-pattern': '^[a-z0-9_-]+(__[a-z0-9_-]+)?(--[a-z0-9_-]+)?$',
    // 禁止重复样式
    'no-duplicate-selectors': true,
    // 禁止空规则
    'no-empty-source': true,
    // SCSS相关规则
    'scss/at-import-no-partial-leading-underscore': null,
    'scss/at-import-partial-extension': ['always', { extension: 'scss' }],
  },
};

5.5 版本控制与迭代(追溯变更)

多团队协作时,SCSS文件的变更需做好版本控制,避免冲突和误修改:

  • 使用Git进行版本控制,每次修改SCSS文件时,提交信息需清晰(如「feat: 新增订单模块卡片样式」「fix: 修复按钮hover样式异常」);

  • 核心样式文件(如全局变量、混合器)的修改,需经过架构团队审核,避免影响所有团队;

  • 定期同步样式变更,告知所有团队,确保各团队使用的样式版本一致。

相关推荐
lizhongxuan42 分钟前
Claude Code 防上下文爆炸:源码级深度解析
前端·后端
柳杉2 小时前
震惊!字符串还能这么玩!
前端·javascript
是上好佳佳佳呀3 小时前
【前端(五)】CSS 知识梳理:浮动与定位
前端·css
wefly20173 小时前
纯前端架构深度解析:jsontop.cn,JSON 格式化与全栈开发效率平台
java·前端·python·架构·正则表达式·json·php
我命由我123455 小时前
React - 类组件 setState 的 2 种写法、LazyLoad、useState
前端·javascript·react.js·html·ecmascript·html5·js
自由生长20245 小时前
IndexedDB的观察
前端
IT_陈寒6 小时前
Vite热更新坑了我三天,原来配置要这么写
前端·人工智能·后端
斯班奇的好朋友阿法法6 小时前
离线ollama导入Qwen3.5-9B.Q8_0.gguf模型
开发语言·前端·javascript
掘金一周6 小时前
每月固定续订,但是token根本不够用,掘友们有无算力焦虑啊 | 沸点周刊 4.2
前端·aigc·openai
小村儿6 小时前
连载加餐01-claude code 源码泄漏 ---一起吃透 Claude Code,告别 AI coding 迷茫
前端·后端·ai编程