Skip to content

类与样式绑定

数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式。因为 classstyle 都是 attribute,我们可以和其他 attribute 一样使用 v-bind 将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 classstylev-bind 用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。

绑定 HTML class

绑定对象

我们可以给 :class (v-bind:class 的缩写) 传递一个对象来动态切换 class:

vue
<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 共存:

vue
<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>

渲染的结果会是:

html
<div class="static active"></div>

isActive 或者 hasError 变化时,class 列表将相应地更新。例如,如果 hasError 的值为 true,class 列表将变为 "static active text-danger"

绑定的对象并不一定需要写成内联字面量的形式:

vue
<template>
  <div :class="classObject"></div>
</template>

<script setup>
import { reactive } from 'vue'

const classObject = reactive({
  active: true,
  'text-danger': false
})
</script>

这也会渲染出相同的结果。我们也可以绑定一个返回对象的计算属性:

vue
<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 列表:

vue
<template>
  <div :class="[activeClass, errorClass]"></div>
</template>

<script setup>
import { ref } from 'vue'

const activeClass = ref('active')
const errorClass = ref('text-danger')
</script>

这会渲染出:

html
<div class="active text-danger"></div>

如果你想根据条件切换列表中的 class,可以使用三元表达式:

vue
<template>
  <div :class="[isActive ? activeClass : '', errorClass]"></div>
</template>

这样写将始终添加 errorClass,但是只有在 isActive 为真时才添加 activeClass

不过,当有多个条件 class 时这样写有些冗长。所以在数组语法中也可以使用对象语法:

vue
<template>
  <div :class="[{ active: isActive }, errorClass]"></div>
</template>

在组件上使用

当你在带有单个根元素的自定义组件上使用 class attribute 时,这些 class 将被添加到该元素中。此元素上的现有 class 将不会被覆盖。

例如,如果你声明了这样一个组件:

vue
<!-- MyComponent.vue -->
<template>
  <p class="foo bar">Hi!</p>
</template>

然后在使用它的时候添加一些 class:

vue
<template>
  <MyComponent class="baz boo" />
</template>

HTML 将被渲染为:

html
<p class="foo bar baz boo">Hi!</p>

对于带数据绑定 class 也同样适用:

vue
<template>
  <MyComponent :class="{ active: isActive }" />
</template>

isActive 为真时,HTML 将被渲染成:

html
<p class="foo bar active">Hi!</p>

如果你的组件有多个根元素,你需要定义哪些部分将接收这个 class。可以使用 $attrs 组件属性执行此操作:

vue
<!-- MyComponent.vue -->
<template>
  <p :class="$attrs.class">Hi!</p>
  <span>This is a child component</span>
</template>

绑定内联样式

绑定对象

:style 支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style 属性:

vue
<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 中的实际名称):

vue
<template>
  <div :style="{ 'font-size': fontSize + 'px' }"></div>
</template>

直接绑定到一个样式对象通常更好,这会让模板更清洁:

vue
<template>
  <div :style="styleObject"></div>
</template>

<script setup>
import { reactive } from 'vue'

const styleObject = reactive({
  color: 'red',
  fontSize: '13px'
})
</script>

同样的,如果样式对象需要更复杂的逻辑,也可以使用返回样式对象的计算属性。

绑定数组

我们可以给 :style 绑定一个包含多个样式对象的数组。这些对象会被合并后应用到同一元素上:

vue
<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 是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。

样式多值

你可以对一个样式属性提供多个 (不同前缀的) 值,举例来说:

vue
<template>
  <div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
</template>

数组仅会渲染浏览器支持的最后一个值。在这个示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex

实际应用示例

主题切换器

vue
<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>

动态样式控制器

vue
<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>

状态指示器

vue
<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>

最佳实践

  1. 优先使用 class 绑定:对于样式切换,优先使用 class 绑定而不是内联样式,这样更易维护。

  2. 使用计算属性:对于复杂的 class 或 style 逻辑,使用计算属性来保持模板的简洁。

  3. 避免内联样式的滥用:内联样式应该只用于真正动态的值,静态样式应该写在 CSS 中。

  4. 保持一致性:在项目中保持 class 命名和样式组织的一致性。

  5. 性能考虑:避免在样式绑定中使用复杂的计算,这可能影响渲染性能。

下一步

现在你已经掌握了 Vue 中的类与样式绑定,让我们继续学习:

类与样式绑定是构建动态用户界面的重要技能,掌握它将让你能够创建更加生动和交互性强的应用!

基于 Vue.js 官方文档构建的学习宝典