mcpField
D
dashboard

dashboard

github.com/starchild-ai-agent设计与 UI2988 次安装1 天前更新
代码仓库

关于 dashboard

dashboard 是用于构建数据仪表板的技能,采用 HTML、CSS 和 JavaScript。内容涵盖数据源调研、图表选择、响应式布局,以及通过 SSE 或 WebSocket 实现实时更新。

主要功能

  1. 01调研并评估数据源
  2. 02生成响应式 HTML/CSS/JS 仪表板
  3. 03集成图表库
  4. 04添加 SSE 或 WebSocket 更新
  5. 05遵循无障碍与设计规范

适用场景

  1. 01使用公共 API 构建仪表板
  2. 02展示频繁更新的指标
  3. 03为仪表板数据选择图表

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/starchild-ai-agent/official-skills --skill dashboard

安装后在 Claude Code 中输入/dashboard确认已加载。

详细介绍

仪表板创建技能

本技能将指导你如何创建专业的 HTML/CSS/JavaScript 仪表板,支持数据可视化、实时更新以及与免费数据源集成。基于 2025-2026 年的最佳实践和现代架构模式。

核心能力

  1. 数据源发现 - 调研并推荐适用于仪表板数据的免费 API
  2. 仪表板生成 - 创建响应式 HTML/CSS/JS 仪表板
  3. 可视化集成 - 使用现代库实现图表
  4. 实时更新 - 使用 SSE 或 WebSocket 添加实时数据流
  5. 最佳实践 - 遵循 2026 年的 UX/UI 和无障碍标准

1. 数据源发现

当用户需要为仪表板寻找数据源时,帮助他们找到合适的免费公共 API。

发现 Star Child 的数据源 ⭐ 请先检查这些

在搜索外部 API 之前,先了解 Star Child 已有的资源。不要想当然,先检查。

如何发现可用的数据源:

# 1. Check which APIs Star Child supports
read_file core/http_client.py | grep -A 15 "DEFAULT_PROXIED_APIS"

# 2. Check environment variables for configured keys
bash env | grep -i "api_key\|_key"

在 core/http_client.py 中要查找的内容:

  • DEFAULT_PROXIED_APIS - 显示哪些 API 支持代理
  • DOMAIN_TO_API_TYPE - 显示实际的域名和端点

常见的 Star Child 数据源(截至本文撰写时,但请务必检查):

  • coingecko - 加密货币市场数据(Bitcoin、Ethereum 等)
  • twelvedata - 股票、外汇、大宗商品(AAPL、EUR/USD、XAU/USD)
  • taapi - 技术分析指标(RSI、MACD 等)
  • coinglass - 衍生品/期权数据
  • lunarcrush - 社交情绪数据
  • twitterapi - Twitter/X 数据
  • birdeye - 智能资金与钱包分析
  • oneinch - DEX 聚合器

不要硬编码假设。 上述列表可能已过时。务必检查 core/http_client.py,了解实际配置了哪些内容。

如果列出了某个 API:

  1. 检查是否存在对应的 *_API_KEY 环境变量
  2. 使用 web_fetch 查看 API 文档(搜索“{api_name} API documentation”)
  3. 这些 API 通过 Star Child 的代理路由,因此初期可以直接使用,无需担心速率限制

示例:查找加密货币数据源

# Step 1: Check what's available
read_file core/http_client.py | grep -i "coingecko\|crypto"

# Step 2: Check if key is configured
bash env | grep COINGECKO_API_KEY

# Step 3: If found, fetch docs to understand endpoints
web_fetch "https://docs.coingecko.com/reference/endpoint-overview"

代理自动配置模式:

// Add this at the top of your data fetching script
// (Only needed if running standalone outside the browser)
if (typeof process !== 'undefined' && process.env) {
  const proxyHost = process.env.PROXY_HOST;
  const proxyPort = process.env.PROXY_PORT || '8080';

  if (proxyHost && !process.env.HTTP_PROXY) {
    // Handle IPv6 addresses
    const host = proxyHost.includes(':') && !proxyHost.startsWith('[')
      ? `[${proxyHost}]`
      : proxyHost;
    const proxyUrl = `http://${host}:${proxyPort}`;

    process.env.HTTP_PROXY = proxyUrl;
    process.env.HTTPS_PROXY = proxyUrl;
  }
}

浏览器用法(Fetch API):

// CoinGecko - Bitcoin price
async function fetchBitcoinPrice() {
  const response = await fetch(
    'https://pro-api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd',
    {
      headers: {
        'x-cg-pro-api-key': 'YOUR_API_KEY'  // Get from env in production
      }
    }
  );
  const data = await response.json();
  return data.bitcoin.usd;
}

// Twelve Data - Apple stock
async function fetchAppleStock() {
  const response = await fetch(
    'https://api.twelvedata.com/price?symbol=AAPL&apikey=YOUR_API_KEY'
  );
  const data = await response.json();
  return data.price;
}
API 选择决策树

重要: Star Child 的高级 API(CoinGecko、Twelve Data)有速率限制。使用此决策树判断是否应使用高级 API 或免费替代方案:

Step 1: Identify data domain
├─ Crypto (BTC, ETH, etc.)
├─ Stocks (AAPL, MSFT, etc.)
├─ Forex (EUR/USD, etc.)
├─ Commodities (Gold, Oil)
└─ Other (Weather, News, etc.)

