mirror of
https://github.com/Gloridust/WechatOnCloud.git
synced 2026-06-16 19:53:53 +08:00
feat(v1.2.0): UI 按应用类型泛化(不再到处写死「微信」)
新增 APP_PROFILES(label/needsInstall/enterHint/updateLabel)+ appProfile()。 - 实例卡片:状态副文案、"进入实例"提示、安装/更新按钮均按 appType 显示;Chromium 已烤进镜像 (needsInstall=false)故不显示"下载安装/更新",状态直接"已就绪"。 - 桌面页:连接提示语用 enterHint(微信=扫码登录,Chromium=直接使用);安装中/未安装提示、iframe 标题、文件传输/剪贴板/输入条文案改为应用名或通用"应用/桌面"。 - 列表区"微信实例"→"实例"、空状态泛化。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,24 @@ export const APP_LABELS: Record<AppType, string> = {
|
||||
chromium: 'Chromium',
|
||||
custom: '自定义应用',
|
||||
};
|
||||
|
||||
// 各应用的 UI 画像,供卡片/桌面页按类型显示正确文案(避免到处写死「微信」)。
|
||||
// needsInstall: 是否需要运行时下载安装(微信/Telegram 是;Chromium 已烤进镜像、即创建即就绪)。
|
||||
// enterHint: 首次进入实例的提示。
|
||||
// updateLabel: 「管理」菜单里的更新按钮文案(needsInstall=false 时不显示)。
|
||||
export interface AppProfile {
|
||||
label: string;
|
||||
needsInstall: boolean;
|
||||
enterHint: string;
|
||||
updateLabel: string;
|
||||
}
|
||||
export const APP_PROFILES: Record<AppType, AppProfile> = {
|
||||
wechat: { label: '微信', needsInstall: true, enterHint: '首次进入请扫码登录微信', updateLabel: '更新微信' },
|
||||
telegram: { label: 'Telegram', needsInstall: true, enterHint: '首次进入请登录 Telegram', updateLabel: '更新 Telegram' },
|
||||
chromium: { label: 'Chromium', needsInstall: false, enterHint: '浏览器已就绪,直接使用即可', updateLabel: '' },
|
||||
custom: { label: '自定义应用', needsInstall: true, enterHint: '', updateLabel: '更新' },
|
||||
};
|
||||
export const appProfile = (t?: AppType): AppProfile => APP_PROFILES[t ?? 'wechat'] ?? APP_PROFILES.wechat;
|
||||
export interface PanelInstance {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { api, APP_LABELS, type PanelUser, type InstanceWithStatus, type VolEntry, type AppType } from '../api';
|
||||
import { api, APP_LABELS, appProfile, type PanelUser, type InstanceWithStatus, type VolEntry, type AppType } from '../api';
|
||||
import { useUI, PasswordInput } from '../ui';
|
||||
import { useAuth } from '../auth';
|
||||
|
||||
@@ -271,7 +271,7 @@ export default function Admin({ onOpenMenu, onChangePassword }: { onOpenMenu: ()
|
||||
{isAdmin && (
|
||||
<>
|
||||
<div className="section-row">
|
||||
<span className="section-title">微信实例</span>
|
||||
<span className="section-title">实例</span>
|
||||
<button className="btn-text" onClick={() => setCreatingInst(true)}>
|
||||
+ 新建实例
|
||||
</button>
|
||||
@@ -279,11 +279,11 @@ export default function Admin({ onOpenMenu, onChangePassword }: { onOpenMenu: ()
|
||||
{instances.length === 0 ? (
|
||||
<EmptyState
|
||||
icon="🖥️"
|
||||
title="还没有微信实例"
|
||||
sub="新建一个实例,进入后扫码登录即可在浏览器里用微信"
|
||||
title="还没有实例"
|
||||
sub="新建一个实例(微信 / Chromium 浏览器),进入后即可在浏览器里使用"
|
||||
action={
|
||||
<button className="btn btn-primary" onClick={() => setCreatingInst(true)}>
|
||||
+ 新建微信实例
|
||||
+ 新建实例
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
@@ -899,6 +899,8 @@ function InstanceAdminCard({
|
||||
return () => document.removeEventListener('mousedown', onDocDown);
|
||||
}, [menuOpen]);
|
||||
|
||||
const profile = appProfile(inst.appType);
|
||||
|
||||
let badge: { text: string; cls: string };
|
||||
if (acting) badge = { text: '处理中', cls: 'tag-busy' };
|
||||
else if (offline) badge = { text: inst.runtime === 'missing' ? '未创建' : '已停止', cls: 'tag-off' };
|
||||
@@ -911,8 +913,8 @@ function InstanceAdminCard({
|
||||
else if (busy) sub = wx.percent >= 0 ? `${wx.message || '处理中'} ${wx.percent}%` : wx.message || '请稍候…';
|
||||
else if (wx.phase === 'error') sub = wx.message || '操作失败,可重试';
|
||||
else if (offline) sub = inst.runtime === 'missing' ? '容器尚未创建' : '容器已停止';
|
||||
else if (installed) sub = wx.version ? `微信 ${wx.version}` : '微信已安装';
|
||||
else sub = '微信尚未安装';
|
||||
else if (installed) sub = wx.version ? `${profile.label} ${wx.version}` : `${profile.label}已就绪`;
|
||||
else sub = `${profile.label}尚未安装`;
|
||||
|
||||
return (
|
||||
<div className={'inst-card' + (menuOpen ? ' open-menu' : '')}>
|
||||
@@ -943,7 +945,7 @@ function InstanceAdminCard({
|
||||
{inst.runtime === 'missing' ? '创建并启动' : '启动实例'}
|
||||
</button>
|
||||
) : (
|
||||
<button className="btn btn-primary inst-act-wide" disabled={!installed} onClick={onEnter} title={installed ? '' : '需先下载安装微信'}>
|
||||
<button className="btn btn-primary inst-act-wide" disabled={!installed} onClick={onEnter} title={installed ? '' : '需先下载安装' + profile.label}>
|
||||
进入实例
|
||||
</button>
|
||||
)}
|
||||
@@ -960,9 +962,9 @@ function InstanceAdminCard({
|
||||
<div className="inst-menu-group">
|
||||
<div className="inst-menu-label">运维</div>
|
||||
<div className="inst-menu-items">
|
||||
{!offline && (
|
||||
{!offline && profile.needsInstall && (
|
||||
<button className="btn-text" onClick={() => onTrigger(inst, installed ? 'update' : 'install')}>
|
||||
{installed ? '更新微信' : '下载安装'}
|
||||
{installed ? profile.updateLabel : '下载安装'}
|
||||
</button>
|
||||
)}
|
||||
<button className="btn-text" onClick={onUpgrade} title="拉取最新镜像并重建(保留聊天记录)">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { api } from '../api';
|
||||
import { api, appProfile } from '../api';
|
||||
import { useUI } from '../ui';
|
||||
import { useAuth } from '../auth';
|
||||
import { useInstances } from '../AppShell';
|
||||
@@ -150,6 +150,8 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
const audioRef = useRef<VncAudio | null>(null);
|
||||
|
||||
const inst = instances.find((i) => i.id === id);
|
||||
const profile = appProfile(inst?.appType); // 按应用类型显示正确文案(微信/Chromium…)
|
||||
const appLabel = profile.label;
|
||||
// 进入实例时,共享列表可能尚未同步(管理页新建/安装后),先按"探测中"显示加载态,
|
||||
// 等列表刷新到该实例或超时后再判定是否真的不存在,避免从管理页跳转时误报"实例不存在"。
|
||||
const [probing, setProbing] = useState(true);
|
||||
@@ -366,7 +368,7 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
}
|
||||
setUploading(false);
|
||||
if (ok) {
|
||||
toast(`已上传 ${ok} 个文件到桌面,微信里可直接选取`, 'ok');
|
||||
toast(`已上传 ${ok} 个文件到桌面,应用里可直接取用`, 'ok');
|
||||
refreshFiles();
|
||||
}
|
||||
};
|
||||
@@ -379,7 +381,7 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
};
|
||||
|
||||
const delFile = async (name: string) => {
|
||||
if (!(await confirm({ title: `删除「${name}」?`, body: '将从微信桌面(~/Desktop)移除该文件。', danger: true, confirmText: '删除' }))) return;
|
||||
if (!(await confirm({ title: `删除「${name}」?`, body: '将从桌面(~/Desktop)移除该文件。', danger: true, confirmText: '删除' }))) return;
|
||||
try {
|
||||
await api.deleteFile(id, name);
|
||||
toast('已删除', 'ok');
|
||||
@@ -445,7 +447,7 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
return;
|
||||
}
|
||||
if (pushClipboardToRemote(t)) {
|
||||
toast('已发送到容器剪贴板,请在微信输入框按 Ctrl+V 粘贴', 'ok');
|
||||
toast('已发送到容器剪贴板,请在应用输入框按 Ctrl+V 粘贴', 'ok');
|
||||
} else {
|
||||
toast('发送失败:桌面尚未连接', 'error');
|
||||
}
|
||||
@@ -486,7 +488,7 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
const restartInstance = async () => {
|
||||
const ok = await confirm({
|
||||
title: '重启该实例?',
|
||||
body: '会重建容器(聊天记录保留),微信重新启动,约十几秒;用于修复卡死/最小化丢失等。',
|
||||
body: `会重建容器(数据保留),${appLabel}重新启动,约十几秒;用于修复卡死/最小化丢失等。`,
|
||||
confirmText: '重启',
|
||||
});
|
||||
if (!ok) return;
|
||||
@@ -525,7 +527,7 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
}
|
||||
};
|
||||
|
||||
const title = inst?.name || '微信实例';
|
||||
const title = inst?.name || '实例';
|
||||
|
||||
return (
|
||||
<div className="ws-page">
|
||||
@@ -550,8 +552,8 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
className={'ws-action' + (inputMode === 'seamless' ? ' on' : '')}
|
||||
title={
|
||||
inputMode === 'seamless'
|
||||
? '无感输入:直接在微信输入框里打中文(提交后转发,已修复混数字丢字)。点击切回「转发输入条」'
|
||||
: '转发输入:用底部输入条打中文,最稳。点击切到「无感输入」(直接在微信里打)'
|
||||
? '无感输入:直接在应用输入框里打中文(提交后转发,已修复混数字丢字)。点击切回「转发输入条」'
|
||||
: '转发输入:用底部输入条打中文,最稳。点击切到「无感输入」(直接在应用里打)'
|
||||
}
|
||||
onClick={() => setMode(inputMode === 'seamless' ? 'forward' : 'seamless')}
|
||||
>
|
||||
@@ -609,7 +611,7 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
<div className="iv-stage iv-center">
|
||||
<div className="iv-notice">
|
||||
<div className="spinner" />
|
||||
<div className="iv-notice-title">微信安装中…</div>
|
||||
<div className="iv-notice-title">{appLabel}安装中…</div>
|
||||
<div className="iv-notice-sub">
|
||||
{inst.wechat.message || '请稍候'}
|
||||
{inst.wechat.percent >= 0 ? ` · ${inst.wechat.percent}%` : ''} ——完成后自动进入,无需刷新
|
||||
@@ -619,18 +621,18 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
) : !installed ? (
|
||||
<div className="iv-stage iv-center">
|
||||
<div className="iv-notice">
|
||||
<div className="iv-notice-title">{inst.wechat.phase === 'error' ? '微信安装出错' : '微信尚未安装'}</div>
|
||||
<div className="iv-notice-title">{inst.wechat.phase === 'error' ? `${appLabel}安装出错` : `${appLabel}尚未安装`}</div>
|
||||
<div className="iv-notice-sub">
|
||||
{inst.wechat.phase === 'error'
|
||||
? inst.wechat.message || '安装失败,可在「管理」重试'
|
||||
: '该实例容器已就绪,但尚未安装微信'}
|
||||
: `该实例容器已就绪,但尚未安装${appLabel}`}
|
||||
</div>
|
||||
{isAdmin ? (
|
||||
<button className="btn btn-primary iv-notice-btn" onClick={() => nav('/admin')}>
|
||||
去「管理」{inst.wechat.phase === 'error' ? '重试 / 更新' : '下载安装'}
|
||||
</button>
|
||||
) : (
|
||||
<div className="iv-notice-sub">请联系管理员在「管理」中下载安装微信</div>
|
||||
<div className="iv-notice-sub">请联系管理员在「管理」中下载安装{appLabel}</div>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<button className="btn-text" onClick={() => window.open(api.instanceLogsUrl(id), '_blank')}>
|
||||
@@ -647,7 +649,7 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
ref={frameRef}
|
||||
className="iv-frame"
|
||||
src={desktopUrl(id)}
|
||||
title="电脑版微信"
|
||||
title={`${appLabel} · 实例桌面`}
|
||||
allow="clipboard-read; clipboard-write; microphone; camera; autoplay"
|
||||
onLoad={() => {
|
||||
setFrameLoaded(true);
|
||||
@@ -664,7 +666,7 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
<div className="iv-loading">
|
||||
<div className="spinner" />
|
||||
<div className="iv-loading-text">正在连接桌面…</div>
|
||||
<div className="iv-loading-sub">首次进入请扫码登录微信</div>
|
||||
<div className="iv-loading-sub">{profile.enterHint}</div>
|
||||
<div className="iv-loading-sub">拖文件到此处即可上传;声音自动开启,点一下画面即可出声</div>
|
||||
{!window.isSecureContext && (
|
||||
<div className="iv-loading-warn">当前非 HTTPS 访问,浏览器将禁用麦克风与摄像头(音频播放不受影响)</div>
|
||||
@@ -703,8 +705,8 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
<div className="iv-drop" onDrop={onDrop} onDragOver={(e) => e.preventDefault()}>
|
||||
<div className="drop-card">
|
||||
<div className="drop-icon">⬇</div>
|
||||
<div className="drop-title">松开上传到微信桌面</div>
|
||||
<div className="drop-sub">上传后在微信里「+ / 文件」选择即可</div>
|
||||
<div className="drop-title">松开上传到桌面</div>
|
||||
<div className="drop-sub">上传后在应用里「+ / 文件」选择即可</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -742,7 +744,7 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
<button className="btn btn-primary files-upload" disabled={uploading} onClick={() => fileInput.current?.click()}>
|
||||
{uploading ? '上传中…' : '+ 选择文件上传'}
|
||||
</button>
|
||||
<div className="files-hint">也可直接把文件拖进来。下方为桌面(~/Desktop)里的文件,微信收到的文件另存到桌面即可在此下载。</div>
|
||||
<div className="files-hint">也可直接把文件拖进来。下方为桌面(~/Desktop)里的文件,应用收到的文件另存到桌面即可在此下载。</div>
|
||||
<div className="files-list">
|
||||
{files.length === 0 && (
|
||||
<div className="muted small" style={{ padding: '10px 2px' }}>
|
||||
@@ -776,17 +778,17 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
className="clip-area"
|
||||
value={clipText}
|
||||
onChange={(e) => setClipText(e.target.value)}
|
||||
placeholder="在此输入或粘贴文本,点「发送到微信」后到微信输入框按 Ctrl+V 粘贴"
|
||||
placeholder="在此输入或粘贴文本,点「发送到剪贴板」后到应用输入框按 Ctrl+V 粘贴"
|
||||
rows={5}
|
||||
/>
|
||||
<button className="btn btn-primary files-upload" onClick={sendClip}>
|
||||
发送到微信(容器剪贴板)
|
||||
发送到剪贴板
|
||||
</button>
|
||||
<button className="btn-text" style={{ alignSelf: 'flex-start', marginTop: 6 }} onClick={pullClipboardFromRemote}>
|
||||
↓ 读取容器剪贴板到此框
|
||||
</button>
|
||||
<div className="files-hint">
|
||||
局域网 http 访问时浏览器会禁用系统级剪贴板同步,故用此框中转:文本→容器剪贴板,再在微信里 Ctrl+V。
|
||||
局域网 http 访问时浏览器会禁用系统级剪贴板同步,故用此框中转:文本→容器剪贴板,再在应用里 Ctrl+V。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -804,7 +806,7 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
||||
sendImeText();
|
||||
}
|
||||
}}
|
||||
placeholder="中文输入这里 → 回车送进微信(先点好微信的输入框)。Shift+回车换行。"
|
||||
placeholder="中文输入这里 → 回车送进应用(先点好应用的输入框)。Shift+回车换行。"
|
||||
rows={1}
|
||||
/>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user