diff --git a/panel/server/src/store.ts b/panel/server/src/store.ts index 9d78238..d69d809 100644 --- a/panel/server/src/store.ts +++ b/panel/server/src/store.ts @@ -43,6 +43,7 @@ export interface Instance { id: string; // 短 id,用于容器/卷命名 name: string; // 显示名 appType?: AppType; // 承载的应用类型;缺省(老实例)= wechat(见 instanceAppType) + icon?: string; // 自定义图标:data: 图片(base64) 或 builtin:;缺省按 appType 取默认图标 containerName: string; // woc-wx- volumeName: string; // woc-data- kasmUser: string; // 随机生成,服务端注入反代,永不下发前端 @@ -216,6 +217,7 @@ export function publicInstance(i: Instance) { id: i.id, name: i.name, appType: instanceAppType(i), // 老实例无字段时回退 wechat + icon: i.icon, createdAt: i.createdAt, createdBy: i.createdBy, memSoftLimitMB: i.memSoftLimitMB, diff --git a/panel/web/src/AppIcon.tsx b/panel/web/src/AppIcon.tsx new file mode 100644 index 0000000..f84d667 --- /dev/null +++ b/panel/web/src/AppIcon.tsx @@ -0,0 +1,74 @@ +import type { AppType } from './api'; + +// 实例图标。支持三种来源(优先级从高到低): +// 1) 自定义上传/裁剪的图片 → inst.icon = "data:image/...;base64,..." +// 2) 内置图标 → inst.icon = "builtin:"(如 builtin:xiaohongshu) +// 3) 缺省:按 appType 给默认图标(微信 / Chromium / Telegram / 通用) +// 内置图标用简洁 SVG(彩色圆角块 + 白色字形),风格统一、无需联网抓取。后续可往 BUILTIN 里加更多平台。 + +type Glyph = { bg: string; el: JSX.Element }; +const G = (bg: string, el: JSX.Element): Glyph => ({ bg, el }); + +// 白色字形(viewBox 0 0 48 48,置于彩色圆角块上) +const chat = ( + +); +const globe = ( + + + + + +); +const plane = ; +const dots = ( + + + + + +); + +// key → 字形。default-by-appType 与「内置图标选择器」共用同一张表。 +export const BUILTIN_ICONS: Record = { + wechat: G('#07c160', chat), + chromium: G('#4285f4', globe), + telegram: G('#2aabee', plane), + globe: G('#5b8def', globe), + app: G('#8a9099', dots), +}; +const DEFAULT_BY_APP: Record = { + wechat: 'wechat', + chromium: 'chromium', + telegram: 'telegram', + custom: 'app', +}; + +export function InstanceIcon({ + icon, + appType, + size = 36, + radius = 12, +}: { + icon?: string; + appType?: AppType; + size?: number; + radius?: number; +}) { + // 1) 自定义图片 + if (icon && icon.startsWith('data:')) { + return ; + } + // 2) 内置 / 3) 默认 + const key = icon && icon.startsWith('builtin:') ? icon.slice(8) : DEFAULT_BY_APP[appType ?? 'wechat'] ?? 'app'; + const g = BUILTIN_ICONS[key] ?? BUILTIN_ICONS.app; + return ( + + ); +} diff --git a/panel/web/src/AppShell.tsx b/panel/web/src/AppShell.tsx index 15b1bf5..48a66cf 100644 --- a/panel/web/src/AppShell.tsx +++ b/panel/web/src/AppShell.tsx @@ -2,7 +2,8 @@ import { createContext, useContext, useEffect, useRef, useState, type ReactNode import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { useAuth } from './auth'; import { useUI, PasswordInput } from './ui'; -import { api, type InstanceWithStatus } from './api'; +import { api, appProfile, type InstanceWithStatus } from './api'; +import { InstanceIcon } from './AppIcon'; import InstanceView from './pages/Desktop'; import Admin from './pages/Admin'; @@ -175,7 +176,7 @@ function Sidebar({ collapsed, onToggleCollapsed }: { collapsed: boolean; onToggl - {!collapsed &&
微信实例
} + {!collapsed &&
实例
}
{instances.length === 0 && !collapsed &&
暂无可用实例
} {instances.map((inst) => { @@ -184,7 +185,7 @@ function Sidebar({ collapsed, onToggleCollapsed }: { collapsed: boolean; onToggl return (
) : (
{instances.map((inst) => { const st = statusOf(inst); + const prof = appProfile(inst.appType); const meta = inst.wechat.installed - ? `微信 ${inst.wechat.version || ''}`.trim() - : inst.runtime === 'running' - ? '待下载安装微信' + ? `${prof.label} ${inst.wechat.version || ''}`.trim() + : inst.runtime === 'running' && prof.needsInstall + ? `待下载安装${prof.label}` : ''; return (