1. CSS样式基础
1.1 样式语法
<template>
<view class="container">
<view class="header" :class="{ active: isActive }">
<text class="title">标题</text>
</view>
<view class="content" :style="dynamicStyle">
<text>内容区域</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
isActive: true,
dynamicStyle: {
color: '#333',
fontSize: '16px'
}
}
}
}
</script>
<style lang="scss" scoped>
.container {
padding: 20rpx;
background-color: #f5f5f5;
.header {
height: 100rpx;
background-color: #fff;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
&.active {
background-color: #007aff;
.title {
color: #fff;
}
}
.title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
}
.content {
background-color: #fff;
padding: 30rpx;
border-radius: 10rpx;
min-height: 200rpx;
}
}
</style>
1.2 尺寸单位
/* UniApp支持的尺寸单位 */
.size-demo {
/* rpx: 响应式像素,推荐使用 */
width: 750rpx; /* 等于屏幕宽度 */
height: 100rpx;
/* px: 物理像素 */
border: 1px solid #ccc;
/* %: 百分比 */
margin: 5%;
/* vh/vw: 视口单位 */
min-height: 50vh;
/* rem/em: 相对单位 */
font-size: 1.2rem;
/* upx: 微信小程序兼容单位 */
padding: 20upx;
}
/* 响应式设计 */
@media screen and (max-width: 768rpx) {
.responsive {
font-size: 28rpx;
}
}
@media screen and (min-width: 769rpx) {
.responsive {
font-size: 32rpx;
}
}
1.3 Flex布局
/* Flex容器 */
.flex-container {
display: flex;
flex-direction: row; /* row | column | row-reverse | column-reverse */
justify-content: center; /* flex-start | flex-end | center | space-between | space-around | space-evenly */
align-items: center; /* flex-start | flex-end | center | baseline | stretch */
flex-wrap: wrap; /* nowrap | wrap | wrap-reverse */
gap: 20rpx; /* 项目间距 */
}
/* Flex项目 */
.flex-item {
flex: 1; /* flex-grow flex-shrink flex-basis */
flex-grow: 1; /* 放大比例 */
flex-shrink: 1; /* 缩小比例 */
flex-basis: auto; /* 基础大小 */
align-self: auto; /* auto | flex-start | flex-end | center | baseline | stretch */
}
/* 常用布局示例 */
.layout-examples {
/* 水平居中 */
.horizontal-center {
display: flex;
justify-content: center;
}
/* 垂直居中 */
.vertical-center {
display: flex;
align-items: center;
}
/* 完全居中 */
.full-center {
display: flex;
justify-content: center;
align-items: center;
}
/* 两端对齐 */
.space-between {
display: flex;
justify-content: space-between;
}
/* 等分布局 */
.equal-width {
display: flex;
.item {
flex: 1;
}
}
/* 固定宽度+自适应 */
.fixed-adaptive {
display: flex;
.fixed {
width: 200rpx;
flex-shrink: 0;
}
.adaptive {
flex: 1;
}
}
}
2. SCSS预处理器
2.1 变量和混入
// styles/variables.scss
// 颜色变量
$primary-color: #007aff;
$secondary-color: #5ac8fa;
$success-color: #4cd964;
$warning-color: #ff9500;
$error-color: #ff3b30;
$text-color: #333;
$text-light: #666;
$text-lighter: #999;
$border-color: #e5e5e5;
$background-color: #f5f5f5;
// 尺寸变量
$border-radius: 10rpx;
$border-radius-large: 20rpx;
$spacing-small: 10rpx;
$spacing-medium: 20rpx;
$spacing-large: 30rpx;
$font-size-small: 24rpx;
$font-size-medium: 28rpx;
$font-size-large: 32rpx;
$font-size-xlarge: 36rpx;
// 阴影变量
$shadow-light: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
$shadow-medium: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
$shadow-heavy: 0 8rpx 30rpx rgba(0, 0, 0, 0.2);
// 动画变量
$transition-fast: 0.2s;
$transition-medium: 0.3s;
$transition-slow: 0.5s;
// 混入(Mixins)
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
@mixin flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
@mixin ellipsis($lines: 1) {
@if $lines == 1 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} @else {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
}
@mixin button-style($bg-color, $text-color: #fff, $border-color: transparent) {
background-color: $bg-color;
color: $text-color;
border: 2rpx solid $border-color;
border-radius: $border-radius;
padding: 20rpx 40rpx;
font-size: $font-size-medium;
transition: all $transition-fast;
&:active {
opacity: 0.8;
transform: scale(0.98);
}
}
@mixin card-style {
background-color: #fff;
border-radius: $border-radius;
box-shadow: $shadow-light;
padding: $spacing-large;
margin-bottom: $spacing-medium;
}
@mixin responsive-font($mobile, $tablet: null, $desktop: null) {
font-size: $mobile;
@media screen and (min-width: 768rpx) {
@if $tablet {
font-size: $tablet;
}
}
@media screen and (min-width: 1024rpx) {
@if $desktop {
font-size: $desktop;
}
}
}
2.2 函数和工具类
// styles/functions.scss
// 颜色函数
@function lighten-color($color, $amount: 10%) {
@return lighten($color, $amount);
}
@function darken-color($color, $amount: 10%) {
@return darken($color, $amount);
}
@function alpha-color($color, $alpha: 0.5) {
@return rgba($color, $alpha);
}
// 尺寸转换函数
@function rpx-to-px($rpx) {
@return $rpx / 2 * 1px;
}
@function px-to-rpx($px) {
@return $px * 2 * 1rpx;
}
// styles/utilities.scss
// 工具类
.u-flex {
display: flex;
&-center {
@include flex-center;
}
&-between {
@include flex-between;
}
&-column {
flex-direction: column;
}
&-wrap {
flex-wrap: wrap;
}
&-1 {
flex: 1;
}
}
.u-text {
&-center {
text-align: center;
}
&-left {
text-align: left;
}
&-right {
text-align: right;
}
&-ellipsis {
@include ellipsis(1);
}
&-ellipsis-2 {
@include ellipsis(2);
}
&-ellipsis-3 {
@include ellipsis(3);
}
}
.u-color {
&-primary {
color: $primary-color;
}
&-success {
color: $success-color;
}
&-warning {
color: $warning-color;
}
&-error {
color: $error-color;
}
&-text {
color: $text-color;
}
&-light {
color: $text-light;
}
&-lighter {
color: $text-lighter;
}
}
.u-bg {
&-primary {
background-color: $primary-color;
}
&-white {
background-color: #fff;
}
&-gray {
background-color: $background-color;
}
}
.u-margin {
&-small {
margin: $spacing-small;
}
&-medium {
margin: $spacing-medium;
}
&-large {
margin: $spacing-large;
}
&-top {
&-small {
margin-top: $spacing-small;
}
&-medium {
margin-top: $spacing-medium;
}
&-large {
margin-top: $spacing-large;
}
}
&-bottom {
&-small {
margin-bottom: $spacing-small;
}
&-medium {
margin-bottom: $spacing-medium;
}
&-large {
margin-bottom: $spacing-large;
}
}
}
.u-padding {
&-small {
padding: $spacing-small;
}
&-medium {
padding: $spacing-medium;
}
&-large {
padding: $spacing-large;
}
}
.u-border {
&-radius {
border-radius: $border-radius;
}
&-radius-large {
border-radius: $border-radius-large;
}
&-bottom {
border-bottom: 1rpx solid $border-color;
}
}
.u-shadow {
&-light {
box-shadow: $shadow-light;
}
&-medium {
box-shadow: $shadow-medium;
}
&-heavy {
box-shadow: $shadow-heavy;
}
}
3. 主题系统
3.1 主题配置
// utils/theme.js
class ThemeManager {
constructor() {
this.currentTheme = 'light'
this.themes = {
light: {
name: 'light',
colors: {
primary: '#007aff',
secondary: '#5ac8fa',
success: '#4cd964',
warning: '#ff9500',
error: '#ff3b30',
background: '#ffffff',
surface: '#f5f5f5',
text: '#333333',
textSecondary: '#666666',
textLight: '#999999',
border: '#e5e5e5',
shadow: 'rgba(0, 0, 0, 0.1)'
},
statusBar: 'dark-content'
},
dark: {
name: 'dark',
colors: {
primary: '#0a84ff',
secondary: '#64d2ff',
success: '#30d158',
warning: '#ff9f0a',
error: '#ff453a',
background: '#000000',
surface: '#1c1c1e',
text: '#ffffff',
textSecondary: '#ebebf5',
textLight: '#8e8e93',
border: '#38383a',
shadow: 'rgba(255, 255, 255, 0.1)'
},
statusBar: 'light-content'
},
blue: {
name: 'blue',
colors: {
primary: '#1890ff',
secondary: '#40a9ff',
success: '#52c41a',
warning: '#faad14',
error: '#f5222d',
background: '#f0f8ff',
surface: '#e6f7ff',
text: '#001529',
textSecondary: '#314659',
textLight: '#8c8c8c',
border: '#d9d9d9',
shadow: 'rgba(24, 144, 255, 0.1)'
},
statusBar: 'dark-content'
}
}
this.init()
}
// 初始化主题
init() {
// 从本地存储获取主题设置
const savedTheme = uni.getStorageSync('theme')
if (savedTheme && this.themes[savedTheme]) {
this.currentTheme = savedTheme
} else {
// 根据系统主题自动设置
this.detectSystemTheme()
}
this.applyTheme(this.currentTheme)
}
// 检测系统主题
detectSystemTheme() {
try {
const systemInfo = uni.getSystemInfoSync()
if (systemInfo.theme === 'dark') {
this.currentTheme = 'dark'
}
} catch (error) {
console.log('无法检测系统主题:', error)
}
}
// 应用主题
applyTheme(themeName) {
if (!this.themes[themeName]) {
console.error('主题不存在:', themeName)
return
}
this.currentTheme = themeName
const theme = this.themes[themeName]
// 设置CSS变量
this.setCSSVariables(theme.colors)
// 设置状态栏样式
this.setStatusBarStyle(theme.statusBar)
// 保存到本地存储
uni.setStorageSync('theme', themeName)
// 触发主题变更事件
uni.$emit('themeChanged', {
theme: themeName,
colors: theme.colors
})
console.log('主题已切换到:', themeName)
}
// 设置CSS变量
setCSSVariables(colors) {
const root = document.documentElement || document.body
Object.keys(colors).forEach(key => {
const cssVar = `--theme-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`
root.style.setProperty(cssVar, colors[key])
})
}
// 设置状态栏样式
setStatusBarStyle(style) {
// #ifdef APP-PLUS
plus.navigator.setStatusBarStyle(style)
// #endif
// #ifdef MP-WEIXIN
wx.setNavigationBarColor({
frontColor: style === 'dark-content' ? '#000000' : '#ffffff',
backgroundColor: this.getThemeColor('background')
})
// #endif
}
// 切换主题
switchTheme(themeName) {
if (themeName === this.currentTheme) {
return
}
this.applyTheme(themeName)
}
// 切换到下一个主题
toggleTheme() {
const themeNames = Object.keys(this.themes)
const currentIndex = themeNames.indexOf(this.currentTheme)
const nextIndex = (currentIndex + 1) % themeNames.length
const nextTheme = themeNames[nextIndex]
this.switchTheme(nextTheme)
}
// 获取当前主题
getCurrentTheme() {
return this.currentTheme
}
// 获取主题颜色
getThemeColor(colorName) {
const theme = this.themes[this.currentTheme]
return theme ? theme.colors[colorName] : null
}
// 获取所有主题
getAllThemes() {
return Object.keys(this.themes).map(key => ({
name: key,
displayName: this.themes[key].name,
colors: this.themes[key].colors
}))
}
// 注册自定义主题
registerTheme(name, themeConfig) {
this.themes[name] = {
name,
...themeConfig
}
}
// 监听系统主题变化
watchSystemTheme() {
// #ifdef APP-PLUS
plus.globalEvent.addEventListener('newintent', () => {
this.detectSystemTheme()
this.applyTheme(this.currentTheme)
})
// #endif
}
}
// 创建主题管理实例
const themeManager = new ThemeManager()
export default themeManager
3.2 主题样式
// styles/theme.scss
// CSS变量定义
:root {
// 颜色变量
--theme-primary: #007aff;
--theme-secondary: #5ac8fa;
--theme-success: #4cd964;
--theme-warning: #ff9500;
--theme-error: #ff3b30;
--theme-background: #ffffff;
--theme-surface: #f5f5f5;
--theme-text: #333333;
--theme-text-secondary: #666666;
--theme-text-light: #999999;
--theme-border: #e5e5e5;
--theme-shadow: rgba(0, 0, 0, 0.1);
}
// 主题样式类
.theme-container {
background-color: var(--theme-background);
color: var(--theme-text);
transition: background-color 0.3s, color 0.3s;
}
.theme-surface {
background-color: var(--theme-surface);
color: var(--theme-text);
}
.theme-card {
background-color: var(--theme-background);
border: 1rpx solid var(--theme-border);
box-shadow: 0 4rpx 12rpx var(--theme-shadow);
border-radius: 12rpx;
padding: 24rpx;
}
.theme-button {
&-primary {
background-color: var(--theme-primary);
color: #ffffff;
border: none;
&:active {
background-color: var(--theme-primary);
opacity: 0.8;
}
}
&-secondary {
background-color: transparent;
color: var(--theme-primary);
border: 2rpx solid var(--theme-primary);
&:active {
background-color: var(--theme-primary);
color: #ffffff;
}
}
&-ghost {
background-color: transparent;
color: var(--theme-text);
border: 2rpx solid var(--theme-border);
&:active {
background-color: var(--theme-surface);
}
}
}
.theme-text {
&-primary {
color: var(--theme-text);
}
&-secondary {
color: var(--theme-text-secondary);
}
&-light {
color: var(--theme-text-light);
}
&-success {
color: var(--theme-success);
}
&-warning {
color: var(--theme-warning);
}
&-error {
color: var(--theme-error);
}
}
.theme-border {
border-color: var(--theme-border);
&-bottom {
border-bottom: 1rpx solid var(--theme-border);
}
&-top {
border-top: 1rpx solid var(--theme-border);
}
}
.theme-input {
background-color: var(--theme-background);
color: var(--theme-text);
border: 2rpx solid var(--theme-border);
border-radius: 8rpx;
padding: 16rpx 20rpx;
&:focus {
border-color: var(--theme-primary);
}
&::placeholder {
color: var(--theme-text-light);
}
}
// 主题切换动画
.theme-transition {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
// 深色主题特殊处理
@media (prefers-color-scheme: dark) {
.auto-theme {
--theme-primary: #0a84ff;
--theme-background: #000000;
--theme-surface: #1c1c1e;
--theme-text: #ffffff;
--theme-text-secondary: #ebebf5;
--theme-text-light: #8e8e93;
--theme-border: #38383a;
--theme-shadow: rgba(255, 255, 255, 0.1);
}
}
3.3 主题切换组件
<!-- components/ThemeSwitch.vue -->
<template>
<view class="theme-switch">
<view class="theme-switch-header">
<text class="theme-switch-title">主题设置</text>
</view>
<view class="theme-list">
<view
v-for="theme in themes"
:key="theme.name"
class="theme-item"
:class="{ active: currentTheme === theme.name }"
@click="switchTheme(theme.name)"
>
<view class="theme-preview">
<view
class="theme-color"
v-for="(color, index) in getPreviewColors(theme.colors)"
:key="index"
:style="{ backgroundColor: color }"
></view>
</view>
<text class="theme-name">{{ getThemeDisplayName(theme.name) }}</text>
<view v-if="currentTheme === theme.name" class="theme-check">
<text class="iconfont icon-check"></text>
</view>
</view>
</view>
<view class="theme-options">
<view class="option-item" @click="toggleAutoTheme">
<text class="option-label">跟随系统</text>
<switch
:checked="autoTheme"
@change="onAutoThemeChange"
color="var(--theme-primary)"
/>
</view>
</view>
</view>
</template>
<script>
import themeManager from '@/utils/theme.js'
export default {
name: 'ThemeSwitch',
data() {
return {
currentTheme: 'light',
autoTheme: false,
themes: []
}
},
mounted() {
this.init()
},
methods: {
init() {
this.currentTheme = themeManager.getCurrentTheme()
this.themes = themeManager.getAllThemes()
this.autoTheme = uni.getStorageSync('autoTheme') || false
// 监听主题变化
uni.$on('themeChanged', this.onThemeChanged)
},
switchTheme(themeName) {
if (this.autoTheme) {
this.autoTheme = false
uni.setStorageSync('autoTheme', false)
}
themeManager.switchTheme(themeName)
},
toggleAutoTheme() {
this.autoTheme = !this.autoTheme
this.onAutoThemeChange({ detail: { value: this.autoTheme } })
},
onAutoThemeChange(e) {
this.autoTheme = e.detail.value
uni.setStorageSync('autoTheme', this.autoTheme)
if (this.autoTheme) {
themeManager.detectSystemTheme()
}
},
onThemeChanged(data) {
this.currentTheme = data.theme
},
getPreviewColors(colors) {
return [colors.primary, colors.background, colors.surface, colors.text]
},
getThemeDisplayName(themeName) {
const names = {
light: '浅色',
dark: '深色',
blue: '蓝色'
}
return names[themeName] || themeName
}
},
beforeDestroy() {
uni.$off('themeChanged', this.onThemeChanged)
}
}
</script>
<style lang="scss" scoped>
.theme-switch {
padding: 40rpx;
&-header {
margin-bottom: 40rpx;
}
&-title {
font-size: 36rpx;
font-weight: bold;
color: var(--theme-text);
}
}
.theme-list {
margin-bottom: 60rpx;
}
.theme-item {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid var(--theme-border);
transition: all 0.3s;
&:last-child {
border-bottom: none;
}
&.active {
.theme-name {
color: var(--theme-primary);
font-weight: bold;
}
}
&:active {
background-color: var(--theme-surface);
}
}
.theme-preview {
display: flex;
margin-right: 30rpx;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx var(--theme-shadow);
}
.theme-color {
width: 20rpx;
height: 40rpx;
}
.theme-name {
flex: 1;
font-size: 32rpx;
color: var(--theme-text);
transition: all 0.3s;
}
.theme-check {
color: var(--theme-primary);
font-size: 32rpx;
}
.theme-options {
border-top: 1rpx solid var(--theme-border);
padding-top: 40rpx;
}
.option-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
}
.option-label {
font-size: 32rpx;
color: var(--theme-text);
}
</style>
4. 响应式设计
4.1 屏幕适配
// styles/responsive.scss
// 断点定义
$breakpoints: (
xs: 0,
sm: 576rpx,
md: 768rpx,
lg: 992rpx,
xl: 1200rpx,
xxl: 1600rpx
);
// 媒体查询混入
@mixin respond-to($breakpoint) {
@if map-has-key($breakpoints, $breakpoint) {
@media screen and (min-width: map-get($breakpoints, $breakpoint)) {
@content;
}
} @else {
@warn "Unknown breakpoint: #{$breakpoint}";
}
}
@mixin respond-between($min, $max) {
@if map-has-key($breakpoints, $min) and map-has-key($breakpoints, $max) {
@media screen and (min-width: map-get($breakpoints, $min)) and (max-width: map-get($breakpoints, $max) - 1rpx) {
@content;
}
}
}
@mixin respond-below($breakpoint) {
@if map-has-key($breakpoints, $breakpoint) {
@media screen and (max-width: map-get($breakpoints, $breakpoint) - 1rpx) {
@content;
}
}
}
// 响应式容器
.container {
width: 100%;
padding: 0 30rpx;
margin: 0 auto;
@include respond-to(sm) {
max-width: 540rpx;
}
@include respond-to(md) {
max-width: 720rpx;
}
@include respond-to(lg) {
max-width: 960rpx;
}
@include respond-to(xl) {
max-width: 1140rpx;
}
@include respond-to(xxl) {
max-width: 1320rpx;
}
}
// 响应式网格
.row {
display: flex;
flex-wrap: wrap;
margin: 0 -15rpx;
}
.col {
padding: 0 15rpx;
// 基础列
&-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
&-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
&-3 { flex: 0 0 25%; max-width: 25%; }
&-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
&-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
&-6 { flex: 0 0 50%; max-width: 50%; }
&-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
&-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
&-9 { flex: 0 0 75%; max-width: 75%; }
&-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
&-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
&-12 { flex: 0 0 100%; max-width: 100%; }
// 响应式列
@include respond-to(sm) {
&-sm-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
&-sm-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
&-sm-3 { flex: 0 0 25%; max-width: 25%; }
&-sm-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
&-sm-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
&-sm-6 { flex: 0 0 50%; max-width: 50%; }
&-sm-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
&-sm-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
&-sm-9 { flex: 0 0 75%; max-width: 75%; }
&-sm-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
&-sm-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
&-sm-12 { flex: 0 0 100%; max-width: 100%; }
}
@include respond-to(md) {
&-md-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
&-md-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
&-md-3 { flex: 0 0 25%; max-width: 25%; }
&-md-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
&-md-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
&-md-6 { flex: 0 0 50%; max-width: 50%; }
&-md-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
&-md-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
&-md-9 { flex: 0 0 75%; max-width: 75%; }
&-md-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
&-md-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
&-md-12 { flex: 0 0 100%; max-width: 100%; }
}
@include respond-to(lg) {
&-lg-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
&-lg-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
&-lg-3 { flex: 0 0 25%; max-width: 25%; }
&-lg-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
&-lg-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
&-lg-6 { flex: 0 0 50%; max-width: 50%; }
&-lg-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
&-lg-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
&-lg-9 { flex: 0 0 75%; max-width: 75%; }
&-lg-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
&-lg-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
&-lg-12 { flex: 0 0 100%; max-width: 100%; }
}
}
// 响应式工具类
.d-none {
display: none;
}
.d-block {
display: block;
}
.d-flex {
display: flex;
}
@include respond-to(sm) {
.d-sm-none { display: none; }
.d-sm-block { display: block; }
.d-sm-flex { display: flex; }
}
@include respond-to(md) {
.d-md-none { display: none; }
.d-md-block { display: block; }
.d-md-flex { display: flex; }
}
@include respond-to(lg) {
.d-lg-none { display: none; }
.d-lg-block { display: block; }
.d-lg-flex { display: flex; }
}
// 响应式字体
.responsive-text {
font-size: 28rpx;
@include respond-to(md) {
font-size: 32rpx;
}
@include respond-to(lg) {
font-size: 36rpx;
}
}
// 响应式间距
.responsive-padding {
padding: 20rpx;
@include respond-to(md) {
padding: 30rpx;
}
@include respond-to(lg) {
padding: 40rpx;
}
}
4.2 设备适配工具
// utils/device.js
class DeviceAdapter {
constructor() {
this.systemInfo = null
this.screenInfo = null
this.init()
}
// 初始化设备信息
init() {
try {
this.systemInfo = uni.getSystemInfoSync()
this.screenInfo = {
screenWidth: this.systemInfo.screenWidth,
screenHeight: this.systemInfo.screenHeight,
windowWidth: this.systemInfo.windowWidth,
windowHeight: this.systemInfo.windowHeight,
pixelRatio: this.systemInfo.pixelRatio,
statusBarHeight: this.systemInfo.statusBarHeight,
safeArea: this.systemInfo.safeArea,
safeAreaInsets: this.systemInfo.safeAreaInsets
}
} catch (error) {
console.error('获取设备信息失败:', error)
}
}
// 获取设备类型
getDeviceType() {
if (!this.systemInfo) return 'unknown'
const { platform, screenWidth } = this.systemInfo
if (platform === 'ios' || platform === 'android') {
if (screenWidth < 768) {
return 'mobile'
} else {
return 'tablet'
}
}
return 'desktop'
}
// 判断是否为移动设备
isMobile() {
return this.getDeviceType() === 'mobile'
}
// 判断是否为平板
isTablet() {
return this.getDeviceType() === 'tablet'
}
// 判断是否为桌面设备
isDesktop() {
return this.getDeviceType() === 'desktop'
}
// 判断是否为iOS
isIOS() {
return this.systemInfo?.platform === 'ios'
}
// 判断是否为Android
isAndroid() {
return this.systemInfo?.platform === 'android'
}
// 判断是否为微信小程序
isWeChat() {
// #ifdef MP-WEIXIN
return true
// #endif
// #ifndef MP-WEIXIN
return false
// #endif
}
// 判断是否为支付宝小程序
isAlipay() {
// #ifdef MP-ALIPAY
return true
// #endif
// #ifndef MP-ALIPAY
return false
// #endif
}
// 判断是否为H5
isH5() {
// #ifdef H5
return true
// #endif
// #ifndef H5
return false
// #endif
}
// 判断是否为App
isApp() {
// #ifdef APP-PLUS
return true
// #endif
// #ifndef APP-PLUS
return false
// #endif
}
// 获取状态栏高度
getStatusBarHeight() {
return this.systemInfo?.statusBarHeight || 0
}
// 获取安全区域
getSafeArea() {
return this.screenInfo?.safeArea || {
left: 0,
right: this.screenInfo?.screenWidth || 0,
top: this.getStatusBarHeight(),
bottom: this.screenInfo?.screenHeight || 0,
width: this.screenInfo?.screenWidth || 0,
height: (this.screenInfo?.screenHeight || 0) - this.getStatusBarHeight()
}
}
// 获取安全区域内边距
getSafeAreaInsets() {
return this.screenInfo?.safeAreaInsets || {
left: 0,
right: 0,
top: this.getStatusBarHeight(),
bottom: 0
}
}
// rpx转px
rpxToPx(rpx) {
if (!this.systemInfo) return rpx
return (rpx * this.systemInfo.screenWidth) / 750
}
// px转rpx
pxToRpx(px) {
if (!this.systemInfo) return px
return (px * 750) / this.systemInfo.screenWidth
}
// 获取屏幕尺寸分类
getScreenSize() {
if (!this.systemInfo) return 'medium'
const { screenWidth } = this.systemInfo
if (screenWidth < 576) {
return 'small'
} else if (screenWidth < 768) {
return 'medium'
} else if (screenWidth < 992) {
return 'large'
} else {
return 'xlarge'
}
}
// 获取设备信息
getDeviceInfo() {
return {
...this.systemInfo,
deviceType: this.getDeviceType(),
screenSize: this.getScreenSize(),
safeArea: this.getSafeArea(),
safeAreaInsets: this.getSafeAreaInsets()
}
}
// 设置页面样式
setPageStyle() {
const safeAreaInsets = this.getSafeAreaInsets()
// 设置CSS变量
const root = document.documentElement || document.body
if (root) {
root.style.setProperty('--status-bar-height', `${safeAreaInsets.top}px`)
root.style.setProperty('--safe-area-inset-top', `${safeAreaInsets.top}px`)
root.style.setProperty('--safe-area-inset-bottom', `${safeAreaInsets.bottom}px`)
root.style.setProperty('--safe-area-inset-left', `${safeAreaInsets.left}px`)
root.style.setProperty('--safe-area-inset-right', `${safeAreaInsets.right}px`)
}
}
}
// 创建设备适配实例
const deviceAdapter = new DeviceAdapter()
export default deviceAdapter
5. 动画效果
5.1 CSS动画
// styles/animations.scss
// 基础动画
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes slideInUp {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideInDown {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideInLeft {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes scaleIn {
from {
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
@keyframes scaleOut {
from {
transform: scale(1);
opacity: 1;
}
to {
transform: scale(0);
opacity: 0;
}
}
@keyframes bounce {
0%, 20%, 53%, 80%, 100% {
transform: translate3d(0, 0, 0);
}
40%, 43% {
transform: translate3d(0, -30rpx, 0);
}
70% {
transform: translate3d(0, -15rpx, 0);
}
90% {
transform: translate3d(0, -4rpx, 0);
}
}
@keyframes shake {
0%, 100% {
transform: translateX(0);
}
10%, 30%, 50%, 70%, 90% {
transform: translateX(-10rpx);
}
20%, 40%, 60%, 80% {
transform: translateX(10rpx);
}
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
// 动画类
.animate {
animation-duration: 0.3s;
animation-fill-mode: both;
&-fast {
animation-duration: 0.15s;
}
&-slow {
animation-duration: 0.6s;
}
&-infinite {
animation-iteration-count: infinite;
}
&-delay-1 {
animation-delay: 0.1s;
}
&-delay-2 {
animation-delay: 0.2s;
}
&-delay-3 {
animation-delay: 0.3s;
}
}
.fade-in {
animation-name: fadeIn;
}
.fade-out {
animation-name: fadeOut;
}
.slide-in-up {
animation-name: slideInUp;
}
.slide-in-down {
animation-name: slideInDown;
}
.slide-in-left {
animation-name: slideInLeft;
}
.slide-in-right {
animation-name: slideInRight;
}
.scale-in {
animation-name: scaleIn;
}
.scale-out {
animation-name: scaleOut;
}
.bounce {
animation-name: bounce;
}
.shake {
animation-name: shake;
}
.pulse {
animation-name: pulse;
}
.rotate {
animation-name: rotate;
}
// 过渡效果
.transition {
transition: all 0.3s ease;
&-fast {
transition-duration: 0.15s;
}
&-slow {
transition-duration: 0.6s;
}
&-ease-in {
transition-timing-function: ease-in;
}
&-ease-out {
transition-timing-function: ease-out;
}
&-ease-in-out {
transition-timing-function: ease-in-out;
}
&-linear {
transition-timing-function: linear;
}
}
// 悬停效果
.hover-scale {
transition: transform 0.3s ease;
&:active {
transform: scale(0.95);
}
}
.hover-lift {
transition: all 0.3s ease;
&:active {
transform: translateY(-4rpx);
box-shadow: 0 8rpx 25rpx rgba(0, 0, 0, 0.15);
}
}
.hover-glow {
transition: box-shadow 0.3s ease;
&:active {
box-shadow: 0 0 20rpx rgba(0, 122, 255, 0.5);
}
}
// 加载动画
.loading-spinner {
width: 40rpx;
height: 40rpx;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid var(--theme-primary);
border-radius: 50%;
animation: rotate 1s linear infinite;
}
.loading-dots {
display: flex;
align-items: center;
justify-content: center;
.dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background-color: var(--theme-primary);
margin: 0 4rpx;
animation: pulse 1.4s ease-in-out infinite both;
&:nth-child(1) {
animation-delay: -0.32s;
}
&:nth-child(2) {
animation-delay: -0.16s;
}
}
}
// 页面切换动画
.page-enter-active,
.page-leave-active {
transition: all 0.3s ease;
}
.page-enter {
transform: translateX(100%);
opacity: 0;
}
.page-leave-to {
transform: translateX(-100%);
opacity: 0;
}
// 列表项动画
.list-enter-active {
transition: all 0.3s ease;
}
.list-leave-active {
transition: all 0.3s ease;
position: absolute;
width: 100%;
}
.list-enter {
transform: translateY(30rpx);
opacity: 0;
}
.list-leave-to {
transform: translateY(-30rpx);
opacity: 0;
}
.list-move {
transition: transform 0.3s ease;
}
6. 总结
样式与主题是UniApp应用用户体验的重要组成部分:
- CSS基础:掌握UniApp支持的CSS特性和尺寸单位
- SCSS预处理器:使用变量、混入、函数提高样式开发效率
- 主题系统:实现多主题切换和深色模式支持
- 响应式设计:适配不同屏幕尺寸和设备类型
- 动画效果:丰富的CSS动画和过渡效果
- 设备适配:处理不同平台的样式差异
- 性能优化:合理使用CSS选择器和动画
- 最佳实践:建立统一的样式规范和组件库