mirror of
https://github.com/Gloridust/WechatOnCloud.git
synced 2026-08-30 18:21:27 -07:00
feat(ui): 自动回车开关 + 功能键排 + 修齿轮图标/子用户文案(issue #125)
1) 转发输入条「自动回车」开关(默认开,保持 issue #81 行为) 关掉后只把文字填进应用输入框、不代发,按钮文案随之变「填入」。 原先无条件补回车,导致想先编辑再发时文本一进框就被发出去, 用户只能靠外部剪贴板工具绕开。开关记在 localStorage。 2) 功能键折叠排(Fn):Esc / Tab / 退格 / Del / 回车 / 方向键 这些键在 VNC 里常被浏览器吞掉,手机端更是没有。经 xdotool 送出, 键名须匹配服务端白名单 /^[A-Za-z_]{1,20}$/,故只放单键不放组合键。 标签用「退格」而非 ⌫(U+232B)、用「Fn」而非 ⌨——实测这两个符号 在容器字体下渲染成豆腐块方框。 3) 齿轮图标:旧路径是手改过的 feather 变体,H9/V9 这类绝对指令混在 相对弧线里,有两颗齿明显走形(右下那颗尤其)。换成 lucide 官方 settings 路径(MIT),12 齿对称。已渲染新旧对比确认。 4) 子用户文案:「重启实例」按钮本就按 isAdmin 隐藏,但两处提示文字没跟着 变,子用户会照着找一个根本不存在的按钮。改为提示联系管理员。 样式复用既有牛奶布艺 token(--crease / --crease-press / --trough / --surface / --muted),未新造阴影值。
This commit is contained in:
@@ -63,7 +63,9 @@ const Icon = {
|
|||||||
),
|
),
|
||||||
gear: (
|
gear: (
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<circle cx="12" cy="12" r="3.2" /><path d="M19.4 13a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-2.7 1.1V21a2 2 0 1 1-4 0v-.1a1.6 1.6 0 0 0-2.7-1.1l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0-1.1-2.7H3a2 2 0 1 1 0-4h.1a1.6 1.6 0 0 0 1.1-2.7l-.1-.1A2 2 0 1 1 6.9 4.5l.1.1a1.6 1.6 0 0 0 1.8.3H9a1.6 1.6 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.6 1.6 0 0 0 2.7 1.1l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0-.3 1.8V9a1.6 1.6 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1z" />
|
{/* 齿轮:原路径是手改过的 feather 变体,H9/V9 这类绝对指令混在相对弧线里,
|
||||||
|
导致有两颗齿明显走形(issue #125)。换成 lucide 官方 settings 路径(MIT),12 齿对称。 */}
|
||||||
|
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
logout: (
|
logout: (
|
||||||
|
|||||||
@@ -133,6 +133,21 @@ function allowAutoRecover(iid: string): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 转发输入条上的功能键(issue #125)。键名走 xdotool,须匹配服务端白名单 /^[A-Za-z_]{1,20}$/,
|
||||||
|
// 故只放单键、不放组合键(ctrl+a 这类含 "+" 会被拒)。
|
||||||
|
const FUNC_KEYS: { key: string; label: string; title: string }[] = [
|
||||||
|
{ key: 'Escape', label: 'Esc', title: 'Escape(关弹窗/退出全屏输入)' },
|
||||||
|
{ key: 'Tab', label: 'Tab', title: 'Tab(切换焦点)' },
|
||||||
|
// 用中文字面而非 ⌫(U+232B):容器/部分系统缺字形会渲染成豆腐块,实测就是方框
|
||||||
|
{ key: 'BackSpace', label: '退格', title: '退格(删除前一个字符)' },
|
||||||
|
{ key: 'Delete', label: 'Del', title: 'Delete(删除后一个字符)' },
|
||||||
|
{ key: 'Return', label: '↵', title: '回车(发送/换行)' },
|
||||||
|
{ key: 'Left', label: '←', title: '左方向键' },
|
||||||
|
{ key: 'Up', label: '↑', title: '上方向键' },
|
||||||
|
{ key: 'Down', label: '↓', title: '下方向键' },
|
||||||
|
{ key: 'Right', label: '→', title: '右方向键' },
|
||||||
|
];
|
||||||
|
|
||||||
const MenuIcon = (
|
const MenuIcon = (
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
||||||
<path d="M4 6h16M4 12h16M4 18h16" />
|
<path d="M4 6h16M4 12h16M4 18h16" />
|
||||||
@@ -189,6 +204,26 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
|||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const [showKeys, setShowKeys] = useState(false);
|
||||||
|
// 转发输入条「发送后自动回车」开关(默认开,保持 issue #81 起的既有行为)。
|
||||||
|
// 关掉的场景(issue #125):想先把长文/多行填进微信输入框里再自己斟酌、编辑后手动发;
|
||||||
|
// 自动回车会让文本一进框就被发出去,也让 Shift+回车换行之外的编辑很别扭。
|
||||||
|
const [autoEnter, setAutoEnter] = useState(() => {
|
||||||
|
try {
|
||||||
|
return window.localStorage.getItem('woc_ime_auto_enter') !== '0';
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const toggleAutoEnter = () => {
|
||||||
|
const v = !autoEnter;
|
||||||
|
setAutoEnter(v);
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem('woc_ime_auto_enter', v ? '1' : '0');
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
};
|
||||||
// 麦克风开关(默认关):仅在「声音」开启时有意义;默认不抢占麦克风,避免把 AirPods 切到低质通话模式。
|
// 麦克风开关(默认关):仅在「声音」开启时有意义;默认不抢占麦克风,避免把 AirPods 切到低质通话模式。
|
||||||
const [micOn, setMicOn] = useState(() => {
|
const [micOn, setMicOn] = useState(() => {
|
||||||
try {
|
try {
|
||||||
@@ -775,16 +810,26 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
|||||||
|
|
||||||
// 中文输入条发送:把本框文本经 xclip+xdotool 直接粘进微信当前聚焦的输入框(绕开 VNC IME)。
|
// 中文输入条发送:把本框文本经 xclip+xdotool 直接粘进微信当前聚焦的输入框(绕开 VNC IME)。
|
||||||
// 在面板的真实 textarea 里用原生输入法打字,100% 可靠,不依赖 VNC 的 enable_ime / 合成事件。
|
// 在面板的真实 textarea 里用原生输入法打字,100% 可靠,不依赖 VNC 的 enable_ime / 合成事件。
|
||||||
|
const pressKey = async (key: string) => {
|
||||||
|
if (!id) return;
|
||||||
|
try {
|
||||||
|
await api.keyInInstance(id, key);
|
||||||
|
} catch (e: any) {
|
||||||
|
toast(e?.message || '按键失败:请确认实例已「升级实例」(镜像含 xdotool)', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const sendImeText = async () => {
|
const sendImeText = async () => {
|
||||||
const t = imeText;
|
const t = imeText;
|
||||||
if (!t.trim() || !id) return;
|
if (!t.trim() || !id) return;
|
||||||
setImeSending(true);
|
setImeSending(true);
|
||||||
try {
|
try {
|
||||||
await api.typeInInstance(id, t);
|
await api.typeInInstance(id, t);
|
||||||
// 打完直接补一个回车把消息发出去(issue #81),焦点【始终留在本输入条】。
|
// 打完补一个回车把消息发出去(issue #81);可由「自动回车」开关关掉(issue #125),
|
||||||
|
// 关掉时只把文字填进应用输入框,发不发由用户自己按。焦点【始终留在本输入条】。
|
||||||
// 切勿在转发模式把焦点切回虚拟机——那等于开了"无感输入",用户接着打的拼音会以原始 keysym 直灌微信
|
// 切勿在转发模式把焦点切回虚拟机——那等于开了"无感输入",用户接着打的拼音会以原始 keysym 直灌微信
|
||||||
// 输入框(出现 "nniih'h你好啊" 这种串码)。下一条仍在本条用本机输入法安全地打。
|
// 输入框(出现 "nniih'h你好啊" 这种串码)。下一条仍在本条用本机输入法安全地打。
|
||||||
await api.keyInInstance(id, 'Return');
|
if (autoEnter) await api.keyInInstance(id, 'Return');
|
||||||
setImeText('');
|
setImeText('');
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
toast(e?.message || '发送失败:请确认实例已「升级实例」(镜像含 xclip/xdotool)', 'error');
|
toast(e?.message || '发送失败:请确认实例已「升级实例」(镜像含 xclip/xdotool)', 'error');
|
||||||
@@ -1031,7 +1076,13 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
|||||||
{!frameLoaded && loadStuck && (
|
{!frameLoaded && loadStuck && (
|
||||||
<div className="iv-loading">
|
<div className="iv-loading">
|
||||||
<div className="iv-loading-text">桌面无响应</div>
|
<div className="iv-loading-text">桌面无响应</div>
|
||||||
<div className="iv-loading-sub">连接超时。可能是实例临时卡住,先「重新连接」;若仍无效请「重启实例」。</div>
|
{/* 子用户无「重启实例」权限(按钮下方已按 isAdmin 隐藏),文案也要跟着变,
|
||||||
|
否则会让人照着找一个根本不存在的按钮(issue #125)。 */}
|
||||||
|
<div className="iv-loading-sub">
|
||||||
|
{isAdmin
|
||||||
|
? '连接超时。可能是实例临时卡住,先「重新连接」;若仍无效请「重启实例」。'
|
||||||
|
: '连接超时。可能是实例临时卡住,请先「重新连接」;若反复无效,请联系管理员重启该实例。'}
|
||||||
|
</div>
|
||||||
<div className="iv-stuck-actions">
|
<div className="iv-stuck-actions">
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
@@ -1046,9 +1097,11 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{isAdmin && (
|
||||||
<div className="iv-loading-sub" style={{ marginTop: 8 }}>
|
<div className="iv-loading-sub" style={{ marginTop: 8 }}>
|
||||||
若反复无响应,点「重启实例」即可恢复(数据保留)。
|
若反复无响应,点「重启实例」即可恢复(数据保留)。
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1216,7 +1269,26 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{inputMode === 'forward' && (
|
{inputMode === 'forward' && (
|
||||||
|
<>
|
||||||
|
{/* 功能键折叠集合(issue #125):VNC 里这些键要么被浏览器吃掉、要么手机端根本没有,
|
||||||
|
给一排直接点。经 xdotool 送出,键名须匹配服务端白名单 /^[A-Za-z_]{1,20}$/。 */}
|
||||||
|
{showKeys && (
|
||||||
|
<div className="iv-keybar">
|
||||||
|
{FUNC_KEYS.map((k) => (
|
||||||
|
<button key={k.key} className="iv-key" title={k.title} onClick={() => pressKey(k.key)}>
|
||||||
|
{k.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="iv-imebar">
|
<div className="iv-imebar">
|
||||||
|
<button
|
||||||
|
className={'iv-imebar-tool' + (showKeys ? ' on' : '')}
|
||||||
|
title="功能键(Esc / Tab / 退格 / 方向键…)"
|
||||||
|
onClick={() => setShowKeys((v) => !v)}
|
||||||
|
>
|
||||||
|
Fn
|
||||||
|
</button>
|
||||||
<textarea
|
<textarea
|
||||||
className="iv-imebar-input"
|
className="iv-imebar-input"
|
||||||
value={imeText}
|
value={imeText}
|
||||||
@@ -1227,17 +1299,33 @@ export default function InstanceView({ onOpenMenu }: { onOpenMenu: () => void })
|
|||||||
sendImeText();
|
sendImeText();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="中文输入这里 → 回车直接发送到应用(先点好应用的输入框)。Shift+回车换行。"
|
placeholder={
|
||||||
|
autoEnter
|
||||||
|
? '中文输入这里 → 回车直接发送到应用(先点好应用的输入框)。Shift+回车换行。'
|
||||||
|
: '中文输入这里 → 回车只把文字填进应用输入框,不自动发送(发送由你按)。Shift+回车换行。'
|
||||||
|
}
|
||||||
rows={1}
|
rows={1}
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
className={'iv-imebar-tool' + (autoEnter ? ' on' : '')}
|
||||||
|
title={
|
||||||
|
autoEnter
|
||||||
|
? '自动回车:开。文字送到应用后立刻回车发出。点击关闭(只填字不发送,便于先编辑再发)'
|
||||||
|
: '自动回车:关。只把文字填进应用输入框,发送由你自己按。点击开启'
|
||||||
|
}
|
||||||
|
onClick={toggleAutoEnter}
|
||||||
|
>
|
||||||
|
↵
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary iv-imebar-send"
|
className="btn btn-primary iv-imebar-send"
|
||||||
disabled={imeSending || !imeText.trim()}
|
disabled={imeSending || !imeText.trim()}
|
||||||
onClick={sendImeText}
|
onClick={sendImeText}
|
||||||
>
|
>
|
||||||
{imeSending ? '发送中' : '发送'}
|
{imeSending ? '发送中' : autoEnter ? '发送' : '填入'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1927,6 +1927,57 @@ button {
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
padding: 0 18px;
|
padding: 0 18px;
|
||||||
}
|
}
|
||||||
|
/* 输入条上的小开关(功能键折叠 / 自动回车):牛奶布艺——默认凹槽,开启态顶起 */
|
||||||
|
.iv-imebar-tool {
|
||||||
|
height: 38px;
|
||||||
|
width: 38px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
background: var(--trough);
|
||||||
|
color: var(--muted);
|
||||||
|
box-shadow: inset 0 1px 3px rgba(var(--shadow) / 0.16);
|
||||||
|
transition: transform 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
}
|
||||||
|
.iv-imebar-tool.on {
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
box-shadow: var(--crease);
|
||||||
|
}
|
||||||
|
.iv-imebar-tool:active {
|
||||||
|
transform: scale(0.94);
|
||||||
|
}
|
||||||
|
/* 功能键排:贴在输入条上方,同一块「布」上顶起的一排小胶囊 */
|
||||||
|
.iv-keybar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px 0;
|
||||||
|
background: var(--base);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.iv-key {
|
||||||
|
min-width: 40px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
box-shadow: var(--crease);
|
||||||
|
transition: transform 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
}
|
||||||
|
.iv-key:active {
|
||||||
|
transform: scale(0.94);
|
||||||
|
box-shadow: var(--crease-press);
|
||||||
|
}
|
||||||
.iv-loading,
|
.iv-loading,
|
||||||
.iv-drop {
|
.iv-drop {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Reference in New Issue
Block a user