Step 2: Determine update frequency
├─ High-frequency (< 5 min intervals)    → Research FREE alternatives
├─ Medium-frequency (5-15 min)           → Star Child premium OK
└─ Low-frequency (> 15 min)              → Star Child premium preferred

Step 3: Assess data requirements
├─ Simple current prices                 → FREE alternatives often sufficient
├─ Historical OHLC data                  → Star Child premium if available
├─ Real-time streaming                   → Research specialized APIs
└─ Multiple data points per asset        → Consider rate limit impact

Step 4: Decision
├─ Use Star Child premium if:
│   - Update frequency is medium/low
│   - Need historical OHLC data
│   - Need reliable, clean data
│   - Total API calls < rate limit
│
└─ Research FREE alternative if:
    - High-frequency updates
    - Simple data needs (current price only)
    - Premium quota exhausted
    - Multiple assets × high frequency = too many calls

速率限制影响计算器:

Updates per hour = (60 / update_interval_minutes)
Assets to track = N
Daily API calls = Updates per hour × 24 × N

Example:
- 5 cryptocurrencies
- Update every 2 minutes
- Calls per hour: 60/2 = 30
- Daily calls: 30 × 24 × 5 = 3,600 calls/day

If Star Child premium limit is 1,000 calls/day → NEED FREE ALTERNATIVE
If Star Child premium limit is 10,000 calls/day → Premium OK

何时调研免费替代方案:

  1. 计算出的每日调用次数超过高级 API 配额
  2. 用户明确要求免费/无需身份验证的方案
  3. 仪表板面向公众(用户越多,请求量越大)
  4. 处于原型设计/测试阶段(节省高级 API 配额)
  5. 用户尚未配置 API 密钥
免费 API 的研究理念

当高级 API 无法满足需求时(速率限制、调用频率高或用户偏好),你的任务是找到真正可行的方案。不要照本宣科,而要切实解决问题。

这样思考:

用户需要数据。你需要找到免费数据的来源,判断数据是否足够可靠,以及是否真的适用于他们的使用场景。先计算所需调用量——如果他们每天需要 3,600 次调用,而“免费” API 只允许 100 次,那你根本没有解决问题。

需要考虑的因素:

  • 速率限制与实际用量 - 不要只列出限制,还要计算是否足够
  • 维护情况 - 一个 2022 年推出、此后再无更新的完美 API 就像一颗定时炸弹
  • CORS - 浏览器仪表板缺少 CORS 支持就无法运行
  • 数据质量 - 如果数据陈旧或错误,“免费”毫无意义
  • 使用门槛 - 对于仪表板场景,无需认证 > API 密钥 > OAuth

研究深度应与利害程度相匹配:

  • 快速原型?找到一个好选项就够了。
  • 生产环境的仪表板?找出 2-3 个选项并比较利弊。
  • 用户说“帮我找最好的”?那就真正做好研究。

查找渠道:

  • github.com/public-apis/public-apis(900+ 个 API,先查这里)
  • 使用类似集成的近期 GitHub 仓库
  • 2025-2026 年的开发者文章(不要看 2022 年的)
  • API 市场网站(rapidapi、publicapis.io)

危险信号:

  • 上次提交距今已有 2 年以上
  • 速率限制被隐藏或说明不清
  • “免费套餐”要求提供信用卡
  • 仅在特定地区可用,却未说明
  • GitHub issues 中提到了 CORS 问题

要测试,不要盲信: 如果能获取文档或测试端点,就亲自去做。不要根据 2023 年的博客文章提出建议。

总结:研究优先的方法

仪表板技能的优先事项:

  1. 先计算再选择:始终根据更新频率和资产数量计算 API 调用总量
  2. 适当使用高级方案:中低频场景和历史数据使用 Star Child 高级 API
  3. 必要时开展研究:针对高频、无需认证或有专门需求的场景进行深入研究
  4. 提供多个选项:为用户提供 2-4 个经过研究的选项,并清楚说明利弊
  5. 确认信息是否最新:优先采用 2025-2026 年的资料,并选择仍在积极维护的项目

Agent 绝不能:

  • 不先计算速率限制的影响就推荐 API
  • 不检查配额就为高频仪表板推荐高级 API
  • 不调查免费 API 的当前情况就推荐特定方案
  • 在需要免费替代方案时跳过研究步骤
  • 推荐已弃用或不再维护的 API

2. 仪表板生成方法

方法 A:基于模板(推荐用于快速开发)

使用预构建的 HTML 仪表板模板作为基础:

Tabler(github.com/tabler/tabler)⭐ 推荐

  • 基于 Bootstrap 5.3,GitHub 星标数超过 40k
  • 100+ 个响应式组件、200+ 个示例页面
  • 内置 5,400+ 个 SVG 图标
  • 内置深色模式
  • MIT 许可

AdminKit(github.com/adminkit/adminkit)

  • 基于 Bootstrap 5,采用现代构建流程
  • 不依赖 jQuery
  • Webpack、Sass、BrowserSync
  • 适配所有设备,支持响应式布局

实现步骤:

# Option 1: Download template
curl -L https://github.com/tabler/tabler/archive/refs/heads/main.zip -o tabler.zip
unzip tabler.zip
cd tabler-main/dist

# Option 2: Use via CDN (faster for prototyping)
# See code example below

Tabler 快速入门:

<!DOCTYPE html>
<html>
<head>
  <title>Dashboard</title>
  <link href="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css" rel="stylesheet"/>
