【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样式异常」);

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

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

相关推荐
用户12589343139602 小时前
边框渐变色的代码实现
前端
Csvn2 小时前
使用 React Hooks 优化组件性能的 5 个技巧
前端·javascript·react.js
weixin199701080162 小时前
开山网商品详情页前端性能优化实战
java·前端·python
HelloReader2 小时前
Flutter 状态管理实战搭建维基百科阅读器项目
前端
掘金者阿豪2 小时前
Joplin笔记告别局域网高效办公就靠cpolar
前端·后端
i建模2 小时前
npm国内镜像源加速
前端·npm·node.js
恋猫de小郭2 小时前
Cursor 自己做了模型 PK ,Cursor 里哪个模型性价比最高?
前端·人工智能·ai编程
张一凡932 小时前
告别 Redux 的繁琐,试试这个基于类模型的 React 状态管理库:easy-model
前端·react.js
巫山老妖2 小时前
OpenClaw 心跳机制实战:让 AI Agent 24 小时不停自主运行
java·前端