feat: dashboard manifest supports 'supported_games' field, frontend filters by selected game

- DashboardTheme model adds supported_games (default: 'all')
- Built-in dashboards declare game compatibility:
  speedometer/tachometer/gear_indicator -> all
  lap_timer -> forza_horizon_5,forza_horizon_4,forza_motorsport,f1_24
- Dashboard browser filters themes by selected game
- Card shows '全' (all) or '限' (limited) badge
- Updated DASHBOARD_DEV.md with supported_games spec
This commit is contained in:
2026-07-25 15:41:55 +08:00
parent 5e177f4514
commit 2177e65fb4
7 changed files with 46 additions and 10 deletions
+1
View File
@@ -5,6 +5,7 @@
"description": "醒目大圆档位显示,适合竖屏手机副屏",
"author": "Yei.J. (AskaEth)",
"version": "1.0.0",
"supported_games": "all",
"config": {
"icon": "⚙️",
"aspect_ratio": "1:1",
+2 -1
View File
@@ -2,9 +2,10 @@
"id": "lap_timer",
"name": "圈速计时器",
"category": "timing",
"description": "当前圈速、最佳圈和上圈时间对比显示",
"description": "当前圈速、最佳圈和上圈时间对比显示。仅支持 Forza 系列和 F1",
"author": "Yei.J. (AskaEth)",
"version": "1.0.0",
"supported_games": "forza_horizon_5,forza_horizon_4,forza_motorsport,f1_24",
"config": {
"icon": "⏱️",
"aspect_ratio": "4:3",
+1 -1
View File
@@ -5,10 +5,10 @@
"description": "大字号实时速度显示,支持km/h和mph,适合横屏全屏显示",
"author": "Yei.J. (AskaEth)",
"version": "1.0.0",
"supported_games": "all",
"config": {
"icon": "🏎️",
"unit": "kmh",
"show_unit": true,
"aspect_ratio": "auto",
"render_mode": "contain"
}
+1
View File
@@ -5,6 +5,7 @@
"description": "赛车风格弧形转速表,支持红区换档提示,推荐横屏",
"author": "Yei.J. (AskaEth)",
"version": "1.0.0",
"supported_games": "all",
"config": {
"icon": "⏱️",
"max_rpm": 8000,
+2
View File
@@ -21,6 +21,7 @@ dashboards/my_dashboard/
"description": "简短描述",
"author": "你的名字",
"version": "1.0.0",
"supported_games": "all",
"config": {
"icon": "🏎️",
"aspect_ratio": "16:9",
@@ -37,6 +38,7 @@ dashboards/my_dashboard/
| `description` | string | 否 | 描述文字 |
| `author` | string | 否 | 作者 |
| `version` | string | 否 | 版本号 |
| `supported_games` | string | 否 | 支持的游戏: `"all"` 或 ID 逗号分隔 (如 `"forza_horizon_5,f1_24"`) |
| `config.icon` | string | 否 | 预览图标 emoji |
| `config.aspect_ratio` | string | 否 | 渲染比例: `auto` / `16:9` / `4:3` / `1:1` / `21:9` |
| `config.render_mode` | string | 否 | 渲染模式: `contain` / `cover` / `fill` / `center` |
+4
View File
@@ -30,6 +30,7 @@ class DashboardTheme:
icon: str = "📊"
aspect_ratio: str = "auto"
render_mode: str = "contain"
supported_games: str = "all"
created_at: str = ""
updated_at: str = ""
is_builtin: bool = False
@@ -46,6 +47,7 @@ class DashboardTheme:
"icon": self.icon,
"aspect_ratio": self.aspect_ratio,
"render_mode": self.render_mode,
"supported_games": self.supported_games,
"created_at": self.created_at,
"updated_at": self.updated_at,
"is_builtin": self.is_builtin,
@@ -67,6 +69,7 @@ class DashboardTheme:
icon=config.get("icon", "📊"),
aspect_ratio=config.get("aspect_ratio", "auto"),
render_mode=config.get("render_mode", "contain"),
supported_games=data.get("supported_games", "all"),
created_at=data.get("created_at", ""),
updated_at=data.get("updated_at", ""),
is_builtin=is_builtin,
@@ -137,6 +140,7 @@ class DashboardManager:
"description": theme.description,
"author": theme.author,
"version": theme.version,
"supported_games": theme.supported_games,
"config": {
"icon": theme.icon,
"aspect_ratio": theme.aspect_ratio,
+35 -8
View File
@@ -1,12 +1,15 @@
const PageDashboard = {
_el: null,
_currentCategory: 'all',
_selectedGameId: null,
init() {
this._el = document.getElementById('page-dashboard');
},
async render() {
const cfg = await API.getConfig();
this._selectedGameId = cfg.selected_game_id;
this._el.innerHTML = this._template();
await this._loadCategories();
await this._loadThemes();
@@ -44,14 +47,18 @@ const PageDashboard = {
gridEl.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-tertiary);">加载中...</div>';
try {
const themes = await API.getDashboards(this._currentCategory);
let themes = await API.getDashboards(this._currentCategory);
if (!themes || themes.length === 0) {
gridEl.innerHTML = `
<div class="empty-state" style="grid-column:1/-1;">
<svg viewBox="0 0 24 24" width="64" height="64"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-12c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z" fill="currentColor"/></svg>
<h4>暂无仪表盘主题</h4>
<p>创建或导入你的第一个仪表盘主题</p>
</div>`;
gridEl.innerHTML = this._emptyHtml();
return;
}
if (this._selectedGameId) {
themes = themes.filter(t => this._isGameSupported(t));
}
if (themes.length === 0) {
gridEl.innerHTML = this._emptyHtml('当前游戏没有兼容的仪表盘');
return;
}
@@ -62,6 +69,22 @@ const PageDashboard = {
}
},
_isGameSupported(theme) {
const supported = theme.supported_games || 'all';
if (supported === 'all') return true;
const ids = supported.split(',').map(s => s.trim());
return ids.includes(this._selectedGameId);
},
_emptyHtml(msg) {
return `
<div class="empty-state" style="grid-column:1/-1;">
<svg viewBox="0 0 24 24" width="64" height="64"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-12c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z" fill="currentColor"/></svg>
<h4>${msg || '暂无仪表盘主题'}</h4>
<p>创建或导入你的仪表盘主题</p>
</div>`;
},
_bindEvents() {
const categoryList = document.getElementById('dash-category-list');
if (categoryList) {
@@ -133,6 +156,10 @@ const PageDashboard = {
_themeCardHtml(theme) {
const icon = theme.icon || '📊';
const aspect = theme.aspect_ratio || 'auto';
const supported = theme.supported_games || 'all';
const isAll = supported === 'all';
const compatLabel = isAll ? '全' : '限';
const compatClass = isAll ? 'badge-info' : 'badge-warning';
return `
<div class="glass-card theme-card" data-theme-id="${theme.id}">
<div class="theme-card-preview">${icon}</div>
@@ -144,7 +171,7 @@ const PageDashboard = {
</div>
<div class="theme-card-meta" style="margin-top:4px;">
<span>${theme.author || ''}</span>
<span class="badge ${theme.is_builtin ? 'badge-info' : 'badge-success'}">${theme.is_builtin ? '内置' : '自定义'}</span>
<span class="badge ${compatClass}">${compatLabel}</span>
</div>
</div>
</div>`;