教學 進階

shadcn/ui + Tailwind:交易儀表板設計系統|現代金融介面組件開發指南

Sentinel Team · 2026-03-04
shadcn/ui + Tailwind:交易儀表板設計系統|現代金融介面組件開發指南

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', // 大螢幕

};

// 交易介面專用響應式規則

/*

*/


### 響應式表格

// 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 與交易心理