mirror of
https://github.com/Gloridust/WechatOnCloud.git
synced 2026-06-16 19:53:53 +08:00
feat(v1.2.0): 实例头像由首字母改为按应用类型的图标(自定义图标铺底)
新增 AppIcon.tsx(InstanceIcon + 内置 SVG 图标:微信/Chromium/Telegram/通用),侧栏与主页 卡片头像改用它(按 appType 出默认图标;data: 图片 / builtin:<key> 优先)。 新增 Instance.icon 字段 + publicInstance 下发,为「自定义图标(内置选择 + 上传裁剪)」铺底。 侧栏/主页标题「微信实例」→「实例」,主页副文案按应用名泛化。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ export interface Instance {
|
||||
id: string; // 短 id,用于容器/卷命名
|
||||
name: string; // 显示名
|
||||
appType?: AppType; // 承载的应用类型;缺省(老实例)= wechat(见 instanceAppType)
|
||||
icon?: string; // 自定义图标:data: 图片(base64) 或 builtin:<key>;缺省按 appType 取默认图标
|
||||
containerName: string; // woc-wx-<id>
|
||||
volumeName: string; // woc-data-<id>
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { AppType } from './api';
|
||||
|
||||
// 实例图标。支持三种来源(优先级从高到低):
|
||||
// 1) 自定义上传/裁剪的图片 → inst.icon = "data:image/...;base64,..."
|
||||
// 2) 内置图标 → inst.icon = "builtin:<key>"(如 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 = (
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M19 12c-6.6 0-12 4.2-12 9.5 0 3 1.8 5.7 4.6 7.4l-1.1 3.9 4.4-2.3c1.3.3 2.7.5 4.1.5 6.6 0 12-4.2 12-9.5S25.6 12 19 12zm-4 8.2a1.6 1.6 0 110-3.2 1.6 1.6 0 010 3.2zm8 0a1.6 1.6 0 110-3.2 1.6 1.6 0 010 3.2z"
|
||||
/>
|
||||
);
|
||||
const globe = (
|
||||
<g fill="none" stroke="#fff" strokeWidth="2.4">
|
||||
<circle cx="24" cy="24" r="13" />
|
||||
<ellipse cx="24" cy="24" rx="5.5" ry="13" />
|
||||
<path d="M11.5 20h25M11.5 28h25" />
|
||||
</g>
|
||||
);
|
||||
const plane = <path fill="#fff" d="M35 14L13 23.2l6.1 2.2 2.3 7.2 3.3-3.9 5.6 4.1L35 14zm-12.4 12.6l9-7.2-6.7 8.1-.1 3.6-2.2-4.5z" />;
|
||||
const dots = (
|
||||
<g fill="#fff">
|
||||
<circle cx="16" cy="24" r="2.6" />
|
||||
<circle cx="24" cy="24" r="2.6" />
|
||||
<circle cx="32" cy="24" r="2.6" />
|
||||
</g>
|
||||
);
|
||||
|
||||
// key → 字形。default-by-appType 与「内置图标选择器」共用同一张表。
|
||||
export const BUILTIN_ICONS: Record<string, Glyph> = {
|
||||
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<AppType, string> = {
|
||||
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 <img src={icon} width={size} height={size} alt="" style={{ borderRadius: radius, objectFit: 'cover', display: 'block' }} />;
|
||||
}
|
||||
// 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 (
|
||||
<svg width={size} height={size} viewBox="0 0 48 48" style={{ display: 'block' }} aria-hidden="true">
|
||||
<rect width="48" height="48" rx={(radius / size) * 48} fill={g.bg} />
|
||||
{g.el}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
+14
-10
@@ -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
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{!collapsed && <div className="sb-section">微信实例</div>}
|
||||
{!collapsed && <div className="sb-section">实例</div>}
|
||||
<div className="sb-list">
|
||||
{instances.length === 0 && !collapsed && <div className="sb-empty">暂无可用实例</div>}
|
||||
{instances.map((inst) => {
|
||||
@@ -184,7 +185,7 @@ function Sidebar({ collapsed, onToggleCollapsed }: { collapsed: boolean; onToggl
|
||||
return (
|
||||
<button key={inst.id} className={'sb-item sb-inst' + (on ? ' on' : '')} onClick={() => go(`/i/${inst.id}`)} title={inst.name}>
|
||||
<span className="sb-avatar">
|
||||
{inst.name.slice(0, 1)}
|
||||
<InstanceIcon icon={inst.icon} appType={inst.appType} size={34} radius={10} />
|
||||
<span className={'sb-dot ' + st.cls} />
|
||||
</span>
|
||||
{!collapsed && <span className="sb-label">{inst.name}</span>}
|
||||
@@ -252,7 +253,7 @@ function HomeView({ onOpenMenu, onChangePassword }: { onOpenMenu: () => void; on
|
||||
)}
|
||||
|
||||
<div className="section-row">
|
||||
<span className="section-title">我的微信实例</span>
|
||||
<span className="section-title">我的实例</span>
|
||||
{isAdmin && (
|
||||
<button className="btn-text" onClick={() => nav('/admin')}>
|
||||
管理 ›
|
||||
@@ -265,21 +266,24 @@ function HomeView({ onOpenMenu, onChangePassword }: { onOpenMenu: () => void; on
|
||||
<div className="empty-blob">
|
||||
<img src="/favicon.svg" alt="" />
|
||||
</div>
|
||||
<div className="empty-title">还没有微信实例</div>
|
||||
<div className="empty-sub">{isAdmin ? '去「管理」新建一个微信实例' : '请联系管理员为你分配实例'}</div>
|
||||
<div className="empty-title">还没有实例</div>
|
||||
<div className="empty-sub">{isAdmin ? '去「管理」新建一个实例' : '请联系管理员为你分配实例'}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="inst-grid">
|
||||
{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 (
|
||||
<button key={inst.id} className="home-card" onClick={() => nav(`/i/${inst.id}`)}>
|
||||
<span className="home-card-av">{inst.name.slice(0, 1)}</span>
|
||||
<span className="home-card-av">
|
||||
<InstanceIcon icon={inst.icon} appType={inst.appType} size={42} radius={12} />
|
||||
</span>
|
||||
<span className="home-card-main">
|
||||
<span className="home-card-name">{inst.name}</span>
|
||||
<span className="home-card-meta">
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface PanelInstance {
|
||||
id: string;
|
||||
name: string;
|
||||
appType?: AppType; // 缺省(老实例)= wechat
|
||||
icon?: string; // 自定义图标:data: 图片 / builtin:<key>;缺省按 appType 取默认图标
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
memSoftLimitMB?: number;
|
||||
|
||||
@@ -1397,15 +1397,6 @@ button {
|
||||
flex: none;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(150deg, var(--wx-green), var(--wx-green-dark));
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.sb-dot {
|
||||
position: absolute;
|
||||
@@ -1562,15 +1553,6 @@ button {
|
||||
flex: none;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(150deg, var(--wx-green), var(--wx-green-dark));
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.home-card-main {
|
||||
position: relative;
|
||||
|
||||
Reference in New Issue
Block a user