</head>
<body>
  <div class="page">
    <div class="page-wrapper">
      <div class="page-header">
        <div class="container-xl">
          <h1>My Dashboard</h1>
        </div>
      </div>
      <div class="page-body">
        <div class="container-xl">
          <div class="row row-cards">
            
          </div>
        </div>
      </div>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/js/tabler.min.js"></script>
</body>
</html>
方法 B:自定义 HTML/CSS/JS(受 worldmonitor 启发)

如果需要更多控制权,可从头开始构建模块化仪表板:

架构模式(受 worldmonitor 启发):

dashboard/
├── index.html          # Main page structure
├── css/
│   ├── base.css       # Reset, typography, variables
│   ├── layout.css     # Grid, containers, responsive
│   └── components.css # Cards, charts, widgets
├── js/
│   ├── config.js      # API endpoints, settings
│   ├── data.js        # Data fetching & caching
│   ├── charts.js      # Visualization logic
│   └── main.js        # Initialization
└── assets/
    └── icons/         # SVG icons

核心 HTML 结构:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Dashboard</title>
  <link rel="stylesheet" href="css/base.css">
  <link rel="stylesheet" href="css/layout.css">
  <link rel="stylesheet" href="css/components.css">
</head>
<body>
  <div class="dashboard">
    <aside class="sidebar">
      <nav class="nav">
        
      </nav>
    </aside>

    <main class="main-content">
      <header class="header">
        <h1 class="title">Dashboard</h1>
      </header>

      <div class="grid">
        
        <div class="card">
          <h2 class="card-title">Metrics</h2>
          <div class="card-body">
            <canvas id="chart1"></canvas>
          </div>
        </div>
      </div>
    </main>
  </div>

  <script src="js/config.js"></script>
  <script src="js/data.js"></script>
  <script src="js/charts.js"></script>
  <script src="js/main.js"></script>
</body>
</html>

响应式 CSS Grid 布局:

/* Mobile-first grid */
.grid {
  display: grid;
  gap: 1.5rem;
  padding: 1.5rem;
}

