关于 dashboard
dashboard 是用于构建数据仪表板的技能,采用 HTML、CSS 和 JavaScript。内容涵盖数据源调研、图表选择、响应式布局,以及通过 SSE 或 WebSocket 实现实时更新。
主要功能
- 01调研并评估数据源
- 02生成响应式 HTML/CSS/JS 仪表板
- 03集成图表库
- 04添加 SSE 或 WebSocket 更新
- 05遵循无障碍与设计规范
适用场景
- 01使用公共 API 构建仪表板
- 02展示频繁更新的指标
- 03为仪表板数据选择图表
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ starchild-ai-agent/ official-skills --skill dashboard安装后在 Claude Code 中输入/dashboard确认已加载。
详细介绍
仪表板创建技能
本技能将指导你如何创建专业的 HTML/CSS/JavaScript 仪表板,支持数据可视化、实时更新以及与免费数据源集成。基于 2025-2026 年的最佳实践和现代架构模式。
核心能力
- 数据源发现 - 调研并推荐适用于仪表板数据的免费 API
- 仪表板生成 - 创建响应式 HTML/CSS/JS 仪表板
- 可视化集成 - 使用现代库实现图表
- 实时更新 - 使用 SSE 或 WebSocket 添加实时数据流
- 最佳实践 - 遵循 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:
- 检查是否存在对应的
*_API_KEY环境变量 - 使用
web_fetch查看 API 文档(搜索“{api_name} API documentation”) - 这些 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
何时调研免费替代方案:
- 计算出的每日调用次数超过高级 API 配额
- 用户明确要求免费/无需身份验证的方案
- 仪表板面向公众(用户越多,请求量越大)
- 处于原型设计/测试阶段(节省高级 API 配额)
- 用户尚未配置 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 年的博客文章提出建议。
总结:研究优先的方法
仪表板技能的优先事项:
- 先计算再选择:始终根据更新频率和资产数量计算 API 调用总量
- 适当使用高级方案:中低频场景和历史数据使用 Star Child 高级 API
- 必要时开展研究:针对高频、无需认证或有专门需求的场景进行深入研究
- 提供多个选项:为用户提供 2-4 个经过研究的选项,并清楚说明利弊
- 确认信息是否最新:优先采用 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>
移动优先的响应式设计
方法:
- 首先针对移动设备(320px)进行设计
- 渐进增强,适配平板(768px+)
- 桌面布局(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¤t_weather=true',
'weather-berlin'
);
9. 部署与托管
静态托管(无后端)
如果仪表板仅使用客户端 JavaScript 和公共 API:
选项:
- GitHub Pages - 免费,适用于静态网站,操作简单
- Netlify - 免费套餐、即时部署、自定义域名
- Vercel - 免费套餐、性能出色
- 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. 资源与参考资料
官方文档
- Chart.js:https://www.chartjs.org/docs/
- ApexCharts:https://apexcharts.com/docs/
- D3.js:https://d3js.org/
- Tabler:https://tabler.io/docs/
- Bootstrap:https://getbootstrap.com/docs/
API 目录
- 公共 API(900+):https://github.com/public-apis/public-apis
- 免费公共 API:https://www.freepublicapis.com/
- RapidAPI Hub:https://rapidapi.com/hub
设计资源
学习资源
- MDN Web Docs:https://developer.mozilla.org/
- Web.dev(性能):https://web.dev/
- A11y(无障碍):https://www.a11yproject.com/
总结
创建仪表板时:
- 从需求入手 - 了解用户需求、数据来源和设备
- 明智选择 - 追求速度就选模板(Tabler),需要掌控力就选自定义
- 默认选择 Chart.js - 简单、可靠,适用于 90% 的情况
- 实时更新使用 SSE - 对大多数仪表板而言,比 WebSocket 更简单
- 始终优先考虑移动端 - 先针对最小屏幕进行设计
- 重视无障碍 - 使用色盲友好配色、ARIA 标签和键盘导航
- 关注性能 - 缓存数据、防抖处理事件、延迟加载
- 协助寻找数据来源 - 用户需要 API 时,进行充分调研
**黄金法则:**构建满足需求的最简单仪表板。不要过度设计。