下载/主界面优化
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { Search, Minus, Square, X, Settings, ChevronRight } from '@lucide/vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { Search, Minus, X, Settings, ChevronRight } from '@lucide/vue'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { useSearchStore, type SearchItem } from '@/stores/searchStore'
|
||||
@@ -64,6 +64,10 @@ const minimize = async () => {
|
||||
await tauriWindow?.minimize()
|
||||
}
|
||||
|
||||
// 窗口最大化状态:切换最大化/还原图标
|
||||
const isMaximized = ref(false)
|
||||
let unlistenMaximize: (() => void) | null = null
|
||||
|
||||
const maximize = async () => {
|
||||
await tauriWindow?.toggleMaximize()
|
||||
}
|
||||
@@ -119,9 +123,9 @@ if (tauriWindow) {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur()
|
||||
}
|
||||
// 200ms 后恢复 hover,足够让浏览器重置伪类状态
|
||||
restoreHover(200)
|
||||
// 同时注册鼠标移动监听作为后备(定时器可能不够,鼠标移动更可靠)
|
||||
// 不使用定时器恢复 hover:窗口从隐藏恢复显示时,浏览器 :hover 可能被冻结
|
||||
// (mouseleave 在窗口隐藏时不触发),定时器恢复会让冻结的 hover 提前暴露
|
||||
// 仅依赖鼠标移动恢复——移动时浏览器才会重新评估 :hover 状态
|
||||
armMouseMoveRestore()
|
||||
} else {
|
||||
hoverSuppressed.value = true
|
||||
@@ -132,9 +136,23 @@ if (tauriWindow) {
|
||||
// 初始时窗口已显示,恢复 hover
|
||||
restoreHover(300)
|
||||
|
||||
onMounted(async () => {
|
||||
if (tauriWindow) {
|
||||
try {
|
||||
isMaximized.value = await tauriWindow.isMaximized()
|
||||
unlistenMaximize = await tauriWindow.onResized(async () => {
|
||||
isMaximized.value = await tauriWindow!.isMaximized()
|
||||
})
|
||||
} catch {
|
||||
// 非 Tauri 环境忽略
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mousemove', handleFirstMouseMove)
|
||||
if (restoreHoverTimer) clearTimeout(restoreHoverTimer)
|
||||
if (unlistenMaximize) unlistenMaximize()
|
||||
})
|
||||
|
||||
const handleBlur = () => {
|
||||
@@ -241,21 +259,30 @@ const handleBlur = () => {
|
||||
|
||||
<div class="flex items-center pointer-events-auto" :class="{ 'hover-suppressed': hoverSuppressed }">
|
||||
<button
|
||||
class="h-10 w-10 flex items-center justify-center hover:bg-secondary/50 transition-colors rounded-sm"
|
||||
class="h-10 w-10 flex items-center justify-center hover:bg-foreground/5 transition-colors rounded-sm"
|
||||
@click="minimize"
|
||||
@mousedown.stop
|
||||
>
|
||||
<Minus class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
class="h-10 w-10 flex items-center justify-center hover:bg-secondary/50 transition-colors rounded-sm"
|
||||
id="titlebar-maximize"
|
||||
class="h-10 w-10 flex items-center justify-center transition-colors rounded-sm titlebar-maximize-btn"
|
||||
@click="maximize"
|
||||
@mousedown.stop
|
||||
>
|
||||
<Square class="h-3.5 w-3.5" />
|
||||
<!-- 最大化:单圆角矩形 -->
|
||||
<svg v-if="!isMaximized" class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||
</svg>
|
||||
<!-- 还原:单圆角矩形 + 右上角圆角 L形(顶边+右边,圆角连接) -->
|
||||
<svg v-else class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||
<path d="M14 4H20V10" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="h-10 w-10 flex items-center justify-center hover:bg-destructive/20 transition-colors rounded-sm text-destructive"
|
||||
class="h-10 w-10 flex items-center justify-center hover:bg-red-500 hover:text-white text-foreground transition-colors rounded-sm"
|
||||
@click="close"
|
||||
@mousedown.stop
|
||||
>
|
||||
@@ -267,10 +294,17 @@ const handleBlur = () => {
|
||||
|
||||
<style scoped>
|
||||
/* 窗口隐藏/重新显示瞬间,抑制按钮 hover 样式,避免冻结的 :hover 残留 */
|
||||
.hover-suppressed button:hover {
|
||||
.hover-suppressed button:hover,
|
||||
.hover-suppressed button.is-hovered {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* 最大化按钮:snap-layout 插件的原生子窗口遮挡了指针事件,
|
||||
:hover 无法触发,改用插件注入的 .is-hovered 类(鼠标进入原生子窗口时添加) */
|
||||
.titlebar-maximize-btn.is-hovered {
|
||||
background-color: color-mix(in srgb, var(--foreground) 5%, transparent);
|
||||
}
|
||||
|
||||
/* 浮动标签切换器进出动画:淡入 + 从左侧滑入 */
|
||||
.floating-tabs-enter-active,
|
||||
.floating-tabs-leave-active {
|
||||
|
||||
@@ -640,17 +640,17 @@ const toggleSortOrder = () => {
|
||||
</div>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
v-if="task.status === 'active'"
|
||||
v-if="task.status === 'active' || task.status === 'queued'"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="h-8 w-8"
|
||||
title="暂停"
|
||||
:title="task.status === 'queued' ? '取消排队' : '暂停'"
|
||||
@click="handlePause(task.id)"
|
||||
>
|
||||
<Pause class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="task.status === 'paused' || task.status === 'queued' || task.status === 'error'"
|
||||
v-if="task.status === 'paused' || task.status === 'error'"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="h-8 w-8"
|
||||
@@ -1099,7 +1099,7 @@ const toggleSortOrder = () => {
|
||||
<Textarea
|
||||
v-model="addUriText"
|
||||
placeholder="https://example.com/file.zip https://example.com/file2.zip"
|
||||
class="min-h-[120px] font-mono text-sm"
|
||||
class="min-h-[120px] font-mono text-sm [field-sizing:fixed] break-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -558,26 +558,40 @@ const handleCheckUpdate = async () => {
|
||||
}
|
||||
|
||||
const handleUpdateKernel = async () => {
|
||||
const ok = await showConfirm({
|
||||
title: '更新内核',
|
||||
description: '更新内核需要先停止 mihomo,确认继续?',
|
||||
confirmText: '继续更新'
|
||||
})
|
||||
if (!ok) return
|
||||
updatingKernel.value = true
|
||||
try {
|
||||
// 先停止
|
||||
if (running.value) {
|
||||
// 展开更新区块(下载源选择 + 进度),由 handleStartUpdate 执行实际更新
|
||||
updateExpanded.value = true
|
||||
}
|
||||
|
||||
/** 是否展开"更新内核"区块(下载源 + 进度) */
|
||||
const updateExpanded = ref(false)
|
||||
|
||||
/** 开始更新:停止 mihomo → 调用 updateKernel(复用 installProgress 进度机制) */
|
||||
const handleStartUpdate = async () => {
|
||||
if (store.installing) return
|
||||
// 确认停止 mihomo
|
||||
if (running.value) {
|
||||
const ok = await showConfirm({
|
||||
title: '更新内核',
|
||||
description: '更新内核需要先停止 mihomo,确认继续?',
|
||||
confirmText: '继续更新'
|
||||
})
|
||||
if (!ok) return
|
||||
updatingKernel.value = true
|
||||
try {
|
||||
await store.stop()
|
||||
} catch (e) {
|
||||
toast.error('停止 mihomo 失败', { description: String(e) })
|
||||
updatingKernel.value = false
|
||||
return
|
||||
} finally {
|
||||
updatingKernel.value = false
|
||||
}
|
||||
toast.info('正在下载并安装内核...')
|
||||
await store.updateKernel()
|
||||
toast.success('内核更新完成')
|
||||
kernelUpdateInfo.value = null
|
||||
}
|
||||
toast.info('开始下载更新...')
|
||||
try {
|
||||
await store.updateKernel(selectedMirrorPrefix.value)
|
||||
} catch (e) {
|
||||
toast.error('内核更新失败', { description: String(e) })
|
||||
} finally {
|
||||
updatingKernel.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,7 +667,8 @@ const handleInstallKernel = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听安装进度终态,弹 toast 并延时清空进度
|
||||
// 监听安装/更新进度终态,弹 toast 并延时清空进度
|
||||
// 同时处理更新场景下的 updateExpanded 清理(与 installProgress 同步清除,避免更新区块闪烁)
|
||||
watch(
|
||||
() => store.installProgress?.stage,
|
||||
(stage) => {
|
||||
@@ -661,13 +676,22 @@ watch(
|
||||
toast.success('内核安装完成', {
|
||||
description: store.installProgress?.message
|
||||
})
|
||||
// 2 秒后清空进度,让用户看到 100% 终态
|
||||
setTimeout(() => store.clearInstallProgress(), 2000)
|
||||
if (updateExpanded.value) {
|
||||
kernelUpdateInfo.value = null
|
||||
}
|
||||
// 2 秒后同时清空进度和更新区块,让用户看到 100% 终态
|
||||
setTimeout(() => {
|
||||
store.clearInstallProgress()
|
||||
updateExpanded.value = false
|
||||
}, 2000)
|
||||
} else if (stage === 'error') {
|
||||
toast.error('内核安装失败', {
|
||||
description: store.installProgress?.message
|
||||
})
|
||||
setTimeout(() => store.clearInstallProgress(), 5000)
|
||||
setTimeout(() => {
|
||||
store.clearInstallProgress()
|
||||
// 错误时不清除 updateExpanded,让用户可以重新选择下载源重试
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -912,11 +936,10 @@ const saveSettingsForm = async () => {
|
||||
v-if="kernelUpdateInfo.hasUpdate"
|
||||
key="btn-update"
|
||||
size="xs" variant="default"
|
||||
:disabled="updatingKernel"
|
||||
:disabled="store.installing || updateExpanded"
|
||||
@click="handleUpdateKernel"
|
||||
>
|
||||
<Loader2 v-if="updatingKernel" class="size-3 animate-spin" />
|
||||
<Download v-else key="icon-download" class="size-3" />更新
|
||||
<Download key="icon-download" class="size-3" />更新
|
||||
</Button>
|
||||
<Check v-else key="icon-updated" class="size-3 text-emerald-500" />
|
||||
</span>
|
||||
@@ -941,6 +964,61 @@ const saveSettingsForm = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 更新内核区块:检查到更新且用户点击"更新"后展开(复用首次安装的下载源选择 UI) -->
|
||||
<div
|
||||
v-if="updateExpanded && !store.installProgress"
|
||||
class="space-y-2 pt-2 border-t"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs text-muted-foreground">下载源</Label>
|
||||
<Select v-model="mirrorChoice">
|
||||
<SelectTrigger size="sm" class="w-full">
|
||||
<SelectValue placeholder="选择下载源" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="m in MIRROR_PRESETS"
|
||||
:key="m.value"
|
||||
:value="m.value"
|
||||
>
|
||||
{{ m.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ MIRROR_PRESETS.find(m => m.value === mirrorChoice)?.hint }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="mirrorChoice === '__custom'" class="space-y-1.5">
|
||||
<Label class="text-xs text-muted-foreground">镜像站前缀</Label>
|
||||
<Input
|
||||
v-model="customMirror"
|
||||
placeholder="如 https://ghproxy.net/"
|
||||
class="h-8 text-xs"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
前缀会拼接到 GitHub 下载链接前,需以 / 结尾(自动补全)
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="sm" variant="outline" class="flex-1"
|
||||
:disabled="store.installing || updatingKernel"
|
||||
@click="updateExpanded = false"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
size="sm" variant="default" class="flex-1"
|
||||
:disabled="store.installing || updatingKernel || (mirrorChoice === '__custom' && !customMirror.trim())"
|
||||
@click="handleStartUpdate"
|
||||
>
|
||||
<Loader2 v-if="store.installing || updatingKernel" class="size-4 animate-spin" />
|
||||
<DownloadCloud v-else class="size-4" />开始更新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 首次安装区块:仅在内核未安装且不在安装中且已初始化时显示 -->
|
||||
<div
|
||||
v-if="store.initialized && !store.kernel?.exists && !store.installProgress"
|
||||
|
||||
@@ -272,9 +272,39 @@ export const useProxyStore = defineStore('proxy', () => {
|
||||
return await invoke<KernelUpdateInfo>('proxy_check_kernel_update')
|
||||
}
|
||||
|
||||
const updateKernel = async (mirrorPrefix: string = '') => {
|
||||
await invoke('proxy_update_kernel', { mirrorPrefix })
|
||||
await refreshKernel()
|
||||
/**
|
||||
* 更新内核:复用 installKernel 的完整进度事件机制(installing/installProgress/事件监听)
|
||||
* 与 installKernel 的区别仅在于后端会先 stop mihomo(由调用方在前端控制),
|
||||
* 后端 proxy_update_kernel 与 proxy_install_kernel 共用 install_kernel 实现
|
||||
*/
|
||||
const updateKernel = async (mirrorPrefix: string = ''): Promise<void> => {
|
||||
if (installing.value) return
|
||||
installing.value = true
|
||||
installProgress.value = {
|
||||
stage: 'downloading',
|
||||
percent: 0,
|
||||
downloadedBytes: 0,
|
||||
totalBytes: null,
|
||||
message: '准备开始下载...'
|
||||
}
|
||||
if (!progressUnlisten) {
|
||||
progressUnlisten = await listen<InstallProgress>('kernel-install-progress', (e) => {
|
||||
installProgress.value = e.payload
|
||||
})
|
||||
}
|
||||
try {
|
||||
await invoke('proxy_update_kernel', { mirrorPrefix })
|
||||
await refreshKernel()
|
||||
} catch (e) {
|
||||
logger.error('内核更新失败: ' + e)
|
||||
throw e
|
||||
} finally {
|
||||
installing.value = false
|
||||
if (progressUnlisten) {
|
||||
progressUnlisten()
|
||||
progressUnlisten = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ export type ModuleCategory = 'network' | 'tool' | 'system' | 'media'
|
||||
|
||||
/** 模块进程配置 —— 需要管理外部子进程的模块填写 */
|
||||
export interface ModuleProcessConfig {
|
||||
/** 进程标识符(如 'mihomo'、'aria2') */
|
||||
/** 进程标识符(如 'mihomo') */
|
||||
name: string
|
||||
/** 可执行文件路径(运行时由模块自行确定) */
|
||||
executable: string
|
||||
|
||||
Reference in New Issue
Block a user