/* Tablet: 2 columns */
@media (min-width: 768px) {
  .grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Desktop: 3 columns */
@media (min-width: 1024px) {
  .grid {
    grid-template-columns: repeat(3, 1fr);
  }

  .card.wide {
    grid-column: span 2;
  }

  .card.full {
    grid-column: span 3;
  }
}

3. 可视化库

根据需求选择合适的库:

Chart.js ⭐ 推荐(默认选择)

最适合: 简单图表、快速实现、合理的默认设置 优点: 易于使用、支持响应式、基于 canvas(速度快)、文档丰富 缺点: 与 D3.js 相比,自定义能力有限


<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<canvas id="myChart"></canvas>

<script>
const ctx = document.getElementById('myChart').getContext('2d');
const chart = new Chart(ctx, {
  type: 'line',
  data: {
    labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
    datasets: [{
      label: 'Sales',
      data: [12, 19, 3, 5, 2],
      borderColor: 'rgb(75, 192, 192)',
      tension: 0.1
    }]
  },
  options: {
    responsive: true,
    maintainAspectRatio: false
  }
});

// Update chart with new data
function updateChart(newData) {
  chart.data.datasets[0].data = newData;
  chart.update();
}
</script>
ApexCharts(适用于精美的实时仪表板)

最适合: 营销仪表板、实时数据、注重美观的场景 优点: 默认样式精美、动画流畅、支持实时更新和注释 缺点: API 稍微复杂一些

<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>

<div id="chart"></div>

<script>
const options = {
  chart: {
    type: 'area',
    height: 350,
    animations: {
      enabled: true,
      dynamicAnimation: {
        speed: 1000
      }
    }
  },
  series: [{
    name: 'Value',
    data: [31, 40, 28, 51, 42, 109, 100]
  }],
  xaxis: {
    categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']
  }
};

const chart = new ApexCharts(document.querySelector("#chart"), options);
chart.render();

// Real-time updates
function addDataPoint(value) {
  chart.appendData([{
    data: [value]
  }]);
}
</script>
D3.js(用于自定义可视化)

最适合: 自定义可视化、完全掌控创意、复杂数据
优点: 灵活性最高、数据绑定功能强大
缺点: 学习曲线陡峭、需要编写更多代码

仅在以下情况下使用 D3.js:

  • 用户明确要求自定义或独特的可视化
  • 标准图表类型(折线图、柱状图、饼图)无法满足需求
  • 需要复杂的交互或动画
<script src="https://d3js.org/d3.v7.min.js"></script>

<svg id="chart" width="600" height="400"></svg>

<script>
const data = [30, 86, 168, 281, 303, 365];

const svg = d3.select("#chart");
const margin = {top: 20, right: 20, bottom: 30, left: 40};
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;

const x = d3.scaleBand()
  .domain(d3.range(data.length))
  .range([0, width])
  .padding(0.1);

const y = d3.scaleLinear()
  .domain([0, d3.max(data)])
  .range([height, 0]);

const g = svg.append("g")
  .attr("transform", `translate(${margin.left},${margin.top})`);

g.selectAll(".bar")
  .data(data)
  .join("rect")
  .attr("class", "bar")
  .attr("x", (d, i) => x(i))
  .attr("y", d => y(d))
  .attr("width", x.bandwidth())
  .attr("height", d => height - y(d))
  .attr("fill", "steelblue");
</script>

库选择指南:

User needs simple charts? → Chart.js
User needs beautiful real-time dashboard? → ApexCharts
User needs custom/unique visualizations? → D3.js
User unsure? → Chart.js (safest default)

4. 实时数据集成

SSE(服务器发送事件)⭐ 推荐

适用场景: 服务器向客户端流式传输(95% 的仪表板场景) 优点: 简单、自动重新连接、基于 HTTP、TTFB 更佳 缺点: 仅支持单向通信(服务器 → 客户端)

2026 年使用 SSE 的原因:

  • HTTP/3(QUIC)消除了队头阻塞
  • 非常适合 AI 流式传输、实时指标和通知
  • 对于单向数据流,比 WebSocket 更简单
  • 更适合仪表板更新(无需客户端→服务器消息)

客户端实现:

// Connect to SSE endpoint
const eventSource = new EventSource('/api/dashboard-updates');

// Listen for updates
eventSource.addEventListener('metric-update', (event) => {
  const data = JSON.parse(event.data);
  updateChart(data);
});

eventSource.addEventListener('error', (error) => {
  console.error('SSE error:', error);
  // Auto-reconnects by default
});

// Close connection when done
function cleanup() {
  eventSource.close();
}

服务端(如果正在构建后端):

# FastAPI example
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
import json

app = FastAPI()

async def event_stream():
    while True:
        # Fetch latest data
        data = get_dashboard_metrics()

        # SSE format: "event: name\ndata: {json}\n\n"
        yield f"event: metric-update\ndata: {json.dumps(data)}\n\n"

        await asyncio.sleep(5)  # Update every 5 seconds

@app.get("/api/dashboard-updates")
async def stream_updates():
    return StreamingResponse(
        event_stream(),
        media_type="text/event-stream"
    )
WebSocket(用于双向通信)

在以下情况下使用 WebSocket:

  • 用户需要向服务器发送数据(聊天、协作编辑)
  • 需要亚秒级延迟(交易平台、多人游戏)
  • 需要真正的双向通信

客户端:

const ws = new WebSocket('wss://api.example.com/ws');

ws.onopen = () => {
  console.log('Connected');
  ws.send(JSON.stringify({ type: 'subscribe', channel: 'metrics' }));
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  updateChart(data);
};

ws.onerror = (error) => {
  console.error('WebSocket error:', error);
};

ws.onclose = () => {
  console.log('Disconnected');
  // Implement reconnection logic
  setTimeout(connectWebSocket, 5000);
};
轮询(备用方案)

在以下情况下使用轮询:

  • SSE/WebSocket 不可用
  • 使用场景简单且更新频率较低
  • 需要最大兼容性
async function pollData() {
  try {
    const response = await fetch('/api/metrics');
    const data = await response.json();
    updateChart(data);
  } catch (error) {
    console.error('Polling error:', error);
  }
}

// Poll every 10 seconds
setInterval(pollData, 10000);
pollData(); // Initial load

5. 仪表板设计最佳实践(2026 年)

视觉层次

优先级布局:

  • 最关键的数据:左上方(符合自然视线流向)
  • 次要指标:右上方
  • 详细图表:中间及下方
  • 操作/筛选器:顶部或侧边栏

布局示例:

<div class="grid">
  
  <div class="card full">
    <div class="metrics-row">
      <div class="metric">
        <span class="metric-label">Total Revenue</span>
        <span class="metric-value">$142,592</span>
        <span class="metric-change positive">+12.5%</span>
      </div>
      
    </div>
  </div>

  
  <div class="card wide">
    <canvas id="revenue-chart"></canvas>
  </div>
  <div class="card">
    <canvas id="users-chart"></canvas>
  </div>

  
  <div class="card full">
    <table class="data-table">
      
    </table>
  </div>
</div>
移动优先的响应式设计

方法:

  1. 首先针对移动设备(320px)进行设计
  2. 渐进增强,适配平板(768px+)
  3. 桌面布局(1024px+)

关键模式:

/* Mobile: Stack vertically */
.metrics-row {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* Tablet: 2 columns */
@media (min-width: 768px) {
  .metrics-row {
    flex-direction: row;
    flex-wrap: wrap;
  }

  .metric {
    flex: 1 1 calc(50% - 0.5rem);
  }
}

/* Desktop: 4 columns */
@media (min-width: 1024px) {
  .metric {
    flex: 1 1 calc(25% - 0.75rem);
  }
}

/* Touch-friendly controls (min 44x44px) */
.button {
  min-height: 44px;
  min-width: 44px;
  padding: 0.75rem 1.5rem;
}
无障碍访问

基本实践:

  • 使用对色盲友好的调色板(结合图案与颜色)
  • 为屏幕阅读器添加 ARIA 标签
  • 支持键盘导航
  • 确保对比度充足(至少达到 WCAG AA)

<div class="chart-container" role="img" aria-label="Revenue trend showing 12% growth over last quarter">
  <canvas id="chart"></canvas>

  
  <table class="sr-only">
    <caption>Revenue Data</caption>
    <thead>
      <tr>
        <th>Month</th>
        <th>Revenue</th>
      </tr>
    </thead>
    <tbody>
      <tr><td>January</td><td>$12,000</td></tr>
      
    </tbody>
  </table>
</div>
/* Screen reader only class */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

/* Color-blind safe palette */
:root {
  --color-primary: #0066CC;    /* Blue */
  --color-success: #28A745;    /* Green */
  --color-warning: #FFC107;    /* Amber */
  --color-danger: #DC3545;     /* Red */
  --color-info: #17A2B8;       /* Cyan */
}
性能优化

关键实践:

// 1. Debounce resize events
let resizeTimeout;
window.addEventListener('resize', () => {
  clearTimeout(resizeTimeout);
  resizeTimeout = setTimeout(() => {
    chart.resize();
  }, 250);
});

// 2. Limit data points for large datasets
function decimateData(data, maxPoints = 100) {
  if (data.length <= maxPoints) return data;

  const step = Math.ceil(data.length / maxPoints);
  return data.filter((_, i) => i % step === 0);
}

// 3. Use RequestAnimationFrame for smooth updates
function smoothUpdate(newValue) {
  requestAnimationFrame(() => {
    updateMetric(newValue);
  });
}

// 4. Lazy load charts (Intersection Observer)
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      initializeChart(entry.target);
      observer.unobserve(entry.target);
    }
  });
});

