下载模块 Init
This commit is contained in:
@@ -490,3 +490,135 @@ const installKernel = async () => {
|
||||
TitleBar 的关闭按钮实际是 `hide()` 到托盘。webview 快速隐藏时浏览器 `mouseleave` 可能不触发,导致从托盘恢复后按钮仍显示 hover 高亮。
|
||||
|
||||
**修复**:`hide()` 前调用 `document.activeElement.blur()`,并监听 `onFocusChanged` 在窗口重新获得焦点时再 blur 一次。新模块若有类似的"隐藏窗口"操作(如全局快捷键隐藏),同样需要 blur。
|
||||
|
||||
### 前端:浮动标签切换器(TabsList 滚动遮挡时在 TitleBar 显示)
|
||||
|
||||
模块详情页内容滚动时,顶部 `TabsList` 会被 `TitleBar` 遮挡,导致用户必须滚回顶部才能切换 Tab。已抽取通用 composable `src/lib/useModuleTabs.ts` 自动处理。
|
||||
|
||||
**接入方式**(任何使用 Tabs 的模块都可用,代理/下载器模块已接入):
|
||||
|
||||
```ts
|
||||
// 模块 <script setup> 顶部
|
||||
import { useModuleTabs } from '@/lib/useModuleTabs'
|
||||
|
||||
const activeTab = ref('overview')
|
||||
const tabsListRef = useModuleTabs(activeTab, [
|
||||
{ value: 'overview', label: '概览' },
|
||||
{ value: 'settings', label: '设置' }
|
||||
])
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- 模板中给 TabsList 包一层带 ref 的 div -->
|
||||
<Tabs v-model="activeTab">
|
||||
<div ref="tabsListRef">
|
||||
<TabsList>...</TabsList>
|
||||
</div>
|
||||
<TabsContent .../>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
**工作原理**:
|
||||
- composable 内部在 `onMounted` 时注册标签到 `moduleTabsStore`,`onUnmounted` 时注销
|
||||
- 用 `IntersectionObserver`(`rootMargin: '-44px 0px 0px 0px'` 裁剪 TitleBar 高度)监听 TabsList 可见性
|
||||
- 滚动遮挡时 `TitleBar` 中"Thing"标题右侧自动显示浮动切换按钮(带淡入+左滑动画)
|
||||
- 双向 watch 同步本地 `activeTab` 与 store,用户点击 TitleBar 浮动按钮也能切换模块内 Tab
|
||||
|
||||
**约束**:TitleBar 高度固定 40px(h-10),composable 已用 44px 裁剪(含 4px 缓冲);store 是单例,一个模块同一时间只能注册一组标签。
|
||||
|
||||
### 前端:内核路径用 %APPDATA% 简化显示
|
||||
|
||||
内核路径较长(如 `C:\Users\xxx\AppData\Roaming\thing.lfeng.me\proxy\cores\mihomo.exe`),展示时用 `%APPDATA%` 替换前缀,并提供复制完整路径 / 在文件夹中显示两个按钮。代理和下载器模块的内核管理区块已采用此模式。
|
||||
|
||||
```ts
|
||||
import { appDataDir } from '@tauri-apps/api/path'
|
||||
|
||||
const appDataPath = ref('')
|
||||
onMounted(async () => {
|
||||
try { appDataPath.value = await appDataDir() } catch {}
|
||||
})
|
||||
|
||||
const pathDisplay = computed(() => {
|
||||
const p = store.kernel?.path
|
||||
if (!p) return ''
|
||||
if (appDataPath.value && p.toLowerCase().startsWith(appDataPath.value.toLowerCase())) {
|
||||
return '%APPDATA%' + p.slice(appDataPath.value.length)
|
||||
}
|
||||
return p
|
||||
})
|
||||
```
|
||||
|
||||
`<title>` 放完整路径,显示文本用 `pathDisplay`,复制时复制完整路径。
|
||||
|
||||
### 前端:任务历史 localStorage 持久化(下载器模块模式)
|
||||
|
||||
子进程未启动时仍想展示历史数据(如下载任务列表),可在 store 中用 localStorage 保存最近一次的任务快照:
|
||||
|
||||
- 每次全量刷新后调用 `persistHistory()` 保存(合并去重,按状态优先级排序,限制条数如 200)
|
||||
- `onMounted` 时无论子进程是否运行都先调用 `loadHistory()` 填充到 `stoppedTasks`
|
||||
- 子进程启动后实时数据会覆盖历史快照
|
||||
|
||||
此模式适用于任何"子进程不运行时也要展示历史"的场景。
|
||||
|
||||
### 前端:轻量设置 Dialog(替代独立 Tab)
|
||||
|
||||
模块设置项较多时,传统做法是单独开一个"设置"Tab。但任务页工具栏需要快速调整少量核心设置(如下载目录、并发数),切到设置 Tab 再切回来体验割裂。
|
||||
|
||||
**模式**:在任务工具栏放一个"下载设置"按钮,点击弹出 Dialog,包含核心设置项(与设置页共用 `store.settings`),保存时调用 `handleSaveSettings`(重启子进程应用配置)并自动关闭弹窗。完整设置仍保留在"设置"Tab。
|
||||
|
||||
适用场景:需要在任务页快速调整的少量高频设置;若设置项不多,可完全用 Dialog 替代设置 Tab。
|
||||
|
||||
### 后端:opener 插件 scope 限制与绕过
|
||||
|
||||
`tauri-plugin-opener` 的 `opener:allow-open-path` 权限默认 **无 scope**,IPC `open_path` 命令会拒绝任何路径并报错 `Not allowed to open path`。
|
||||
|
||||
**解决方案**:在 Rust 端写一个自定义命令,用 `OpenerExt::opener().open_path()` 直接调用(绕过 IPC scope 检查):
|
||||
|
||||
```rust
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn my_open_dir(app: AppHandle, path: String) -> Result<(), String> {
|
||||
let p = std::path::Path::new(&path);
|
||||
if !p.exists() {
|
||||
return Err(format!("路径不存在: {}", path));
|
||||
}
|
||||
app.opener().open_path(path, None::<&str>).map_err(|e| e.to_string())
|
||||
}
|
||||
```
|
||||
|
||||
在 `lib.rs` 的 `invoke_handler` 中注册,前端通过 `invoke('my_open_dir', { path })` 调用。下载器模块的"打开下载目录"已采用此方案。
|
||||
|
||||
### 前端:剩余时间(ETA)可读化
|
||||
|
||||
下载卡片直接显示 `(total-completed)/speed` 会得到 `17.217666215634114 B` 这种难懂的字节数。应格式化为时间:
|
||||
|
||||
```ts
|
||||
const formatEta = (seconds: number): string => {
|
||||
if (!isFinite(seconds) || seconds <= 0) return ''
|
||||
if (seconds < 60) return `剩余 ${Math.ceil(seconds)} 秒`
|
||||
if (seconds < 3600) {
|
||||
const m = Math.floor(seconds / 60)
|
||||
const s = Math.ceil(seconds % 60)
|
||||
return `剩余 ${m} 分 ${s} 秒`
|
||||
}
|
||||
const h = Math.floor(seconds / 3600)
|
||||
const m = Math.ceil((seconds % 3600) / 60)
|
||||
return `剩余 ${h} 小时 ${m} 分`
|
||||
}
|
||||
```
|
||||
|
||||
`getEta(task) = (totalLength - completedLength) / downloadSpeed`,speed≤0 时返回 0(不显示)。
|
||||
|
||||
### 前端:状态统计用 Badge 替代纯文本
|
||||
|
||||
下载任务工具栏的速度/活跃/等待/已完成数量,以及代理概览页的内核状态/版本,从纯文本改为 `Badge variant="secondary"` 或带颜色的 `Badge`(绿色已安装/红色未安装),视觉更醒目。新模块的状态展示建议统一用 Badge。
|
||||
|
||||
### 后端:应用退出清理必须覆盖所有退出路径
|
||||
|
||||
`lib.rs` 中应用退出有两种触发路径,**都要**调用 `cleanup_on_exit` + `stop_all`:
|
||||
|
||||
1. `quit_app` Tauri 命令(前端 `appWindow.destroy()` 触发)
|
||||
2. 系统托盘的退出菜单项
|
||||
|
||||
漏掉任何一个都会导致子进程残留(如 aria2 继续占用端口、mihomo 系统代理未清除)。新增管理子进程的模块时,检查这两处是否都调用了清理逻辑。
|
||||
|
||||
Reference in New Issue
Block a user