在 Day 11,我們為 BaseSelect
建立了完整的測試覆蓋,確保元件在未來修改時不會出現回歸問題。今天我們要繼續我們元件的開發:Card 容器元件的設計與實作。
在現代 Web 應用中,Card 是最常見的 UI 模式之一。無論是:
Card 元件提供了一個統一的容器,讓內容有層次感、易於閱讀,並且保持一致的視覺風格。
我們將採用漸進式設計的理念,先建立一個基礎的 BaseCard
元件,然後基於它建立更專門的 ActionCard
元件。
<template>
<div class="pos-card">
<div v-if="title" class="pos-card-header">
<h3 class="pos-card-title">{{ title }}</h3>
<p v-if="subtitle" class="pos-card-subtitle">{{ subtitle }}</p>
</div>
<div class="pos-card-content">
<slot />
</div>
</div>
</template>
<script setup>
defineProps({
title: {
type: String,
default: '',
},
subtitle: {
type: String,
default: '',
},
});
</script>
<style scoped> ... </style>
設計特點:
<slot />
讓內容完全自定義基於 BaseCard
,我們建立專門用於功能操作的 ActionCard
:
<template>
<BaseCard>
<div :class="['pos-action-card', variant]" @click="$emit('click')">
<div class="action-icon">
<component :is="iconComponent" v-if="iconComponent" />
<div v-else v-html="iconPath"></div>
</div>
<div class="action-content">
<h3 class="action-title">{{ title }}</h3>
<p class="action-desc">{{ description }}</p>
<div class="action-stats" v-if="badge">
<span :class="['stat-badge', variant]">{{ badge }}</span>
</div>
</div>
<div class="action-arrow">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</div>
</div>
</BaseCard>
</template>
<div class="action-icon">
<component :is="iconComponent" v-if="iconComponent" />
<div v-else v-html="iconPath"></div>
</div>
Prototype 中 icon svg 的寫法:
<svg width="48" height="48" viewBox="0 0 24 24" fill="none">
<path
d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"
stroke="#3b82f6"
stroke-width="2"
fill="none"
></path>
</svg>
目前因為是從 prototype 轉過來的,icon 的部分未來其實可以進一步元件化為 ActionIcon
組件
<!-- ActionIcon.vue -->
<template>
<div class="action-icon" :class="[`action-icon--${variant}`, { 'action-icon--clickable': clickable }]">
<component :is="iconComponent" v-if="iconComponent" />
<div v-else v-html="iconPath"></div>
</div>
</template>
// ...
在 DashBoard.vue
中,我們將硬編碼的卡片替換為數據驅動的方式:
const actionCards = ref([
{
id: 'order-management',
title: '訂單管理',
description: '查詢、處理現有訂單',
variant: 'primary',
badge: '',
route: 'order-management',
iconPath: `
<svg width="48" height="48" viewBox="0 0 24 24" fill="none">
<circle cx="9" cy="21" r="1" fill="#EF4444"></circle>
<circle cx="20" cy="21" r="1" fill="#EF4444"></circle>
<path
d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"
stroke="#3b82f6"
stroke-width="2"
fill="none"
></path>
</svg>
`,
},
// ... 更多卡片配置
]);
這邊 Prototype 的寫法原本為用html寫死每張 ActionCard
的內容,所以重複的代碼非常多。
改為使用Vue重構,用 v-for
的方式渲染,將原本的 300+ 行重複 HTML 可以縮減為:
<div class="pos-actions-grid">
<ActionCard
v-for="card in actionCards"
:key="card.id"
:title="card.title"
:description="card.description"
:variant="card.variant"
:badge="card.badge"
:icon-path="card.iconPath"
@click="handleActionCardClick(card)"
/>
</div>
BaseCard
可以在任何需要卡片容器的地方使用ActionCard
專門處理功能操作卡片基於 BaseCard
的設計理念,我們可以輕鬆延伸出各種專門的卡片組件:
所有基於 BaseCard
的組件都遵循相同的設計原則:
BaseCard
提供一致的樣式通過 BaseCard
和 ActionCard
的設計,我們展示了 Vue 組件化的核心價值:
在下一篇文章中,我們將探討如何為這些 Card 元件建立完整的測試覆蓋,確保它們在未來修改時不會出現回歸問題。
明日,Day 13:[Componentの呼吸・陸之型] 測試Card - 驗證容器元件功能。心を燃やせ 🔥!