Tailwind CSS
2025年5月14日大约 6 分鐘
學習目標
- 了解 Vue 組件中的樣式處理方式
- 掌握 Tailwind CSS 在 Vue 專案中的設置方法
- 學習如何合理組織 Tailwind 樣式
- 實現響應式設計並理解其在 Vue 中的應用
- 掌握 Tailwind 與 Vue 組件的最佳實踐
1. Tailwind CSS 在 Vue 專案中的設置
1.1 安裝與配置
步驟 1:安裝必要的套件
# 安裝 Tailwind CSS 及其依賴
npm install -D tailwindcss postcss autoprefixer
# 生成配置檔案
npx tailwindcss init -p
步驟 2:配置 Tailwind CSS
修改 tailwind.config.js
:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
步驟 3:創建主 CSS 文件
在 src/assets/main.css
中:
@tailwind base;
@tailwind components;
@tailwind utilities;
步驟 4:在 main.js 中引入 CSS
import { createApp } from 'vue'
import App from './App.vue'
import './assets/main.css' // 引入 Tailwind CSS
createApp(App).mount('#app')
1.2 使用 Tailwind 構建自定義組件
結合 @apply
指令:
<style>
/* 自定義 Tailwind 組件 */
.btn-primary {
@apply bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded;
}
.card {
@apply bg-white rounded-lg shadow-md p-6 m-4;
}
</style>
2. Tailwind 與 Vue 組件的整合實踐
2.1 基本使用方式
在模板中直接使用:
<template>
<div class="flex flex-col items-center p-4">
<h1 class="text-2xl font-bold text-blue-600 mb-4">使用 Tailwind 的標題</h1>
<button class="bg-green-500 hover:bg-green-700 text-white px-4 py-2 rounded">
點擊我
</button>
</div>
</template>
2.2 動態和條件樣式
結合 Vue 的條件渲染:
<template>
<div class="p-4">
<button
:class="[
'px-4 py-2 rounded font-bold',
isActive
? 'bg-green-500 text-white'
: 'bg-gray-300 text-gray-700'
]"
@click="toggleActive"
>
{{ isActive ? '啟用狀態' : '禁用狀態' }}
</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isActive = ref(false)
const toggleActive = () => {
isActive.value = !isActive.value
}
</script>
2.3 封裝通用組件
創建可重用的按鈕組件:
<!-- src/components/BaseButton.vue -->
<template>
<button
:class="[
'font-bold rounded transition',
sizeClasses,
variantClasses
]"
:disabled="disabled"
>
<slot></slot>
</button>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
variant: {
type: String,
default: 'primary',
validator: (value) => ['primary', 'secondary', 'danger'].includes(value)
},
size: {
type: String,
default: 'md',
validator: (value) => ['sm', 'md', 'lg'].includes(value)
},
disabled: {
type: Boolean,
default: false
}
})
const sizeClasses = computed(() => {
return {
'sm': 'px-2 py-1 text-sm',
'md': 'px-4 py-2 text-base',
'lg': 'px-6 py-3 text-lg'
}[props.size]
})
const variantClasses = computed(() => {
return {
'primary': 'bg-blue-500 hover:bg-blue-700 text-white',
'secondary': 'bg-gray-300 hover:bg-gray-400 text-gray-800',
'danger': 'bg-red-500 hover:bg-red-700 text-white'
}[props.variant]
})
</script>
使用封裝的按鈕:
<template>
<div>
<BaseButton>默認按鈕</BaseButton>
<BaseButton variant="secondary" size="sm">小型次級按鈕</BaseButton>
<BaseButton variant="danger" size="lg">大型危險按鈕</BaseButton>
</div>
</template>
<script setup>
import BaseButton from './components/BaseButton.vue'
</script>
3. 響應式設計與 Tailwind 斷點
3.1 Tailwind 斷點介紹
Tailwind CSS 提供的默認斷點:
斷點前綴 | 最小寬度 | CSS |
---|---|---|
sm | 640px | @media (min-width: 640px) { ... } |
md | 768px | @media (min-width: 768px) { ... } |
lg | 1024px | @media (min-width: 1024px) { ... } |
xl | 1280px | @media (min-width: 1280px) { ... } |
2xl | 1536px | @media (min-width: 1536px) { ... } |
3.2 實現響應式布局
基本響應式網格:
<template>
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div v-for="n in 8" :key="n" class="bg-blue-100 p-4 rounded shadow">
卡片 {{ n }}
</div>
</div>
</div>
</template>
響應式導航欄:
<template>
<nav class="bg-blue-600 text-white p-4">
<div class="container mx-auto flex flex-col md:flex-row md:justify-between md:items-center">
<div class="flex justify-between items-center">
<h1 class="text-xl font-bold">Logo</h1>
<button class="md:hidden" @click="isOpen = !isOpen">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path v-if="!isOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<ul class="mt-4 md:mt-0 md:flex space-y-2 md:space-y-0 md:space-x-4" :class="{ 'hidden': !isOpen, 'block': isOpen, 'md:flex': true }">
<li><a href="#" class="block hover:text-blue-200">首頁</a></li>
<li><a href="#" class="block hover:text-blue-200">關於</a></li>
<li><a href="#" class="block hover:text-blue-200">服務</a></li>
<li><a href="#" class="block hover:text-blue-200">聯繫我們</a></li>
</ul>
</div>
</nav>
</template>
<script setup>
import { ref } from 'vue'
const isOpen = ref(false)
</script>
4. 深入 Tailwind 自定義
4.1 擴展主題
在 tailwind.config.js
中自定義主題:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
'primary': '#1a56db',
'secondary': '#7c3aed',
'accent': '#0ea5e9',
},
fontFamily: {
sans: ['Noto Sans TC', 'sans-serif'],
},
spacing: {
'72': '18rem',
'84': '21rem',
'96': '24rem',
},
},
},
plugins: [],
}
4.2 暗黑模式
在 tailwind.config.js
中啟用暗黑模式:
/** @type {import('tailwindcss').Config} */
module.exports = {
// ...其他設置
darkMode: 'class', // 或 'media'
// ...
}
在 Vue 組件中實現:
<template>
<div :class="isDark ? 'dark' : ''">
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white min-h-screen p-4">
<button @click="toggleDarkMode" class="bg-gray-200 dark:bg-gray-700 p-2 rounded">
切換 {{ isDark ? '淺色' : '深色' }} 模式
</button>
<h1 class="text-2xl font-bold mt-4">內容標題</h1>
<p class="mt-2">這是一些示例內容,在暗黑模式下會有不同的顏色。</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
const isDark = ref(false)
// 檢查系統偏好和本地存儲
onMounted(() => {
// 從本地存儲加載設置
const savedTheme = localStorage.getItem('theme')
if (savedTheme) {
isDark.value = savedTheme === 'dark'
} else {
// 檢查系統偏好
isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches
}
})
// 監聽變更並保存
watch(isDark, (newValue) => {
localStorage.setItem('theme', newValue ? 'dark' : 'light')
})
// 切換暗黑模式
const toggleDarkMode = () => {
isDark.value = !isDark.value
}
</script>
5. 實作案例:構建響應式卡片組件
5.1 產品卡片組件
<!-- src/components/ProductCard.vue -->
<template>
<div class="group bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden transition-all duration-300 hover:shadow-xl">
<!-- 圖片區域 -->
<div class="relative overflow-hidden h-48">
<img
:src="product.image"
:alt="product.name"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
>
<!-- 折扣標籤 -->
<div
v-if="product.discount"
class="absolute top-2 right-2 bg-red-500 text-white text-xs font-bold px-2 py-1 rounded"
>
-{{ product.discount }}%
</div>
</div>
<!-- 內容區域 -->
<div class="p-4">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-2 truncate">
{{ product.name }}
</h3>
<!-- 價格區域 -->
<div class="flex items-center space-x-2 mb-3">
<span
v-if="product.originalPrice"
class="text-sm text-gray-500 line-through"
>
NT${{ product.originalPrice }}
</span>
<span class="text-lg font-bold text-blue-600 dark:text-blue-400">
NT${{ product.price }}
</span>
</div>
<!-- 評分 -->
<div class="flex items-center mb-3">
<div class="flex text-yellow-400">
<svg v-for="i in 5" :key="i" class="w-4 h-4" :class="i <= product.rating ? 'text-yellow-400' : 'text-gray-300'" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
</div>
<span class="text-xs text-gray-500 dark:text-gray-400 ml-1">({{ product.reviews }} 評論)</span>
</div>
<!-- 標籤 -->
<div class="flex flex-wrap gap-1 mb-3">
<span
v-for="(tag, index) in product.tags"
:key="index"
class="text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 px-2 py-1 rounded"
>
{{ tag }}
</span>
</div>
<!-- 按鈕 -->
<button class="w-full bg-blue-500 hover:bg-blue-600 text-white py-2 rounded-md transition-colors duration-300">
加入購物車
</button>
</div>
</div>
</template>
<script setup>
defineProps({
product: {
type: Object,
required: true
}
})
</script>
5.2 響應式產品網格
<!-- src/components/ProductGrid.vue -->
<template>
<div class="container mx-auto px-4 py-8">
<h2 class="text-2xl font-bold mb-6 text-gray-800 dark:text-white">熱門產品</h2>
<!-- 產品篩選器 -->
<div class="flex flex-wrap gap-2 mb-6">
<button
v-for="category in categories"
:key="category"
@click="selectedCategory = category"
class="px-3 py-1 rounded-full text-sm transition-colors duration-300"
:class="selectedCategory === category
? 'bg-blue-500 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'"
>
{{ category }}
</button>
</div>
<!-- 產品網格 -->
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<ProductCard
v-for="product in filteredProducts"
:key="product.id"
:product="product"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import ProductCard from './ProductCard.vue'
// 產品數據
const products = ref([
{
id: 1,
name: '超輕量運動鞋',
price: 2580,
originalPrice: 3600,
discount: 28,
image: 'https://via.placeholder.com/300x300',
rating: 4,
reviews: 127,
tags: ['運動', '休閒', '新品'],
category: '鞋子'
},
{
id: 2,
name: '透氣排汗T恤',
price: 790,
originalPrice: 990,
discount: 20,
image: 'https://via.placeholder.com/300x300',
rating: 5,
reviews: 89,
tags: ['排汗', '運動'],
category: '上衣'
},
{
id: 3,
name: '多功能運動背包',
price: 1250,
originalPrice: null,
discount: 0,
image: 'https://via.placeholder.com/300x300',
rating: 4,
reviews: 56,
tags: ['配件', '實用'],
category: '配件'
},
// ... 更多產品
])
// 分類
const categories = ['全部', '鞋子', '上衣', '配件']
const selectedCategory = ref('全部')
// 篩選產品
const filteredProducts = computed(() => {
if (selectedCategory.value === '全部') {
return products.value
}
return products.value.filter(product => product.category === selectedCategory.value)
})
</script>