document.querySelectorAll('.chart-container').forEach(el => {
  observer.observe(el);
});
深色模式支持
/* CSS Variables for theming */
:root {
  --bg-primary: #ffffff;
  --bg-secondary: #f8f9fa;
  --text-primary: #212529;
  --text-secondary: #6c757d;
  --border-color: #dee2e6;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg-primary: #1a1a1a;
    --bg-secondary: #2d2d2d;
    --text-primary: #e9ecef;
    --text-secondary: #adb5bd;
    --border-color: #495057;
  }
}

body {
  background-color: var(--bg-primary);
  color: var(--text-primary);
}

.card {
  background-color: var(--bg-secondary);
  border: 1px solid var(--border-color);
}

6. 完整仪表板示例

以下是一个融合所有最佳实践、可用于生产环境的模板:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Live Dashboard</title>
  <link href="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css" rel="stylesheet"/>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <style>
    .metric-card {
      text-align: center;
      padding: 1.5rem;
    }
    .metric-value {
      font-size: 2.5rem;
      font-weight: bold;
      margin: 0.5rem 0;
    }
    .metric-change {
      font-size: 0.9rem;
      font-weight: 500;
    }
    .metric-change.positive { color: #28a745; }
    .metric-change.negative { color: #dc3545; }
    .chart-container {
      position: relative;
      height: 300px;
    }
  </style>
</head>
<body>
  <div class="page">
    <div class="page-wrapper">
      <div class="page-header">
        <div class="container-xl">
          <h1 class="page-title">Analytics Dashboard</h1>
        </div>
      </div>

      <div class="page-body">
        <div class="container-xl">
          
          <div class="row row-cards mb-3">
            <div class="col-sm-6 col-lg-3">
              <div class="card metric-card">
                <div class="metric-label text-muted">Total Users</div>
                <div class="metric-value" id="total-users">0</div>
                <div class="metric-change positive" id="users-change">+0%</div>
              </div>
            </div>
            <div class="col-sm-6 col-lg-3">
              <div class="card metric-card">
                <div class="metric-label text-muted">Revenue</div>
                <div class="metric-value" id="revenue">$0</div>
                <div class="metric-change positive" id="revenue-change">+0%</div>
              </div>
            </div>
            <div class="col-sm-6 col-lg-3">
              <div class="card metric-card">
                <div class="metric-label text-muted">Conversion</div>
                <div class="metric-value" id="conversion">0%</div>
                <div class="metric-change" id="conversion-change">+0%</div>
              </div>
            </div>
            <div class="col-sm-6 col-lg-3">
              <div class="card metric-card">
                <div class="metric-label text-muted">Active Now</div>
                <div class="metric-value" id="active">0</div>
                <div class="metric-change" id="active-change">--</div>
              </div>
            </div>
          </div>

          
          <div class="row row-cards">
            <div class="col-lg-8">
              <div class="card">
                <div class="card-header">
                  <h3 class="card-title">Revenue Trend</h3>
                </div>
                <div class="card-body">
                  <div class="chart-container">
                    <canvas id="revenueChart"></canvas>
                  </div>
                </div>
              </div>
            </div>
            <div class="col-lg-4">
              <div class="card">
                <div class="card-header">
                  <h3 class="card-title">Traffic Sources</h3>
                </div>
                <div class="card-body">
                  <div class="chart-container">
                    <canvas id="trafficChart"></canvas>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <script>
    // Chart configurations
    const revenueChart = new Chart(document.getElementById('revenueChart'), {
      type: 'line',
      data: {
        labels: [],
        datasets: [{
          label: 'Revenue',
          data: [],
          borderColor: '#0066CC',
          backgroundColor: 'rgba(0, 102, 204, 0.1)',
          tension: 0.4,
          fill: true
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: { display: false }
        },
        scales: {
          y: { beginAtZero: true }
        }
      }
    });

    const trafficChart = new Chart(document.getElementById('trafficChart'), {
      type: 'doughnut',
      data: {
        labels: ['Direct', 'Social', 'Search', 'Referral'],
        datasets: [{
          data: [30, 25, 35, 10],
          backgroundColor: ['#0066CC', '#28A745', '#FFC107', '#DC3545']
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: { position: 'bottom' }
        }
      }
    });

    // Real-time updates with SSE
    const eventSource = new EventSource('/api/dashboard-updates');

    eventSource.addEventListener('metrics', (event) => {
      const data = JSON.parse(event.data);

      // Update metric cards
      document.getElementById('total-users').textContent = data.users.toLocaleString();
      document.getElementById('revenue').textContent = '$' + data.revenue.toLocaleString();
      document.getElementById('conversion').textContent = data.conversion + '%';
      document.getElementById('active').textContent = data.active.toLocaleString();

      // Update changes
      updateChange('users-change', data.usersChange);
      updateChange('revenue-change', data.revenueChange);
      updateChange('conversion-change', data.conversionChange);

      // Update charts
      revenueChart.data.labels.push(data.timestamp);
      revenueChart.data.datasets[0].data.push(data.revenue);

      // Keep only last 20 points
      if (revenueChart.data.labels.length > 20) {
        revenueChart.data.labels.shift();
        revenueChart.data.datasets[0].data.shift();
      }

      revenueChart.update('none'); // No animation for real-time
    });

    function updateChange(elementId, value) {
      const element = document.getElementById(elementId);
      element.textContent = (value >= 0 ? '+' : '') + value + '%';
      element.className = 'metric-change ' + (value >= 0 ? 'positive' : 'negative');
    }

    // Fallback to polling if SSE fails
    eventSource.onerror = () => {
      console.log('SSE failed, falling back to polling');
      eventSource.close();
      setInterval(fetchData, 10000);
    };

    async function fetchData() {
      try {
        const response = await fetch('/api/metrics');
        const data = await response.json();
        // Update dashboard with data
      } catch (error) {
        console.error('Fetch error:', error);
      }
    }
  </script>
</body>
</html>

7. 仪表板创建工作流

当用户要求你创建仪表板时,请遵循以下工作流:

步骤 1:收集需求
Ask the user:
1. What data do you want to display? (metrics, trends, comparisons)
2. Do you have data sources already? (If no, proceed to discovery)
3. Real-time updates needed? (yes/no, update frequency)
4. Target devices? (desktop-only, mobile-first, both)
5. Preferred style? (minimal, professional, colorful)
步骤 2:发现数据源(如有需要)
1. Identify domain(s): finance, weather, social, etc.
2. Use web_search to find appropriate APIs
3. Recommend 2-3 options with examples
4. Document API endpoints and authentication
步骤 3:确定架构
Choose approach:
- **Template-based (Tabler/AdminKit)**: Fast, professional, best for standard dashboards
- **Custom HTML/CSS/JS**: More control, best for unique requirements
- **Hybrid**: Template + custom visualizations

Choose visualization library:
- **Chart.js**: Default choice for most use cases
- **ApexCharts**: If real-time + beautiful aesthetics required
- **D3.js**: Only if custom visualizations explicitly needed
步骤 4:实施
1. Create HTML structure (responsive grid layout)
2. Add CSS (mobile-first, accessibility, dark mode)
3. Integrate chart library
4. Implement data fetching (API calls)
5. Add real-time updates (SSE preferred, polling fallback)
6. Test responsiveness and accessibility
步骤 5:编写文档
Provide user with:
1. Installation instructions
2. API setup guide (if external APIs used)
3. Customization options
4. How to deploy (static host, backend requirements)

8. 常见模式与示例

模式:多源仪表板

组合来自 Star Child 内置 API 和外部来源的数据:

// config.js
const API_CONFIG = {
  coingecko: {
    baseUrl: 'https://pro-api.coingecko.com/api/v3',
    apiKey: process.env.COINGECKO_API_KEY || 'YOUR_API_KEY'
  },
  twelvedata: {
    baseUrl: 'https://api.twelvedata.com',
    apiKey: process.env.TWELVEDATA_API_KEY || 'YOUR_API_KEY'
  },
  openMeteo: {
    baseUrl: 'https://api.open-meteo.com/v1'
  }
};

// data.js
async function fetchAllData() {
  const results = await Promise.all([
    fetchCryptoPrices(),
    fetchStockPrices(),
    fetchWeather(),
    fetchNews()
  ]);

  return {
    crypto: results[0],
    stocks: results[1],
    weather: results[2],
    news: results[3],
    timestamp: new Date().toISOString()
  };
}

// CoinGecko - Multiple crypto prices
async function fetchCryptoPrices() {
  const response = await fetch(
    `${API_CONFIG.coingecko.baseUrl}/simple/price?ids=bitcoin,ethereum,solana&vs_currencies=usd&include_24hr_change=true`,
    {
      headers: {
        'x-cg-pro-api-key': API_CONFIG.coingecko.apiKey
      }
    }
  );

  if (!response.ok) {
    throw new Error(`CoinGecko API error: ${response.status}`);
  }

  return response.json();
}

// Twelve Data - Stock prices
async function fetchStockPrices() {
  const symbols = ['AAPL', 'MSFT', 'GOOGL'];
  const promises = symbols.map(symbol =>
    fetch(`${API_CONFIG.twelvedata.baseUrl}/price?symbol=${symbol}&apikey=${API_CONFIG.twelvedata.apiKey}`)
      .then(r => r.json())
      .then(data => ({ symbol, price: parseFloat(data.price) }))
  );

  const results = await Promise.all(promises);
  return results.reduce((acc, { symbol, price }) => {
    acc[symbol] = price;
    return acc;
  }, {});
}

// Open-Meteo - Weather (no auth required)
async function fetchWeather() {
  const params = new URLSearchParams({
    latitude: 52.52,
    longitude: 13.41,
    current_weather: true
  });

  const response = await fetch(`${API_CONFIG.openMeteo.baseUrl}/forecast?${params}`);
  return response.json();
}

// Hacker News - Tech news (no auth required)
async function fetchNews() {
  const response = await fetch('https://hn.algolia.com/api/v1/search?tags=front_page&hitsPerPage=5');
  return response.json();
}

// main.js
async function initializeDashboard() {
  try {
    const data = await fetchAllData();

    // Update crypto cards
    updateCryptoCard('bitcoin', data.crypto.bitcoin.usd, data.crypto.bitcoin.usd_24h_change);
    updateCryptoCard('ethereum', data.crypto.ethereum.usd, data.crypto.ethereum.usd_24h_change);
    updateCryptoCard('solana', data.crypto.solana.usd, data.crypto.solana.usd_24h_change);

    // Update stock cards
    updateStockCard('AAPL', data.stocks.AAPL);
    updateStockCard('MSFT', data.stocks.MSFT);
    updateStockCard('GOOGL', data.stocks.GOOGL);

    // Update weather widget
    updateWeatherWidget(data.weather);

    // Update news feed
    updateNewsFeed(data.news.hits);

  } catch (error) {
    console.error('Dashboard initialization failed:', error);
    showError('Failed to load dashboard data. Please try again.');
  }
}

function updateCryptoCard(coinId, price, change24h) {
  const card = document.getElementById(`crypto-${coinId}`);
  if (!card) return;

  card.querySelector('.price').textContent = '$' + price.toLocaleString(undefined, {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  });

  const changeEl = card.querySelector('.change');
  const changeValue = change24h.toFixed(2);
  changeEl.textContent = (change24h >= 0 ? '+' : '') + changeValue + '%';
  changeEl.className = 'change ' + (change24h >= 0 ? 'positive' : 'negative');
}

function updateStockCard(symbol, price) {
  const card = document.getElementById(`stock-${symbol}`);
  if (!card) return;

  card.querySelector('.price').textContent = '$' + price.toFixed(2);
}

function updateWeatherWidget(weatherData) {
  const widget = document.getElementById('weather-widget');
  if (!widget) return;

  const temp = weatherData.current_weather.temperature;
  const windSpeed = weatherData.current_weather.windspeed;

  widget.querySelector('.temperature').textContent = temp + '°C';
  widget.querySelector('.wind').textContent = windSpeed + ' km/h';
}

function updateNewsFeed(newsItems) {
  const feed = document.getElementById('news-feed');
  if (!feed) return;

  feed.innerHTML = newsItems.map(item => `
    <div class="news-item">
      <a href="${item.url}" target="_blank">${item.title}</a>
      <span class="news-meta">${item.points} points • ${item.author}</span>
    </div>
  `).join('');
}

function showError(message) {
  const errorEl = document.getElementById('error-message');
  if (errorEl) {
    errorEl.textContent = message;
    errorEl.style.display = 'block';
    setTimeout(() => errorEl.style.display = 'none', 5000);
  }
}

// Initialize on page load
document.addEventListener('DOMContentLoaded', initializeDashboard);

// Auto-refresh every 5 minutes
setInterval(initializeDashboard, 5 * 60 * 1000);

多源仪表板的 HTML 结构:

<div class="dashboard-grid">
  
  <div class="section">
    <h2>Cryptocurrency</h2>
    <div class="cards">
      <div class="card" id="crypto-bitcoin">
        <h3>Bitcoin</h3>
        <div class="price">$0</div>
        <div class="change">0%</div>
      </div>
      <div class="card" id="crypto-ethereum">
        <h3>Ethereum</h3>
        <div class="price">$0</div>
        <div class="change">0%</div>
      </div>
      <div class="card" id="crypto-solana">
        <h3>Solana</h3>
        <div class="price">$0</div>
        <div class="change">0%</div>
      </div>
    </div>
  </div>

  
  <div class="section">
    <h2>Stock Market</h2>
    <div class="cards">
      <div class="card" id="stock-AAPL">
        <h3>Apple</h3>
        <div class="price">$0</div>
      </div>
      <div class="card" id="stock-MSFT">
        <h3>Microsoft</h3>
        <div class="price">$0</div>
      </div>
      <div class="card" id="stock-GOOGL">
        <h3>Google</h3>
        <div class="price">$0</div>
      </div>
    </div>
  </div>

  
  <div class="card" id="weather-widget">
    <h3>Weather</h3>
    <div class="temperature">--°C</div>
    <div class="wind">-- km/h</div>
  </div>

  
  <div class="section">
    <h2>Tech News</h2>
    <div id="news-feed"></div>
  </div>
</div>

<div id="error-message" class="error" style="display: none;"></div>
模式:错误处理与加载状态
<div class="card">
  <div class="card-header">
    <h3>Bitcoin Price</h3>
  </div>
  <div class="card-body">
    
    <div class="loading" id="crypto-loading">
      <div class="spinner"></div>
      <p>Loading...</p>
    </div>

    
    <div class="error" id="crypto-error" style="display: none;">
      <p class="error-message"></p>
      <button onclick="retryFetch()">Retry</button>
    </div>

    
    <div class="content" id="crypto-content" style="display: none;">
      <div class="metric-value" id="btc-price">$0</div>
    </div>
  </div>
</div>

<script>
async function fetchCryptoWithStates() {
  const loadingEl = document.getElementById('crypto-loading');
  const errorEl = document.getElementById('crypto-error');
  const contentEl = document.getElementById('crypto-content');

  // Show loading
  loadingEl.style.display = 'block';
  errorEl.style.display = 'none';
  contentEl.style.display = 'none';

  try {
    const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd');

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const data = await response.json();

    // Show content
    loadingEl.style.display = 'none';
    contentEl.style.display = 'block';

    document.getElementById('btc-price').textContent =
      '$' + data.bitcoin.usd.toLocaleString();

  } catch (error) {
    // Show error
    loadingEl.style.display = 'none';
    errorEl.style.display = 'block';
    errorEl.querySelector('.error-message').textContent =
      'Failed to load Bitcoin price: ' + error.message;
  }
}
</script>
模式:性能缓存
// Simple in-memory cache with TTL
class DataCache {
  constructor(ttlSeconds = 300) {
    this.cache = new Map();
    this.ttl = ttlSeconds * 1000;
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;

    if (Date.now() > item.expires) {
      this.cache.delete(key);
      return null;
    }

    return item.data;
  }

  set(key, data) {
    this.cache.set(key, {
      data,
      expires: Date.now() + this.ttl
    });
  }

  clear() {
    this.cache.clear();
  }
}

const dataCache = new DataCache(300); // 5 minute TTL

async function fetchWithCache(url, cacheKey) {
  // Check cache first
  const cached = dataCache.get(cacheKey);
  if (cached) {
    console.log('Cache hit:', cacheKey);
    return cached;
  }

  // Fetch fresh data
  console.log('Cache miss:', cacheKey);
  const response = await fetch(url);
  const data = await response.json();

  // Store in cache
  dataCache.set(cacheKey, data);

  return data;
}

// Usage
const weather = await fetchWithCache(
  'https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true',
  'weather-berlin'
);

9. 部署与托管

静态托管(无后端)

如果仪表板仅使用客户端 JavaScript 和公共 API:

选项:

  1. GitHub Pages - 免费,适用于静态网站,操作简单
  2. Netlify - 免费套餐、即时部署、自定义域名
  3. Vercel - 免费套餐、性能出色
  4. Cloudflare Pages - 免费,全球 CDN 速度快

部署步骤(Netlify):

# 1. Build your dashboard
mkdir my-dashboard
cd my-dashboard
# Copy your HTML/CSS/JS files

# 2. Initialize git
git init
git add .
git commit -m "Initial dashboard"

# 3. Push to GitHub
gh repo create my-dashboard --public --source=. --push

# 4. Connect to Netlify (or use drag-and-drop on netlify.com)
netlify deploy --prod
使用后端(SSE/API)

如果需要服务端数据处理或 SSE:

简单的 FastAPI 后端:

# backend/main.py
from fastapi import FastAPI
from fastapi.responses import StreamingResponse, FileResponse
from fastapi.staticfiles import StaticFiles
import asyncio
import json

app = FastAPI()

# Serve static dashboard files
app.mount("/static", StaticFiles(directory="dashboard"), name="static")

@app.get("/")
async def root():
    return FileResponse("dashboard/index.html")

# SSE endpoint for real-time updates
async def event_generator():
    while True:
        # Fetch data from external APIs
        data = {
            "users": 1250,
            "revenue": 45230,
            "conversion": 3.2,
            "active": 42,
            "timestamp": "12:00"
        }

        yield f"event: metrics\ndata: {json.dumps(data)}\n\n"
        await asyncio.sleep(5)

@app.get("/api/dashboard-updates")
async def stream():
    return StreamingResponse(event_generator(), media_type="text/event-stream")

# Run with: uvicorn main:app --reload

部署到 Render/Railway/Fly.io: 这些平台都为爱好项目提供免费套餐,并支持 FastAPI。


10. 故障排除

常见问题

CORS 错误:

// If API doesn't support CORS, use a proxy
const PROXY = 'https://corsproxy.io/?';
const response = await fetch(PROXY + encodeURIComponent(apiUrl));

速率限制:

// Implement exponential backoff
async function fetchWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url);

      if (response.status === 429) {
        // Rate limited, wait and retry
        const waitTime = Math.pow(2, i) * 1000;
        console.log(`Rate limited, waiting ${waitTime}ms`);
        await new Promise(resolve => setTimeout(resolve, waitTime));
        continue;
      }

      return response;
    } catch (error) {
      if (i === maxRetries - 1) throw error;
    }
  }
}

