完善桌面壳托盘关闭行为
主窗口关闭在托盘可用时隐藏到托盘 托盘不可用时保持默认关闭避免窗口无法恢复 补充关闭策略检查测试和架构文档
This commit is contained in:
@@ -143,6 +143,11 @@ const requiredMainSnippets = [
|
|||||||
'TRAY_MENU_QUIT',
|
'TRAY_MENU_QUIT',
|
||||||
'show_main_window',
|
'show_main_window',
|
||||||
'reload_main_window',
|
'reload_main_window',
|
||||||
|
'register_desktop_window_close_events',
|
||||||
|
'resolve_desktop_window_close_action',
|
||||||
|
'WindowEvent::CloseRequested',
|
||||||
|
'api.prevent_close()',
|
||||||
|
'close_window.hide()',
|
||||||
'desktop tray registration failed',
|
'desktop tray registration failed',
|
||||||
'"appearance.getColorScheme"',
|
'"appearance.getColorScheme"',
|
||||||
'"app.lifecycle"',
|
'"app.lifecycle"',
|
||||||
|
|||||||
@@ -898,6 +898,12 @@ enum DesktopTrayAction {
|
|||||||
Ignore,
|
Ignore,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
enum DesktopWindowCloseAction {
|
||||||
|
HideToTray,
|
||||||
|
CloseWindow,
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_desktop_tray_menu_action(menu_id: &str) -> DesktopTrayAction {
|
fn resolve_desktop_tray_menu_action(menu_id: &str) -> DesktopTrayAction {
|
||||||
match menu_id {
|
match menu_id {
|
||||||
TRAY_MENU_SHOW => DesktopTrayAction::ShowMainWindow,
|
TRAY_MENU_SHOW => DesktopTrayAction::ShowMainWindow,
|
||||||
@@ -918,6 +924,14 @@ fn resolve_desktop_tray_icon_action(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_desktop_window_close_action(tray_registered: bool) -> DesktopWindowCloseAction {
|
||||||
|
if tray_registered {
|
||||||
|
DesktopWindowCloseAction::HideToTray
|
||||||
|
} else {
|
||||||
|
DesktopWindowCloseAction::CloseWindow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn show_main_window(app: &tauri::AppHandle) -> tauri::Result<()> {
|
fn show_main_window(app: &tauri::AppHandle) -> tauri::Result<()> {
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
window.show()?;
|
window.show()?;
|
||||||
@@ -984,6 +998,20 @@ fn register_desktop_tray(app: &tauri::App) -> tauri::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn register_desktop_window_close_events(window: &WebviewWindow, tray_registered: bool) {
|
||||||
|
let close_window = window.clone();
|
||||||
|
window.on_window_event(move |event| {
|
||||||
|
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
if resolve_desktop_window_close_action(tray_registered)
|
||||||
|
== DesktopWindowCloseAction::HideToTray
|
||||||
|
{
|
||||||
|
api.prevent_close();
|
||||||
|
let _ = close_window.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn payload_string<'a>(value: &'a Value, field: &str) -> Option<&'a str> {
|
fn payload_string<'a>(value: &'a Value, field: &str) -> Option<&'a str> {
|
||||||
value
|
value
|
||||||
.get(field)
|
.get(field)
|
||||||
@@ -1447,13 +1475,18 @@ fn main() {
|
|||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
if let Err(error) = register_desktop_tray(app) {
|
let tray_registered = match register_desktop_tray(app) {
|
||||||
|
Ok(()) => true,
|
||||||
|
Err(error) => {
|
||||||
eprintln!("desktop tray registration failed: {error}");
|
eprintln!("desktop tray registration failed: {error}");
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
};
|
||||||
let window_config = app.config().app.windows.get(0).cloned();
|
let window_config = app.config().app.windows.get(0).cloned();
|
||||||
if let Some(config) = window_config {
|
if let Some(config) = window_config {
|
||||||
let window =
|
let window =
|
||||||
tauri::WebviewWindowBuilder::from_config(app.handle(), &config)?.build()?;
|
tauri::WebviewWindowBuilder::from_config(app.handle(), &config)?.build()?;
|
||||||
|
register_desktop_window_close_events(&window, tray_registered);
|
||||||
register_desktop_lifecycle_events(&window);
|
register_desktop_lifecycle_events(&window);
|
||||||
let _ = emit_desktop_lifecycle_event(&window, "active", true, "created");
|
let _ = emit_desktop_lifecycle_event(&window, "active", true, "created");
|
||||||
let _ = register_desktop_network_events(&window);
|
let _ = register_desktop_network_events(&window);
|
||||||
@@ -1728,6 +1761,18 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn desktop_close_hides_to_tray_only_when_tray_is_registered() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_desktop_window_close_action(true),
|
||||||
|
DesktopWindowCloseAction::HideToTray
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_desktop_window_close_action(false),
|
||||||
|
DesktopWindowCloseAction::CloseWindow
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn external_url_normalization_allows_only_safe_protocols() {
|
fn external_url_normalization_allows_only_safe_protocols() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
- 2026-06-18 草稿生成完成 / 失败通知:平台壳层的 `markDraftReady` / `markDraftFailed` 统一收口会在原生壳声明 `notification.showLocal` 时请求即时本地通知;通知 payload 只包含生成完成 / 失败标题和草稿来源正文,按草稿来源去重,同一草稿重新进入生成中后才允许再次通知。该能力不替代现有完成 / 错误弹窗、作品架红点、队列概览或后端状态回读,通知失败不阻断主流程。
|
- 2026-06-18 草稿生成完成 / 失败通知:平台壳层的 `markDraftReady` / `markDraftFailed` 统一收口会在原生壳声明 `notification.showLocal` 时请求即时本地通知;通知 payload 只包含生成完成 / 失败标题和草稿来源正文,按草稿来源去重,同一草稿重新进入生成中后才允许再次通知。该能力不替代现有完成 / 错误弹窗、作品架红点、队列概览或后端状态回读,通知失败不阻断主流程。
|
||||||
- 2026-06-18 剪贴板读取能力:新增 `clipboard.readText` HostBridge capability,H5 只能读取纯文本结果,契约限制返回文本最多 100000 字符;Expo 壳通过 `expo-clipboard` 读取系统剪贴板文本,Tauri 壳通过 Rust 侧 `tauri-plugin-clipboard-manager` 读取文本且不开放插件 JS guest API。该能力不读取图片、HTML、文件列表或剪贴板监听事件,宿主未声明或读取失败时由 H5 视作失败并保留原流程。
|
- 2026-06-18 剪贴板读取能力:新增 `clipboard.readText` HostBridge capability,H5 只能读取纯文本结果,契约限制返回文本最多 100000 字符;Expo 壳通过 `expo-clipboard` 读取系统剪贴板文本,Tauri 壳通过 Rust 侧 `tauri-plugin-clipboard-manager` 读取文本且不开放插件 JS guest API。该能力不读取图片、HTML、文件列表或剪贴板监听事件,宿主未声明或读取失败时由 H5 视作失败并保留原流程。
|
||||||
- 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capability,H5 统一通过 `importHostTextFile()` 读取宿主返回的纯文本内容;Expo 壳通过 `expo-document-picker` 打开系统文档选择器,Tauri 壳通过系统文件选择框读取真实文本文件。两端只接受 `text/plain`、`text/markdown`、`text/csv`、`application/json` 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备 URI / 本机绝对路径,也不开放通用文件系统。
|
- 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capability,H5 统一通过 `importHostTextFile()` 读取宿主返回的纯文本内容;Expo 壳通过 `expo-document-picker` 打开系统文档选择器,Tauri 壳通过系统文件选择框读取真实文本文件。两端只接受 `text/plain`、`text/markdown`、`text/csv`、`application/json` 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备 URI / 本机绝对路径,也不开放通用文件系统。
|
||||||
- 2026-06-18 Tauri 系统托盘:桌面壳启用真实 OS 托盘并复用品牌图标,托盘菜单只执行显示主窗口、刷新主窗口和退出应用,左键点击托盘图标恢复并聚焦主窗口;该能力归桌面壳自身,不进入 HostBridge capability,不向 H5 暴露托盘、菜单、shell 或任意窗口控制 API。托盘注册失败不得阻断主窗口启动,`check:native-shells` 和 Tauri cargo test 覆盖托盘配置与菜单动作映射。
|
- 2026-06-18 Tauri 系统托盘:桌面壳启用真实 OS 托盘并复用品牌图标,托盘菜单只执行显示主窗口、刷新主窗口和退出应用,左键点击托盘图标恢复并聚焦主窗口;该能力归桌面壳自身,不进入 HostBridge capability,不向 H5 暴露托盘、菜单、shell 或任意窗口控制 API。托盘注册成功时主窗口关闭按钮只隐藏到托盘,必须通过托盘“退出”结束应用;托盘注册失败不得阻断主窗口启动,也不得拦截关闭,避免窗口消失后无法恢复。`check:native-shells` 和 Tauri cargo test 覆盖托盘配置、菜单动作映射和关闭策略。
|
||||||
- 影响范围:`src/services/host-bridge/`、未来 `apps/mobile-shell/`、未来 `apps/desktop-shell/`、移动端支付 / 分享 / 深链 / 推送、桌面端系统能力、AI H5 sandbox 的 GameBridge 边界。
|
- 影响范围:`src/services/host-bridge/`、未来 `apps/mobile-shell/`、未来 `apps/desktop-shell/`、移动端支付 / 分享 / 深链 / 推送、桌面端系统能力、AI H5 sandbox 的 GameBridge 边界。
|
||||||
- 验证方式:普通浏览器、小程序、Expo 壳、Tauri 壳都能返回正确 `getHostRuntime()`;未支持能力能回退 H5;固定玩法在各宿主中读取同一作品数据和运行态 snapshot;AI sandbox 无法直接调用 HostBridge;Tauri release 不允许任意远端页面调用桌面命令。
|
- 验证方式:普通浏览器、小程序、Expo 壳、Tauri 壳都能返回正确 `getHostRuntime()`;未支持能力能回退 H5;固定玩法在各宿主中读取同一作品数据和运行态 snapshot;AI sandbox 无法直接调用 HostBridge;Tauri release 不允许任意远端页面调用桌面命令。
|
||||||
- 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md`、`docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`。
|
- 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md`、`docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`。
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ GameBridge 禁止:
|
|||||||
|
|
||||||
2026-06-18 追加:H5 账号状态刷新开始消费 `app.reloadWebView`。用户登录成功、退出登录或其它身份边界变化需要整页重新初始化时,`AuthGate` 会优先请求 Tauri 主 WebViewWindow 刷新;宿主未声明或刷新失败时再回退浏览器刷新,Tauri 仍只暴露 `host_bridge_request` 这一受控命令入口。
|
2026-06-18 追加:H5 账号状态刷新开始消费 `app.reloadWebView`。用户登录成功、退出登录或其它身份边界变化需要整页重新初始化时,`AuthGate` 会优先请求 Tauri 主 WebViewWindow 刷新;宿主未声明或刷新失败时再回退浏览器刷新,Tauri 仍只暴露 `host_bridge_request` 这一受控命令入口。
|
||||||
|
|
||||||
2026-06-18 追加:桌面壳启用 Tauri 真实系统托盘,并复用品牌图标。托盘菜单只提供宿主壳级动作:显示主窗口、刷新主窗口和退出应用;左键点击托盘图标恢复并聚焦主窗口。该能力不进入 HostBridge capability 清单,不向 H5 暴露托盘 API、菜单 API、shell API 或任意窗口控制;如果当前桌面环境无法注册托盘,壳会继续启动主窗口。
|
2026-06-18 追加:桌面壳启用 Tauri 真实系统托盘,并复用品牌图标。托盘菜单只提供宿主壳级动作:显示主窗口、刷新主窗口和退出应用;左键点击托盘图标恢复并聚焦主窗口。该能力不进入 HostBridge capability 清单,不向 H5 暴露托盘 API、菜单 API、shell API 或任意窗口控制;如果当前桌面环境无法注册托盘,壳会继续启动主窗口。托盘注册成功时,用户点击主窗口关闭按钮会隐藏到托盘,必须通过托盘“退出”动作结束应用;托盘注册失败时不拦截关闭,避免窗口消失后没有恢复入口。
|
||||||
|
|
||||||
### Phase 4:宿主能力扩展
|
### Phase 4:宿主能力扩展
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user