shadcn/ui + Tailwind:交易儀表板設計系統
快速導覽:本文深入探討 shadcn/ui 與 Tailwind CSS 在交易儀表板的應用,從設計系統建構到組件客製化,提供專業級金融介面開發的完整指南。預計閱讀時間 13 分鐘。
為什麼選擇 shadcn/ui + Tailwind?
在金融交易介面的開發中,設計系統的選擇直接影響開發效率與用戶體驗。傳統的 UI 組件庫(如 Material-UI、Ant Design)雖然功能豐富,但往往難以客製化,且 bundle 體積龐大。
shadcn/ui 採用獨特的「Copy-Paste 組件」模式,將組件原始碼直接放入你的專案,提供無限的客製化彈性。搭配 Tailwind CSS 的原子化樣式,成為現代金融介面開發的最佳組合。
與傳統組件庫比較
| 特性 | Material-UI | Ant Design | shadcn/ui + Tailwind |
|:---|:---|:---|:---|
| 客製化彈性 | 中等(需覆蓋樣式)| 中等(主題系統)| 極高(直接修改原始碼)|
| Bundle 大小 | 大(200KB+)| 大(300KB+)| 小(僅使用組件)|
| 學習曲線 | 中等 | 中等 | 低(Tailwind 直觀)|
| 設計一致性 | Material Design | Ant Design | 完全客製|
| TypeScript | 支援 | 支援 | 原生優先|
| 暗色模式 | 需設定 | 需設定 | 內建支援|
關鍵洞察:shadcn/ui 不是「組件庫」,而是「組件集合」。你擁有每個組件的完整控制權,這對需要高度品牌識別的金融產品至關重要。
設計系統基礎:Tailwind 配置
色彩系統設計
交易介面的色彩不僅是美學,更承載資訊傳達的功能。上漲/下跌、盈利/虧損、警示/安全 —— 每個顏色都有明確的語義。
// tailwind.config.ts
import type { Config } from 'tailwindcss';
const config: Config = {
darkMode: ['class'],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
extend: {
colors: {
// 主色調 - 品牌識別
primary: {
DEFAULT: '#0ea5e9', // Sky 500
foreground: '#ffffff',
50: '#f0f9ff',
100: '#e0f2fe',
500: '#0ea5e9',
600: '#0284c7',
900: '#0c4a6e',
},
// 交易語義色彩
trade: {
up: '#22c55e', // Green 500 - 上漲/盈利
down: '#ef4444', // Red 500 - 下跌/虧損
neutral: '#6b7280', // Gray 500 - 持平
warning: '#f59e0b', // Amber 500 - 警告
info: '#3b82f6', // Blue 500 - 資訊
},
// 背景層次
background: {
DEFAULT: '#0f172a', // Slate 900 - 主背景
secondary: '#1e293b', // Slate 800 - 卡片
tertiary: '#334155', // Slate 700 - 邊框/分隔
},
// 文字層次
foreground: {
DEFAULT: '#f8fafc', // Slate 50 - 主要文字
secondary: '#cbd5e1', // Slate 300 - 次要文字
muted: '#64748b', // Slate 500 - 輔助文字
},
// 邊框與分隔
border: '#334155',
input: '#475569',
ring: '#0ea5e9',
},
// 間距系統 - 8px 基準
spacing: {
'4.5': '1.125rem', // 18px
'13': '3.25rem', // 52px
},
// 字體系統
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'Fira Code', 'monospace'], // 數字顯示
},
// 動畫
animation: {
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'flash-green': 'flashGreen 0.5s ease-out',
'flash-red': 'flashRed 0.5s ease-out',
},
keyframes: {
flashGreen: {
'0%, 100%': { backgroundColor: 'transparent' },
'50%': { backgroundColor: 'rgba(34, 197, 94, 0.2)' },
},
flashRed: {
'0%, 100%': { backgroundColor: 'transparent' },
'50%': { backgroundColor: 'rgba(239, 68, 68, 0.2)' },
},
},
},
},
plugins: [require('tailwindcss-animate')],
};
export default config;
CSS 變數與暗色模式
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 222 47% 11%;
--foreground: 210 40% 98%;
--card: 217 33% 17%;
--card-foreground: 210 40% 98%;
--popover: 222 47% 11%;
--popover-foreground: 210 40% 98%;
--primary: 199 89% 48%;
--primary-foreground: 0 0% 100%;
--secondary: 217 33% 17%;
--secondary-foreground: 210 40% 98%;
--muted: 217 33% 17%;
--muted-foreground: 215 20% 65%;
--accent: 217 33% 17%;
--accent-foreground: 210 40% 98%;
--destructive: 0 84% 60%;
--destructive-foreground: 210 40% 98%;
--border: 217 33% 25%;
--input: 217 33% 25%;
--ring: 199 89% 48%;
--radius: 0.5rem;
/* 交易語義 */
--trade-up: 142 71% 45%;
--trade-down: 0 84% 60%;
--trade-warning: 38 92% 50%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
}
shadcn/ui 組件客製化
基礎組件安裝
# 初始化 shadcn/ui
npx shadcn-ui@latest init
# 安裝交易介面常用組件
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add table
npx shadcn-ui@latest add tabs
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add dropdown-menu
npx shadcn-ui@latest add select
npx shadcn-ui@latest add input
npx shadcn-ui@latest add badge
npx shadcn-ui@latest add skeleton
npx shadcn-ui@latest add tooltip
npx shadcn-ui@latest add toast
n```
### 交易專用組件客製化
#### 價格變動顯示組件
// components/ui/price-change.tsx
import { cn } from '@/lib/utils';
import { ArrowUp, ArrowDown, Minus } from 'lucide-react';
interface PriceChangeProps {
value: number; // 變動值
percentage?: number; // 變動百分比
showIcon?: boolean;
className?: string;
decimalPlaces?: number;
}
export function PriceChange({
value,
percentage,
showIcon = true,
className,
decimalPlaces = 2,
}: PriceChangeProps) {
const isPositive = value > 0;
const isNeutral = value === 0;
const colorClass = isPositive
? 'text-trade-up'
: isNeutral
? 'text-trade-neutral'
: 'text-trade-down';
const Icon = isPositive ? ArrowUp : isNeutral ? Minus : ArrowDown;
return (
<div className={cn('flex items-center gap-1 font-mono', colorClass, className)}>
{showIcon && <Icon className="h-4 w-4" />}
<span>
{isPositive ? '+' : ''}
{value.toFixed(decimalPlaces)}
{percentage !== undefined && (
<span className="ml-1 text-xs">
({isPositive ? '+' : ''}
{percentage.toFixed(decimalPlaces)}%)
</span>
)}
</span>
</div>
);
}
#### 交易對卡片組件
// components/features/trading-pair-card.tsx
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { PriceChange } from '@/components/ui/price-change';
import { Badge } from '@/components/ui/badge';
interface TradingPairCardProps {
symbol: string;
baseAsset: string;
quoteAsset: string;
price: number;
change24h: number;
change24hPercent: number;
volume24h: number;
high24h: number;
low24h: number;
isFavorite?: boolean;
onClick?: () => void;
}
export function TradingPairCard({
symbol,
baseAsset,
quoteAsset,
price,
change24h,
change24hPercent,
volume24h,
high24h,
low24h,
isFavorite,
onClick,
}: TradingPairCardProps) {
return (
<Card
className="cursor-pointer transition-all hover:border-primary/50 hover:shadow-lg"
onClick={onClick}
>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10">
<span className="text-xs font-bold text-primary">{baseAsset[0]}</span>
</div>
<div>
<h4 className="font-semibold">{baseAsset}/{quoteAsset}</h4>
<p className="text-xs text-muted-foreground">{symbol}</p>
</div>
</div>
{isFavorite && (
<Badge variant="secondary" className="h-6 w-6 p-0">
★
</Badge>
)}
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-baseline justify-between">
<span className="text-2xl font-bold font-mono">
{price.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 8,
})}
</span>
<PriceChange
value={change24h}
percentage={change24hPercent}
/>
</div>
<div className="grid grid-cols-2 gap-2 text-xs text-muted-foreground">
<div>
<span>24h 高: </span>
<span className="font-mono text-foreground">{high24h.toLocaleString()}</span>
</div>
<div>
<span>24h 低: </span>
<span className="font-mono text-foreground">{low24h.toLocaleString()}</span>
</div>
<div className="col-span-2">
<span>24h 成交量: </span>
<span className="font-mono text-foreground">
{(volume24h / 1e6).toFixed(2)}M
</span>
</div>
</div>
</CardContent>
</Card>
);
}
---
## 交易儀表板佈局系統
### 網格佈局設計
// components/layout/dashboard-grid.tsx
import { cn } from '@/lib/utils';
import { ReactNode } from 'react';
interface DashboardGridProps {
children: ReactNode;
className?: string;
}
export function DashboardGrid({ children, className }: DashboardGridProps) {
return (
<div className={cn(
'grid gap-4 p-4',
'grid-cols-1',
'md:grid-cols-2',
'lg:grid-cols-3',
'xl:grid-cols-4',
className
)}>
{children}
</div>
);
}
// 可調整大小的儀表板網格
interface ResizableGridProps {
layouts: Layout[];
children: ReactNode[];
}
export function ResizableDashboardGrid({ layouts, children }: ResizableGridProps) {
// 使用 react-grid-layout 實作
return (
<ResponsiveGridLayout
className="layout"
layouts={layouts}
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
rowHeight={60}
draggableHandle=".drag-handle"
>
{children}
</ResponsiveGridLayout>
);
}
### 側邊導航設計
// components/layout/sidebar.tsx
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
LayoutDashboard,
Bot,
TrendingUp,
BarChart3,
Settings,
Wallet
} from 'lucide-react';
const navItems = [
{ icon: LayoutDashboard, label: '儀表板', href: '/dashboard' },
{ icon: Bot, label: '機器人', href: '/bots' },
{ icon: TrendingUp, label: '策略市集', href: '/strategies' },
{ icon: BarChart3, label: '分析', href: '/analysis' },
{ icon: Wallet, label: '資產', href: '/wallet' },
{ icon: Settings, label: '設定', href: '/settings' },
];
export function Sidebar() {
return (
<div className="flex h-full w-64 flex-col border-r bg-card">
<div className="flex h-16 items-center border-b px-6">
<Logo className="h-8 w-8" />
<span className="ml-3 text-lg font-bold">Sentinel</span>
</div>
<ScrollArea className="flex-1 py-4">
<nav className="space-y-1 px-3">
{navItems.map((item) => (
<NavLink
key={item.href}
to={item.href}
className={({ isActive }) =>
cn(
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
)
}
>
<item.icon className="h-5 w-5" />
{item.label}
</NavLink>
))}
</nav>
</ScrollArea>
<div className="border-t p-4">
<UserProfile />
</div>
</div>
);
}
---
## 響應式設計策略
### 斷點系統
// 設計系統斷點
const breakpoints = {
sm: '640px', // 手機橫向
md: '768px', // 平板
lg: '1024px', // 小桌機
xl: '1280px', // 桌機
'2xl': '1536px', // 大螢幕
};
// 交易介面專用響應式規則
/*
- < 768px: 單欄佈局,簡化圖表
- 768px - 1024px: 雙欄佈局
- 1024px - 1280px: 三欄佈局
- > 1280px: 完整多欄佈局
*/
### 響應式表格
// components/ui/responsive-table.tsx
import { cn } from '@/lib/utils';
interface ResponsiveTableProps {
children: React.ReactNode;
className?: string;
}
export function ResponsiveTable({ children, className }: ResponsiveTableProps) {
return (
<div className={cn('w-full overflow-auto', className)}>
<table className="w-full caption-bottom text-sm">
{children}
</table>
</div>
);
}
// 手機端卡片視圖
export function MobileCardView({ data, renderCard }: MobileCardViewProps) {
return (
<div className="grid gap-4 md:hidden">
{data.map((item, index) => (
<div key={index}>{renderCard(item)}</div>
))}
</div>
);
}
---
## 動畫與微互動
### 價格閃爍動畫
// hooks/usePriceFlash.ts
import { useState, useEffect } from 'react';
export function usePriceFlash(price: number) {
const [flash, setFlash] = useState<'up' | 'down' | null>(null);
const [prevPrice, setPrevPrice] = useState(price);
useEffect(() => {
if (price > prevPrice) {
setFlash('up');
} else if (price < prevPrice) {
setFlash('down');
}
setPrevPrice(price);
const timer = setTimeout(() => setFlash(null), 500);
return () => clearTimeout(timer);
}, [price]);
return flash;
}
// 使用
function PriceDisplay({ price }: { price: number }) {
const flash = usePriceFlash(price);
return (
<span
className={cn(
'font-mono text-2xl font-bold transition-colors',
flash === 'up' && 'animate-flash-green',
flash === 'down' && 'animate-flash-red'
)}
>
{price.toFixed(2)}
</span>
);
}
### 載入狀態骨架屏
// components/ui/skeleton.tsx(shadcn/ui 內建)
import { Skeleton } from '@/components/ui/skeleton';
export function TradingPairCardSkeleton() {
return (
<Card>
<CardHeader className="flex flex-row items-center gap-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-[150px]" />
<Skeleton className="h-4 w-[100px]" />
</div>
</CardHeader>
<CardContent className="space-y-4">
<Skeleton className="h-8 w-[200px]" />
<div className="grid grid-cols-2 gap-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
</CardContent>
</Card>
);
}
---
## 實戰案例:Sentinel Bot 設計系統
### 設計原則
Sentinel Design Principles:
├── 清晰優先 (Clarity First)
│ └── 價格、盈虧等關鍵數據一目瞭然
├── 效率導向 (Efficiency Oriented)
│ └── 減少操作步驟,一鍵完成常用功能
├── 專業質感 (Professional Aesthetic)
│ └── 深色主題,金融級視覺品質
└── 響應即時 (Instant Feedback)
└── 價格變動、操作結果即時視覺回饋
### 組件使用統計
| 組件類型 | 使用次數 | 客製化程度 |
|:---|:---:|:---:|
| Button | 120+ | 中等(交易語義色彩)|
| Card | 80+ | 高(多種變體)|
| Table | 25+ | 高(響應式、排序)|
| Dialog | 30+ | 中等 |
| Badge | 200+ | 高(狀態標籤)|
| Chart | 15+ | 高(Recharts 封裝)|
---
## 常見問題 FAQ
### Q1: shadcn/ui 與 Radix UI 的關係?
**A**: shadcn/ui **基於** Radix UI 的無頭組件(headless),加上 Tailwind 樣式。你獲得的是:
- Radix 的無障礙與鍵盤導航
- Tailwind 的彈性樣式
- 完全可客製化的原始碼
### Q2: 如何更新 shadcn/ui 組件?
**A**: 因為組件在你的專案中,更新需手動:
重新安裝特定組件(會覆蓋)
npx shadcn-ui@latest add button -o
或手動比較官方更新
https://ui.shadcn.com/docs/components/button
### Q3: Tailwind 的 bundle 會很大嗎?
**A**: 不會。Tailwind 使用 PurgeCSS,**只打包實際使用的類別**。生產環境通常 < 10KB。
### Q4: 如何實作主題切換?
**A**: shadcn/ui 內建支援:
// providers/theme-provider.tsx
import { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'dark' | 'light' | 'system';
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState<Theme>('dark');
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
root.classList.add(theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
### Q5: 金融資料的特殊字體需求?
**A**: 數字建議使用等寬字體(monospace),確保對齊:
/ 價格顯示 /
.font-mono-tabular {
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum';
}
### Q6: 如何處理大量資料的效能?
**A**: 組合策略:
1. 虛擬滾動(react-window)
2. 分頁或無限滾動
3. 資料預取與快取
4. 骨架屏減少感知延遲
### Q7: shadcn/ui 適合大型團隊嗎?
**A**: 適合,但需建立規範:
- 組件客製化指南
- 設計 Token 文件
- Code Review 檢查清單
### Q8: 與 Figma 設計稿如何協作?
**A**: 建議流程:
1. Figma 設計 → 2. Tailwind 配置同步 → 3. shadcn/ui 組件實作 → 4. 設計系統文件
---
## 結論與行動建議
shadcn/ui + Tailwind CSS 是現代交易介面開發的最佳組合:
- ✅ 完全客製化控制
- ✅ 優秀的開發體驗
- ✅ 專業的視覺品質
- ✅ 輕量的 bundle 體積
### 立即行動
- [ ] 初始化 Tailwind + shadcn/ui
- [ ] 定義設計 Token(色彩、字體、間距)
- [ ] 安裝並客製化基礎組件
- [ ] 建立交易專用組件庫
- [ ] 實作響應式佈局系統
---
**延伸閱讀**:
- [shadcn/ui 官方文件](https://ui.shadcn.com/)
- [Tailwind CSS 文件](https://tailwindcss.com/docs)
- [Radix UI Primitives](https://www.radix-ui.com/)
---
**作者**:Sentinel Team
**最後更新**:2026-03-04
**設計驗證**:本文基於 Sentinel Bot 實際設計系統經驗
---
*正在建構交易介面設計系統?立即體驗 Sentinel Bot 的 shadcn/ui 驅動介面,或下載我們的設計系統模板快速開始。*
**[免費試用 Sentinel Bot](https://sentinel.redclawey.com/pricing)** | **[下載設計系統模板](https://sentinel.redclawey.com/pricing)** | **[設計諮詢](https://sentinel.redclawey.com/pricing)**
---
## 相關文章
### 同系列延伸閱讀
- [React 18 交易介面](./react-18-automated-trading-interface-guide.md) - React 生態整合
- [TypeScript 5 型別安全](./typescript-5-trading-type-safety-guide.md) - 型別安全開發
### 跨系列推薦
- [交易介面心理](../trading-psychology/trading-emotion-management-guide.md) - UI/UX 與交易心理