图表未渲染:

// Ensure canvas is visible before initialization
const canvas = document.getElementById('myChart');
if (canvas.offsetParent === null) {
  // Canvas is hidden, wait for it to be visible
  const observer = new MutationObserver(() => {
    if (canvas.offsetParent !== null) {
      initializeChart();
      observer.disconnect();
    }
  });
  observer.observe(canvas.parentElement, { attributes: true });
}

11. 资源与参考资料

官方文档
API 目录
设计资源
学习资源

总结

创建仪表板时:

  1. 从需求入手 - 了解用户需求、数据来源和设备
  2. 明智选择 - 追求速度就选模板(Tabler),需要掌控力就选自定义
  3. 默认选择 Chart.js - 简单、可靠,适用于 90% 的情况
  4. 实时更新使用 SSE - 对大多数仪表板而言,比 WebSocket 更简单
  5. 始终优先考虑移动端 - 先针对最小屏幕进行设计
  6. 重视无障碍 - 使用色盲友好配色、ARIA 标签和键盘导航
  7. 关注性能 - 缓存数据、防抖处理事件、延迟加载
  8. 协助寻找数据来源 - 用户需要 API 时,进行充分调研

**黄金法则:**构建满足需求的最简单仪表板。不要过度设计。

评论

支持 Markdown