类与样式绑定
数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式。因为 class
和 style
都是 attribute,我们可以和其他 attribute 一样使用 v-bind
将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 class
和 style
的 v-bind
用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。
绑定 HTML class
绑定对象
我们可以给 :class
(v-bind:class
的缩写) 传递一个对象来动态切换 class:
<template>
<div :class="{ active: isActive }"></div>
</template>
<script setup>
import { ref } from 'vue'
const isActive = ref(true)
</script>
上面的语法表示 active
这个 class 存在与否将取决于数据属性 isActive
的真假值。
你可以在对象中传入更多字段来动态切换多个 class。此外,:class
指令也可以与普通的 class
attribute 共存:
<template>
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>
</template>
<script setup>
import { ref } from 'vue'
const isActive = ref(true)
const hasError = ref(false)
</script>
渲染的结果会是:
<div class="static active"></div>
当 isActive
或者 hasError
变化时,class 列表将相应地更新。例如,如果 hasError
的值为 true
,class 列表将变为 "static active text-danger"
。
绑定的对象并不一定需要写成内联字面量的形式:
<template>
<div :class="classObject"></div>
</template>
<script setup>
import { reactive } from 'vue'
const classObject = reactive({
active: true,
'text-danger': false
})
</script>
这也会渲染出相同的结果。我们也可以绑定一个返回对象的计算属性:
<template>
<div :class="classObject"></div>
</template>
<script setup>
import { ref, computed } from 'vue'
const isActive = ref(true)
const error = ref(null)
const classObject = computed(() => ({
active: isActive.value && !error.value,
'text-danger': error.value && error.value.type === 'fatal'
}))
</script>
绑定数组
我们可以把一个数组传给 :class
,以应用一个 class 列表:
<template>
<div :class="[activeClass, errorClass]"></div>
</template>
<script setup>
import { ref } from 'vue'
const activeClass = ref('active')
const errorClass = ref('text-danger')
</script>
这会渲染出:
<div class="active text-danger"></div>
如果你想根据条件切换列表中的 class,可以使用三元表达式:
<template>
<div :class="[isActive ? activeClass : '', errorClass]"></div>
</template>
这样写将始终添加 errorClass
,但是只有在 isActive
为真时才添加 activeClass
。
不过,当有多个条件 class 时这样写有些冗长。所以在数组语法中也可以使用对象语法:
<template>
<div :class="[{ active: isActive }, errorClass]"></div>
</template>
在组件上使用
当你在带有单个根元素的自定义组件上使用 class
attribute 时,这些 class 将被添加到该元素中。此元素上的现有 class 将不会被覆盖。
例如,如果你声明了这样一个组件:
<!-- MyComponent.vue -->
<template>
<p class="foo bar">Hi!</p>
</template>
然后在使用它的时候添加一些 class:
<template>
<MyComponent class="baz boo" />
</template>
HTML 将被渲染为:
<p class="foo bar baz boo">Hi!</p>
对于带数据绑定 class 也同样适用:
<template>
<MyComponent :class="{ active: isActive }" />
</template>
当 isActive
为真时,HTML 将被渲染成:
<p class="foo bar active">Hi!</p>
如果你的组件有多个根元素,你需要定义哪些部分将接收这个 class。可以使用 $attrs
组件属性执行此操作:
<!-- MyComponent.vue -->
<template>
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
</template>
绑定内联样式
绑定对象
:style
支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style
属性:
<template>
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
</template>
<script setup>
import { ref } from 'vue'
const activeColor = ref('red')
const fontSize = ref(30)
</script>
尽管推荐使用 camelCase,但 :style
也支持 kebab-cased 形式的 CSS 属性 key (对应其 CSS 中的实际名称):
<template>
<div :style="{ 'font-size': fontSize + 'px' }"></div>
</template>
直接绑定到一个样式对象通常更好,这会让模板更清洁:
<template>
<div :style="styleObject"></div>
</template>
<script setup>
import { reactive } from 'vue'
const styleObject = reactive({
color: 'red',
fontSize: '13px'
})
</script>
同样的,如果样式对象需要更复杂的逻辑,也可以使用返回样式对象的计算属性。
绑定数组
我们可以给 :style
绑定一个包含多个样式对象的数组。这些对象会被合并后应用到同一元素上:
<template>
<div :style="[baseStyles, overridingStyles]"></div>
</template>
<script setup>
import { reactive } from 'vue'
const baseStyles = reactive({
color: 'red',
fontSize: '13px'
})
const overridingStyles = reactive({
fontWeight: 'bold'
})
</script>
自动前缀
当你在 :style
中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他们加上相应的前缀。Vue 是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。
样式多值
你可以对一个样式属性提供多个 (不同前缀的) 值,举例来说:
<template>
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
</template>
数组仅会渲染浏览器支持的最后一个值。在这个示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex
。
实际应用示例
主题切换器
<template>
<div class="theme-switcher" :class="themeClass">
<div class="header">
<h1>主题切换演示</h1>
<div class="theme-controls">
<button
v-for="theme in themes"
:key="theme.name"
@click="currentTheme = theme.name"
:class="{ active: currentTheme === theme.name }"
class="theme-btn"
>
{{ theme.label }}
</button>
</div>
</div>
<div class="content">
<div class="card">
<h2>卡片标题</h2>
<p>这是一个演示卡片,用来展示不同主题下的样式效果。</p>
<button class="primary-btn">主要按钮</button>
<button class="secondary-btn">次要按钮</button>
</div>
<div class="stats">
<div class="stat-item">
<div class="stat-number">1,234</div>
<div class="stat-label">用户数</div>
</div>
<div class="stat-item">
<div class="stat-number">5,678</div>
<div class="stat-label">订单数</div>
</div>
<div class="stat-item">
<div class="stat-number">9,012</div>
<div class="stat-label">收入</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentTheme = ref('light')
const themes = [
{ name: 'light', label: '浅色' },
{ name: 'dark', label: '深色' },
{ name: 'blue', label: '蓝色' },
{ name: 'green', label: '绿色' }
]
const themeClass = computed(() => `theme-${currentTheme.value}`)
</script>
<style scoped>
.theme-switcher {
min-height: 100vh;
padding: 20px;
transition: all 0.3s ease;
}
/* 浅色主题 */
.theme-light {
background-color: #ffffff;
color: #333333;
}
.theme-light .header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.theme-light .card {
background-color: #ffffff;
border: 1px solid #dee2e6;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.theme-light .primary-btn {
background-color: #007bff;
color: white;
}
.theme-light .secondary-btn {
background-color: #6c757d;
color: white;
}
/* 深色主题 */
.theme-dark {
background-color: #121212;
color: #ffffff;
}
.theme-dark .header {
background-color: #1e1e1e;
border-bottom: 1px solid #333333;
}
.theme-dark .card {
background-color: #1e1e1e;
border: 1px solid #333333;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.theme-dark .primary-btn {
background-color: #0d6efd;
color: white;
}
.theme-dark .secondary-btn {
background-color: #495057;
color: white;
}
/* 蓝色主题 */
.theme-blue {
background-color: #e3f2fd;
color: #0d47a1;
}
.theme-blue .header {
background-color: #bbdefb;
border-bottom: 1px solid #90caf9;
}
.theme-blue .card {
background-color: #ffffff;
border: 1px solid #90caf9;
box-shadow: 0 2px 4px rgba(33,150,243,0.2);
}
.theme-blue .primary-btn {
background-color: #1976d2;
color: white;
}
.theme-blue .secondary-btn {
background-color: #42a5f5;
color: white;
}
/* 绿色主题 */
.theme-green {
background-color: #e8f5e8;
color: #1b5e20;
}
.theme-green .header {
background-color: #c8e6c9;
border-bottom: 1px solid #a5d6a7;
}
.theme-green .card {
background-color: #ffffff;
border: 1px solid #a5d6a7;
box-shadow: 0 2px 4px rgba(76,175,80,0.2);
}
.theme-green .primary-btn {
background-color: #388e3c;
color: white;
}
.theme-green .secondary-btn {
background-color: #66bb6a;
color: white;
}
/* 通用样式 */
.header {
padding: 20px;
margin-bottom: 20px;
border-radius: 8px;
transition: all 0.3s ease;
}
.header h1 {
margin: 0 0 15px 0;
}
.theme-controls {
display: flex;
gap: 10px;
}
.theme-btn {
padding: 8px 16px;
border: 2px solid transparent;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
background-color: rgba(0,0,0,0.1);
}
.theme-btn.active {
border-color: currentColor;
font-weight: bold;
}
.content {
display: grid;
gap: 20px;
}
.card {
padding: 20px;
border-radius: 8px;
transition: all 0.3s ease;
}
.card h2 {
margin: 0 0 10px 0;
}
.card p {
margin: 0 0 20px 0;
line-height: 1.6;
}
.primary-btn,
.secondary-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
margin-right: 10px;
transition: all 0.3s ease;
}
.primary-btn:hover,
.secondary-btn:hover {
opacity: 0.8;
transform: translateY(-1px);
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-item {
text-align: center;
padding: 20px;
border-radius: 8px;
background-color: rgba(0,0,0,0.05);
transition: all 0.3s ease;
}
.stat-number {
font-size: 2em;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9em;
opacity: 0.8;
}
</style>
动态样式控制器
<template>
<div class="style-controller">
<h2>动态样式控制器</h2>
<div class="controls">
<div class="control-group">
<label>背景颜色:</label>
<input v-model="styles.backgroundColor" type="color">
</div>
<div class="control-group">
<label>文字颜色:</label>
<input v-model="styles.color" type="color">
</div>
<div class="control-group">
<label>字体大小: {{ styles.fontSize }}px</label>
<input
v-model.number="styles.fontSize"
type="range"
min="12"
max="48"
>
</div>
<div class="control-group">
<label>边框宽度: {{ styles.borderWidth }}px</label>
<input
v-model.number="styles.borderWidth"
type="range"
min="0"
max="10"
>
</div>
<div class="control-group">
<label>边框颜色:</label>
<input v-model="styles.borderColor" type="color">
</div>
<div class="control-group">
<label>圆角: {{ styles.borderRadius }}px</label>
<input
v-model.number="styles.borderRadius"
type="range"
min="0"
max="50"
>
</div>
<div class="control-group">
<label>内边距: {{ styles.padding }}px</label>
<input
v-model.number="styles.padding"
type="range"
min="0"
max="50"
>
</div>
<div class="control-group">
<label>阴影模糊: {{ styles.shadowBlur }}px</label>
<input
v-model.number="styles.shadowBlur"
type="range"
min="0"
max="20"
>
</div>
<div class="control-group">
<label>旋转角度: {{ styles.rotation }}deg</label>
<input
v-model.number="styles.rotation"
type="range"
min="-180"
max="180"
>
</div>
<div class="control-group">
<label>缩放: {{ styles.scale }}</label>
<input
v-model.number="styles.scale"
type="range"
min="0.5"
max="2"
step="0.1"
>
</div>
</div>
<div class="preview">
<div
class="preview-box"
:style="computedStyles"
>
<h3>预览区域</h3>
<p>这是一个动态样式演示区域,你可以通过左侧的控制器来调整样式。</p>
<button class="demo-btn">示例按钮</button>
</div>
</div>
<div class="code-output">
<h3>生成的CSS代码:</h3>
<pre><code>{{ cssCode }}</code></pre>
<button @click="copyCSS" class="copy-btn">复制CSS</button>
</div>
</div>
</template>
<script setup>
import { reactive, computed } from 'vue'
const styles = reactive({
backgroundColor: '#ffffff',
color: '#333333',
fontSize: 16,
borderWidth: 1,
borderColor: '#cccccc',
borderRadius: 8,
padding: 20,
shadowBlur: 4,
rotation: 0,
scale: 1
})
const computedStyles = computed(() => ({
backgroundColor: styles.backgroundColor,
color: styles.color,
fontSize: `${styles.fontSize}px`,
border: `${styles.borderWidth}px solid ${styles.borderColor}`,
borderRadius: `${styles.borderRadius}px`,
padding: `${styles.padding}px`,
boxShadow: `0 2px ${styles.shadowBlur}px rgba(0,0,0,0.1)`,
transform: `rotate(${styles.rotation}deg) scale(${styles.scale})`,
transition: 'all 0.3s ease'
}))
const cssCode = computed(() => {
return `{
background-color: ${styles.backgroundColor};
color: ${styles.color};
font-size: ${styles.fontSize}px;
border: ${styles.borderWidth}px solid ${styles.borderColor};
border-radius: ${styles.borderRadius}px;
padding: ${styles.padding}px;
box-shadow: 0 2px ${styles.shadowBlur}px rgba(0,0,0,0.1);
transform: rotate(${styles.rotation}deg) scale(${styles.scale});
transition: all 0.3s ease;
}`
})
function copyCSS() {
navigator.clipboard.writeText(cssCode.value).then(() => {
alert('CSS代码已复制到剪贴板!')
})
}
</script>
<style scoped>
.style-controller {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
.style-controller h2 {
grid-column: 1 / -1;
text-align: center;
margin-bottom: 20px;
}
.controls {
display: grid;
gap: 15px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.control-group label {
font-weight: bold;
font-size: 14px;
}
.control-group input[type="color"] {
width: 50px;
height: 30px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.control-group input[type="range"] {
width: 100%;
}
.preview {
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
background-color: #f8f9fa;
border-radius: 8px;
padding: 20px;
}
.preview-box {
max-width: 300px;
text-align: center;
}
.preview-box h3 {
margin: 0 0 10px 0;
}
.preview-box p {
margin: 0 0 15px 0;
line-height: 1.6;
}
.demo-btn {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.code-output {
grid-column: 1 / -1;
margin-top: 20px;
}
.code-output h3 {
margin-bottom: 10px;
}
.code-output pre {
background-color: #f8f9fa;
padding: 15px;
border-radius: 6px;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.4;
}
.copy-btn {
margin-top: 10px;
padding: 8px 16px;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.copy-btn:hover {
background-color: #218838;
}
@media (max-width: 768px) {
.style-controller {
grid-template-columns: 1fr;
}
}
</style>
状态指示器
<template>
<div class="status-indicators">
<h2>状态指示器</h2>
<div class="indicator-grid">
<!-- 服务器状态 -->
<div class="indicator-card">
<h3>服务器状态</h3>
<div
class="status-indicator"
:class="serverStatusClass"
>
<div class="status-dot"></div>
<span class="status-text">{{ serverStatus.text }}</span>
</div>
<div class="status-controls">
<button
v-for="status in serverStatuses"
:key="status.value"
@click="serverStatus = status"
:class="{ active: serverStatus.value === status.value }"
class="status-btn"
>
{{ status.text }}
</button>
</div>
</div>
<!-- 网络连接 -->
<div class="indicator-card">
<h3>网络连接</h3>
<div
class="connection-bar"
:style="connectionBarStyle"
>
<div class="connection-fill"></div>
</div>
<div class="connection-info">
<span>信号强度: {{ connectionStrength }}%</span>
<input
v-model.number="connectionStrength"
type="range"
min="0"
max="100"
class="strength-slider"
>
</div>
</div>
<!-- 电池状态 -->
<div class="indicator-card">
<h3>电池状态</h3>
<div class="battery-indicator">
<div
class="battery-level"
:style="batteryStyle"
:class="batteryClass"
></div>
<div class="battery-percentage">{{ batteryLevel }}%</div>
</div>
<div class="battery-controls">
<button @click="batteryLevel = Math.max(0, batteryLevel - 10)">-10%</button>
<button @click="batteryLevel = Math.min(100, batteryLevel + 10)">+10%</button>
<label>
<input v-model="isCharging" type="checkbox">
充电中
</label>
</div>
</div>
<!-- 下载进度 -->
<div class="indicator-card">
<h3>下载进度</h3>
<div class="progress-container">
<div
class="progress-bar"
:style="progressStyle"
:class="{ completed: downloadProgress >= 100 }"
>
<div class="progress-text">{{ downloadProgress }}%</div>
</div>
</div>
<div class="progress-controls">
<button @click="startDownload" :disabled="isDownloading">开始下载</button>
<button @click="pauseDownload" :disabled="!isDownloading">暂停</button>
<button @click="resetDownload">重置</button>
</div>
</div>
<!-- 温度计 -->
<div class="indicator-card">
<h3>温度监控</h3>
<div class="thermometer">
<div
class="temperature-fill"
:style="temperatureStyle"
:class="temperatureClass"
></div>
<div class="temperature-scale">
<div v-for="mark in temperatureMarks" :key="mark" class="scale-mark">
{{ mark }}°
</div>
</div>
</div>
<div class="temperature-display">
<span class="temp-value">{{ temperature }}°C</span>
<input
v-model.number="temperature"
type="range"
min="-10"
max="50"
class="temp-slider"
>
</div>
</div>
<!-- 磁盘使用率 -->
<div class="indicator-card">
<h3>磁盘使用率</h3>
<div class="disk-usage">
<div
v-for="disk in disks"
:key="disk.name"
class="disk-item"
>
<div class="disk-info">
<span class="disk-name">{{ disk.name }}</span>
<span class="disk-percentage">{{ disk.used }}%</span>
</div>
<div class="disk-bar">
<div
class="disk-fill"
:style="{ width: disk.used + '%' }"
:class="getDiskClass(disk.used)"
></div>
</div>
</div>
</div>
<button @click="updateDiskUsage" class="refresh-btn">刷新数据</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
// 服务器状态
const serverStatuses = [
{ value: 'online', text: '在线', class: 'status-online' },
{ value: 'offline', text: '离线', class: 'status-offline' },
{ value: 'maintenance', text: '维护中', class: 'status-maintenance' },
{ value: 'error', text: '错误', class: 'status-error' }
]
const serverStatus = ref(serverStatuses[0])
const serverStatusClass = computed(() => serverStatus.value.class)
// 网络连接
const connectionStrength = ref(75)
const connectionBarStyle = computed(() => ({
'--connection-width': `${connectionStrength.value}%`,
'--connection-color': connectionStrength.value > 70 ? '#28a745' :
connectionStrength.value > 30 ? '#ffc107' : '#dc3545'
}))
// 电池状态
const batteryLevel = ref(65)
const isCharging = ref(false)
const batteryStyle = computed(() => ({
width: `${batteryLevel.value}%`
}))
const batteryClass = computed(() => {
if (isCharging.value) return 'charging'
if (batteryLevel.value <= 20) return 'low'
if (batteryLevel.value <= 50) return 'medium'
return 'high'
})
// 下载进度
const downloadProgress = ref(0)
const isDownloading = ref(false)
let downloadInterval = null
const progressStyle = computed(() => ({
width: `${downloadProgress.value}%`
}))
function startDownload() {
if (downloadProgress.value >= 100) {
downloadProgress.value = 0
}
isDownloading.value = true
downloadInterval = setInterval(() => {
downloadProgress.value += Math.random() * 3
if (downloadProgress.value >= 100) {
downloadProgress.value = 100
isDownloading.value = false
clearInterval(downloadInterval)
}
}, 100)
}
function pauseDownload() {
isDownloading.value = false
clearInterval(downloadInterval)
}
function resetDownload() {
isDownloading.value = false
downloadProgress.value = 0
clearInterval(downloadInterval)
}
// 温度监控
const temperature = ref(25)
const temperatureMarks = [0, 10, 20, 30, 40]
const temperatureStyle = computed(() => {
const percentage = ((temperature.value + 10) / 60) * 100
return { height: `${Math.max(0, Math.min(100, percentage))}%` }
})
const temperatureClass = computed(() => {
if (temperature.value >= 35) return 'hot'
if (temperature.value >= 25) return 'warm'
if (temperature.value >= 15) return 'cool'
return 'cold'
})
// 磁盘使用率
const disks = reactive([
{ name: 'C:', used: 45 },
{ name: 'D:', used: 78 },
{ name: 'E:', used: 23 },
{ name: 'F:', used: 92 }
])
function getDiskClass(usage) {
if (usage >= 90) return 'critical'
if (usage >= 70) return 'warning'
return 'normal'
}
function updateDiskUsage() {
disks.forEach(disk => {
disk.used = Math.floor(Math.random() * 100)
})
}
</script>
<style scoped>
.status-indicators {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.indicator-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.indicator-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.indicator-card h3 {
margin: 0 0 15px 0;
color: #333;
}
/* 服务器状态 */
.status-indicator {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-online .status-dot {
background-color: #28a745;
}
.status-offline .status-dot {
background-color: #6c757d;
animation: none;
}
.status-maintenance .status-dot {
background-color: #ffc107;
}
.status-error .status-dot {
background-color: #dc3545;
animation: blink 1s infinite;
}
.status-controls {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.status-btn {
padding: 4px 8px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.status-btn.active {
background-color: #007bff;
color: white;
border-color: #007bff;
}
/* 网络连接 */
.connection-bar {
height: 20px;
background-color: #e9ecef;
border-radius: 10px;
overflow: hidden;
position: relative;
margin-bottom: 10px;
}
.connection-fill {
height: 100%;
width: var(--connection-width);
background-color: var(--connection-color);
transition: all 0.3s ease;
border-radius: 10px;
}
.strength-slider {
width: 100%;
margin-top: 5px;
}
/* 电池状态 */
.battery-indicator {
position: relative;
width: 100px;
height: 50px;
border: 2px solid #333;
border-radius: 4px;
margin: 10px auto;
}
.battery-indicator::after {
content: '';
position: absolute;
right: -6px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 20px;
background-color: #333;
border-radius: 0 2px 2px 0;
}
.battery-level {
height: 100%;
transition: all 0.3s ease;
border-radius: 2px;
}
.battery-level.high {
background-color: #28a745;
}
.battery-level.medium {
background-color: #ffc107;
}
.battery-level.low {
background-color: #dc3545;
}
.battery-level.charging {
background: linear-gradient(45deg, #28a745, #20c997);
animation: charging 2s infinite;
}
.battery-percentage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 12px;
font-weight: bold;
color: #333;
}
.battery-controls {
display: flex;
gap: 5px;
justify-content: center;
margin-top: 10px;
flex-wrap: wrap;
}
.battery-controls button {
padding: 4px 8px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
/* 下载进度 */
.progress-container {
background-color: #e9ecef;
border-radius: 4px;
overflow: hidden;
margin-bottom: 15px;
}
.progress-bar {
height: 30px;
background: linear-gradient(90deg, #007bff, #0056b3);
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
position: relative;
}
.progress-bar.completed {
background: linear-gradient(90deg, #28a745, #20c997);
}
.progress-controls {
display: flex;
gap: 5px;
}
.progress-controls button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.progress-controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 温度计 */
.thermometer {
display: flex;
align-items: flex-end;
height: 200px;
margin: 20px 0;
}
.temperature-fill {
width: 30px;
background: linear-gradient(to top, #007bff, #0056b3);
border-radius: 15px 15px 0 0;
transition: all 0.3s ease;
margin-right: 10px;
}
.temperature-fill.cold {
background: linear-gradient(to top, #17a2b8, #138496);
}
.temperature-fill.cool {
background: linear-gradient(to top, #28a745, #20c997);
}
.temperature-fill.warm {
background: linear-gradient(to top, #ffc107, #e0a800);
}
.temperature-fill.hot {
background: linear-gradient(to top, #dc3545, #c82333);
}
.temperature-scale {
display: flex;
flex-direction: column-reverse;
justify-content: space-between;
height: 100%;
font-size: 12px;
}
.temp-slider {
width: 100%;
margin-top: 10px;
}
/* 磁盘使用率 */
.disk-item {
margin-bottom: 15px;
}
.disk-info {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 14px;
}
.disk-bar {
height: 8px;
background-color: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.disk-fill {
height: 100%;
transition: all 0.3s ease;
}
.disk-fill.normal {
background-color: #28a745;
}
.disk-fill.warning {
background-color: #ffc107;
}
.disk-fill.critical {
background-color: #dc3545;
}
.refresh-btn {
width: 100%;
padding: 8px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
}
/* 动画 */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes blink {
0%, 50% {
opacity: 1;
}
51%, 100% {
opacity: 0;
}
}
@keyframes charging {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
</style>
最佳实践
优先使用 class 绑定:对于样式切换,优先使用 class 绑定而不是内联样式,这样更易维护。
使用计算属性:对于复杂的 class 或 style 逻辑,使用计算属性来保持模板的简洁。
避免内联样式的滥用:内联样式应该只用于真正动态的值,静态样式应该写在 CSS 中。
保持一致性:在项目中保持 class 命名和样式组织的一致性。
性能考虑:避免在样式绑定中使用复杂的计算,这可能影响渲染性能。
下一步
现在你已经掌握了 Vue 中的类与样式绑定,让我们继续学习:
类与样式绑定是构建动态用户界面的重要技能,掌握它将让你能够创建更加生动和交互性强的应用!