修复: 版本更新与设置面板走查问题

- 更新链路: 检查记录远端SHA256,下载完成事件回传,安装时带哈希校验
- 续传: 服务器忽略Range返回200时截断重下防追加损坏,文件名去query
- 定时器: 自动检查可重入,配置变更免重启生效,关闭即停,下载加互斥
- 前端: 支持跳过此版本并按版本去重,手动检查不受门禁限制,启动未就绪延时重试
- 更新面板: 新增自动检查开关/间隔/地址配置与进度条状态映射
- 版本历史: cache参数强制刷新与加载超时重试兜底
- 配置: 侧边栏分区全链路持久化,保存请求复用api类型防字段丢失
- 侧边栏: 区块定位改显式类名,不再依赖排序位置
- 绑定: 重新生成并与镜像目录同步
This commit is contained in:
lxy
2026-10-03 10:18:27 +08:00
parent 81e245eade
commit c7978a5bfd
25 changed files with 589 additions and 286 deletions
+5 -1
View File
@@ -15,6 +15,9 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr
import * as application$0 from "../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as api$0 from "./internal/api/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as filesystem$0 from "./internal/filesystem/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -549,8 +552,9 @@ export function RestoreFromRecycleBin(recyclePath: string): $CancellablePromise<
/**
* SaveAppConfig 保存应用配置
* 直接复用 api.SaveAppConfigRequest,避免 main 包副本转发时丢字段
*/
export function SaveAppConfig(req: $models.SaveAppConfigRequest): $CancellablePromise<{ [_ in string]?: any }> {
export function SaveAppConfig(req: api$0.SaveAppConfigRequest): $CancellablePromise<{ [_ in string]?: any }> {
return $Call.ByID(1942219977, req).then(($result: any) => {
return $$createType2($result);
});
-1
View File
@@ -11,7 +11,6 @@ export {
OssConnectRequest,
OssRenamePathRequest,
RenamePathRequest,
SaveAppConfigRequest,
SaveBase64FileRequest,
SaveProfileRequest,
SftpConnectRequest,
@@ -2,5 +2,6 @@
// This file is automatically generated. DO NOT EDIT
export {
AppTabDefinition
AppTabDefinition,
SaveAppConfigRequest
} from "./models.js";
@@ -40,3 +40,56 @@ export class AppTabDefinition {
return new AppTabDefinition($$parsedSource as Partial<AppTabDefinition>);
}
}
/**
* SaveAppConfigRequest 保存应用配置请求(前端格式)
*/
export class SaveAppConfigRequest {
"tabs": AppTabDefinition[];
"visibleTabs": string[];
"defaultTab": string;
"sidebarSections": string[];
/** Creates a new SaveAppConfigRequest instance. */
constructor($$source: Partial<SaveAppConfigRequest> = {}) {
if (!("tabs" in $$source)) {
this["tabs"] = [];
}
if (!("visibleTabs" in $$source)) {
this["visibleTabs"] = [];
}
if (!("defaultTab" in $$source)) {
this["defaultTab"] = "";
}
if (!("sidebarSections" in $$source)) {
this["sidebarSections"] = [];
}
Object.assign(this, $$source);
}
/**
* Creates a new SaveAppConfigRequest instance from a string or object.
*/
static createFrom($$source: any = {}): SaveAppConfigRequest {
const $$createField0_0 = $$createType1;
const $$createField1_0 = $$createType2;
const $$createField3_0 = $$createType2;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("tabs" in $$parsedSource) {
$$parsedSource["tabs"] = $$createField0_0($$parsedSource["tabs"]);
}
if ("visibleTabs" in $$parsedSource) {
$$parsedSource["visibleTabs"] = $$createField1_0($$parsedSource["visibleTabs"]);
}
if ("sidebarSections" in $$parsedSource) {
$$parsedSource["sidebarSections"] = $$createField3_0($$parsedSource["sidebarSections"]);
}
return new SaveAppConfigRequest($$parsedSource as Partial<SaveAppConfigRequest>);
}
}
// Private type creation functions
const $$createType0 = AppTabDefinition.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $Create.Array($Create.Any);
-49
View File
@@ -5,10 +5,6 @@
// @ts-ignore: Unused imports
import { Create as $Create } from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as api$0 from "./internal/api/models.js";
/**
* BgmPlaylistItem 播放列表条目
*/
@@ -134,46 +130,6 @@ export class RenamePathRequest {
}
}
/**
* SaveAppConfigRequest 保存应用配置请求
*/
export class SaveAppConfigRequest {
"tabs": api$0.AppTabDefinition[];
"visibleTabs": string[];
"defaultTab": string;
/** Creates a new SaveAppConfigRequest instance. */
constructor($$source: Partial<SaveAppConfigRequest> = {}) {
if (!("tabs" in $$source)) {
this["tabs"] = [];
}
if (!("visibleTabs" in $$source)) {
this["visibleTabs"] = [];
}
if (!("defaultTab" in $$source)) {
this["defaultTab"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new SaveAppConfigRequest instance from a string or object.
*/
static createFrom($$source: any = {}): SaveAppConfigRequest {
const $$createField0_0 = $$createType1;
const $$createField1_0 = $$createType2;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("tabs" in $$parsedSource) {
$$parsedSource["tabs"] = $$createField0_0($$parsedSource["tabs"]);
}
if ("visibleTabs" in $$parsedSource) {
$$parsedSource["visibleTabs"] = $$createField1_0($$parsedSource["visibleTabs"]);
}
return new SaveAppConfigRequest($$parsedSource as Partial<SaveAppConfigRequest>);
}
}
/**
* SaveBase64FileRequest 保存 Base64 编码的二进制文件
*/
@@ -422,8 +378,3 @@ export class WriteFileRequest {
return new WriteFileRequest($$parsedSource as Partial<WriteFileRequest>);
}
}
// Private type creation functions
const $$createType0 = api$0.AppTabDefinition.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $Create.Array($Create.Any);
+6 -4
View File
@@ -85,7 +85,6 @@
<UpdateNotification
v-model="updateStore.showUpdate"
:update-info="updateStore.updateInfo"
@install="updateStore.installUpdate"
/>
</a-layout>
</template>
@@ -179,9 +178,12 @@ onMounted(() => {
// 禁用浏览器默认右键菜单(桌面应用体验)
document.addEventListener('contextmenu', e => e.preventDefault())
// 延迟检查更新(启动后 3 秒,静默模式)
setTimeout(() => {
updateStore.checkForUpdates(true)
// 延迟检查更新(启动后 3 秒,静默模式;后端更新模块异步初始化,未就绪则 15 秒后重试一次)
setTimeout(async () => {
const ok = await updateStore.checkForUpdates(true)
if (!ok) {
setTimeout(() => { updateStore.checkForUpdates(true) }, 15000)
}
}, 3000)
})
@@ -3,7 +3,7 @@
<div v-show="config.visible" class="sidebar" :style="{ width: width + 'px' }">
<template v-for="section in sectionOrder" :key="section">
<!-- 服务器区块 -->
<div v-if="section === 'server' && showServer" class="sidebar-section">
<div v-if="section === 'server' && showServer" class="sidebar-section server-section">
<div class="section-header" @click="serverCollapsed = !serverCollapsed">
<span class="section-title">🖥️ 服务器</span>
<icon-down v-if="!serverCollapsed" class="section-toggle" />
@@ -160,7 +160,7 @@
</div>
<!-- 帮助文档区块 -->
<div v-if="section === 'help' && showHelp" class="sidebar-section">
<div v-if="section === 'help' && showHelp" class="sidebar-section help-section">
<div class="section-header" @click="helpCollapsed = !helpCollapsed">
<span class="section-title">📖 帮助</span>
<icon-down v-if="!helpCollapsed" class="section-toggle" />
@@ -694,13 +694,13 @@ function onSrvCtxVisible(index: number, visible: boolean) {
position: relative;
}
/* 服务器区块不收缩 */
.sidebar-section:first-child {
/* 服务器区块不收缩(按显式类名定位,不依赖排序位置) */
.server-section {
flex-shrink: 0;
}
/* 服务器区块不需要 max-height 限制 */
.sidebar-section:first-child > .section-content-wrap {
.server-section > .section-content-wrap {
max-height: none;
}
@@ -736,8 +736,8 @@ function onSrvCtxVisible(index: number, visible: boolean) {
flex-direction: column;
}
/* 帮助区块固定在底部 */
.sidebar-section:last-child {
/* 帮助区块不收缩(按显式类名定位,不依赖排序位置) */
.help-section {
flex-shrink: 0;
}
+54 -49
View File
@@ -3,14 +3,12 @@
</template>
<script setup>
import { computed, watch, onMounted, onUnmounted, h, nextTick } from 'vue'
import { Modal, Message, Progress } from '@arco-design/web-vue'
import { computed, ref, watch, onMounted, onUnmounted, h, nextTick } from 'vue'
import { Modal, Message, Progress, Button } from '@arco-design/web-vue'
import { useUpdateStore } from '../stores/update'
import { parseMarkdown, isMarkedReady, preloadMarked } from '../utils/markedExtensions'
import { sanitizeHtml } from '@/utils/fileUtils'
import { parseWailsEventData } from '@/utils/wailsEvent'
import { DownloadUpdate } from '../wailsjs/v3-bindings/u-desk/app'
import { On, Off } from '@wailsio/events'
import { GetCurrentVersion } from '../wailsjs/v3-bindings/u-desk/app'
const props = defineProps({
modelValue: {
@@ -33,7 +31,9 @@ let confirmModalInstance = null
let progressModalInstance = null
// Computed
const currentVersion = computed(() => props.updateInfo?.current_version || '0.1.0')
// 更新信息未带 current_version 时从后端兜底获取
const backendVersion = ref('')
const currentVersion = computed(() => props.updateInfo?.current_version || backendVersion.value)
const latestVersion = computed(() => props.updateInfo?.latest_version || '')
const changelog = computed(() => props.updateInfo?.changelog || '')
const forceUpdate = computed(() => props.updateInfo?.force_update || false)
@@ -133,6 +133,15 @@ const showUpdateModal = () => {
)
}
// 跳过此版本(强制更新不可跳过)
if (!forceUpdate.value) {
elements.push(
h('div', { style: { marginTop: '8px', textAlign: 'right' } }, [
h(Button, { size: 'small', type: 'text', onClick: handleSkipVersion }, () => '跳过此版本')
])
)
}
return elements
},
okText: '立即更新',
@@ -187,22 +196,12 @@ const getProgressModalContent = () => {
]
}
// 安装中状态
if (updateStore.installing) {
return [
h('div', { style: { marginBottom: '16px' } }, [
h('div', { style: { marginBottom: '8px', fontSize: '14px', color: 'var(--color-text-2)' } }, '正在安装更新...')
]),
h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)' } }, '请稍候,应用将在安装完成后自动重启...')
]
}
// 完成状态
// 安装中状态:下载完成后进入安装阶段,弹窗保持打开直到进程重启
return [
h('div', { style: { marginBottom: '16px', textAlign: 'center' } }, [
h('div', { style: { fontSize: '16px', color: 'rgb(var(--success-6))', marginBottom: '8px' } }, '✓ 更新完成')
h('div', { style: { marginBottom: '16px' } }, [
h('div', { style: { marginBottom: '8px', fontSize: '14px', color: 'var(--color-text-2)' } }, '正在安装更新...')
]),
h('div', { style: { fontSize: '14px', color: 'var(--color-text-2)' } }, '应用将在几秒后自动重启...')
h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)' } }, '请稍候,应用将在安装完成后自动重启...')
]
}
@@ -274,16 +273,23 @@ const closeModals = () => {
closeProgressModal()
}
// 下载更新
// 跳过此版本
const handleSkipVersion = async () => {
try {
await updateStore.skipVersion(props.updateInfo.latest_version)
} catch (error) {
console.error('跳过版本失败:', error)
}
closeModals()
emit('update:modelValue', false)
}
// 下载更新(进度与完成事件由 store 统一处理)
const handleDownload = async () => {
await showProgressModal()
try {
const result = await DownloadUpdate(props.updateInfo.download_url)
if (result.success) return
closeProgressModal()
Message.error(result.message || '下载启动失败')
await updateStore.downloadUpdate()
} catch (error) {
console.error('下载失败:', error)
closeProgressModal()
@@ -291,35 +297,34 @@ const handleDownload = async () => {
}
}
// 下载完成处理(本地覆盖:关闭弹窗)
const onDownloadComplete = async (event) => {
const data = parseWailsEventData(event)
if (data.error) {
closeProgressModal()
Message.error('下载失败:' + data.error)
return
// 下载/安装状态收尾:下载失败收起进度弹窗;安装流程结束(重启前或失败)关闭弹窗
watch(
() => [updateStore.installing, updateStore.downloadStatus],
([installing, status], [prevInstalling]) => {
if (status === 'exception') {
closeProgressModal()
return
}
if (prevInstalling && !installing) {
closeProgressModal()
emit('update:modelValue', false)
}
}
if (!data.success || !data.file_path) {
closeProgressModal()
Message.error('下载完成但数据不完整')
return
}
// 等待安装完成
await new Promise(resolve => setTimeout(resolve, 3000))
closeProgressModal()
emit('update:modelValue', false)
}
)
// 生命周期
onMounted(() => {
On('download-complete', onDownloadComplete)
onMounted(async () => {
try {
const result = await GetCurrentVersion()
if (result.success) {
backendVersion.value = result.data?.version || ''
}
} catch {
// 后端未就绪时忽略,留空兜底
}
})
onUnmounted(() => {
Off('download-complete')
closeModals()
})
</script>
+98 -8
View File
@@ -45,6 +45,30 @@
</a-button>
</a-space>
<!-- 更新设置 -->
<div class="config-section">
<div class="config-row">
<span class="config-label">自动检查更新</span>
<a-switch v-model="autoCheckEnabled" />
</div>
<div class="config-row">
<span class="config-label">检查间隔(分钟)</span>
<a-input-number v-model="checkIntervalMinutes" :min="5" :max="1440" style="width: 120px" />
</div>
<div class="config-row">
<span class="config-label">检查地址</span>
<a-input
v-model="checkURL"
placeholder="留空使用默认 https://c.1216.top/last-version.json"
style="width: 320px"
allow-clear
/>
</div>
<a-button type="primary" size="small" :loading="savingConfig" @click="handleSaveConfig">
保存设置
</a-button>
</div>
<!-- 更新信息 -->
<a-alert
v-if="updateInfo"
@@ -67,7 +91,7 @@
<template #icon>
<icon-download />
</template>
{{ downloadProgress > 0 ? '下载中...' : '下载更新' }}
{{ downloadButtonText }}
</a-button>
<a-button
v-if="downloadedFile"
@@ -89,7 +113,7 @@
<div v-if="downloadProgress > 0 || downloading" class="download-progress">
<a-progress
:percent="downloadProgress"
:status="downloadStatus"
:status="progressBarStatus"
/>
<div class="progress-info">
<span>{{ updateStore.formatFileSize(progressInfo.downloaded) }} / {{ updateStore.formatFileSize(progressInfo.total) }}</span>
@@ -113,7 +137,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { storeToRefs } from 'pinia'
import { IconHistory } from '@arco-design/web-vue/es/icon'
@@ -121,8 +145,8 @@ import { useUpdateStore } from '../stores/update'
import { parseMarkdown, isMarkedReady, preloadMarked } from '../utils/markedExtensions'
import { sanitizeHtml } from '@/utils/fileUtils'
import { parseWailsEventData } from '@/utils/wailsEvent'
import { GetCurrentVersion, GetUpdateConfig, InstallUpdate } from '../wailsjs/v3-bindings/u-desk/app'
import { On, Off } from '@wailsio/events'
import { GetCurrentVersion, GetUpdateConfig, SetUpdateConfig, InstallUpdate } from '../wailsjs/v3-bindings/u-desk/app'
import { Events } from '@wailsio/runtime'
// Emits
defineEmits(['open-version-history'])
@@ -139,6 +163,26 @@ const lastCheckTime = ref('-')
const installResult = ref(null)
const downloadedFile = ref(null)
// 更新设置
const autoCheckEnabled = ref(false)
const checkIntervalMinutes = ref(60)
const checkURL = ref('')
const savingConfig = ref(false)
// 下载按钮文案:下载中/安装中/可下载三态
const downloadButtonText = computed(() => {
if (downloading.value) return `下载中...${Math.round(downloadProgress.value)}%`
if (installing.value) return '安装中...'
return '下载更新'
})
// a-progress 状态映射:store 的 active/exception/success → arco 的 normal/danger/success
const progressBarStatus = computed(() => {
if (downloadStatus.value === 'exception') return 'danger'
if (downloadStatus.value === 'success') return 'success'
return 'normal'
})
/** 渲染 changelog(Markdown → HTML) */
function renderChangelog(text: string): string {
if (!text) return ''
@@ -163,13 +207,39 @@ const loadConfig = async () => {
const result = await GetUpdateConfig()
if (!result.success) return
const { last_check_time = '-' } = result.data || {}
const {
last_check_time = '-',
auto_check_enabled = false,
check_interval_minutes = 60,
check_url = ''
} = result.data || {}
lastCheckTime.value = last_check_time
autoCheckEnabled.value = auto_check_enabled
checkIntervalMinutes.value = check_interval_minutes
checkURL.value = check_url
} catch (error) {
console.error('加载配置失败:', error)
}
}
// 保存设置
const handleSaveConfig = async () => {
savingConfig.value = true
try {
const result = await SetUpdateConfig(autoCheckEnabled.value, checkIntervalMinutes.value, checkURL.value)
if (result.success) {
Message.success('设置已保存')
} else {
Message.error(result.message || '保存设置失败')
}
} catch (error) {
console.error('保存设置失败:', error)
Message.error('保存设置失败:' + (error as Error).message)
} finally {
savingConfig.value = false
}
}
// 检查更新
const handleCheckUpdate = async () => {
installResult.value = null
@@ -241,11 +311,11 @@ onMounted(async () => {
await loadConfig()
// 监听下载完成事件(仅用于记录文件路径)
On('download-complete', onDownloadComplete)
Events.On('download-complete', onDownloadComplete)
})
onUnmounted(() => {
Off('download-complete')
Events.Off('download-complete')
})
</script>
@@ -353,4 +423,24 @@ onUnmounted(() => {
font-size: 12px;
color: var(--color-text-3);
}
.config-section {
margin-top: 16px;
padding: 12px 16px;
background: var(--color-fill-1);
border-radius: 4px;
}
.config-row {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.config-label {
width: 110px;
flex-shrink: 0;
font-size: 13px;
color: var(--color-text-2);
}
</style>
+44 -14
View File
@@ -17,14 +17,20 @@
<!-- iframe 嵌入远程页面 -->
<div class="iframe-wrapper">
<iframe
ref="iframeRef"
:src="iframeSrc"
class="version-iframe"
frameborder="0"
@load="onIframeLoad"
/>
<div v-if="loading" class="iframe-loading">
<a-spin size="32" />
<div v-if="loadFailed" class="iframe-loading">
<p>加载失败,请检查网络</p>
<a-button type="primary" size="small" @click="handleRefresh">
<template #icon><icon-refresh /></template>
重试
</a-button>
</div>
<div v-else-if="loading" class="iframe-loading">
<a-spin :size="32" />
<p>加载中...</p>
</div>
</div>
@@ -32,33 +38,50 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { IconRefresh, IconExport } from '@arco-design/web-vue/es/icon'
import { GetCurrentVersion } from '../wailsjs/v3-bindings/u-desk/app'
// ==================== 常量 ====================
const BASE_URL = 'https://c.1216.top/u-desk/version.html'
const LOAD_TIMEOUT = 10000
// ==================== 状态 ====================
const currentVersion = ref('')
const loading = ref(true)
const refreshing = ref(false)
const iframeRef = ref<HTMLIFrameElement | null>(null)
const loadFailed = ref(false)
const cacheBuster = ref(Date.now())
let loadTimer: number | undefined
// ==================== 计算属性 ====================
const iframeSrc = computed(() => {
if (currentVersion.value) {
return `${BASE_URL}?v=${currentVersion.value}&vis=u-desk`
}
return `${BASE_URL}?vis=u-desk`
const version = currentVersion.value ? `v=${currentVersion.value}&` : ''
return `${BASE_URL}?${version}vis=u-desk&_t=${cacheBuster.value}`
})
// ==================== 操作 ====================
function clearLoadTimer(): void {
if (loadTimer !== undefined) {
window.clearTimeout(loadTimer)
loadTimer = undefined
}
}
function startLoadTimer(): void {
clearLoadTimer()
loadFailed.value = false
loadTimer = window.setTimeout(() => {
if (loading.value) loadFailed.value = true
}, LOAD_TIMEOUT)
}
function handleRefresh(): void {
refreshing.value = true
loading.value = true
if (iframeRef.value) {
iframeRef.value.src = iframeSrc.value
}
// 更新 cache-buster 触发 iframe 重载(赋相同值不会重新加载)
cacheBuster.value = Date.now()
startLoadTimer()
}
function handleOpenExternal(): void {
@@ -66,21 +89,28 @@ function handleOpenExternal(): void {
}
function onIframeLoad(): void {
clearLoadTimer()
loading.value = false
refreshing.value = false
}
// ==================== 生命周期 ====================
onMounted(async () => {
startLoadTimer()
try {
const result = await window.go?.main?.App?.GetCurrentVersion?.()
const result = await GetCurrentVersion()
if (result?.success) {
currentVersion.value = result.data?.version || ''
startLoadTimer()
}
} catch {
// Wails 未就绪时忽略
// 后端未就绪时忽略,不带版本参数加载
}
})
onUnmounted(() => {
clearLoadTimer()
})
</script>
<style scoped>
+1 -1
View File
@@ -97,7 +97,7 @@ export const useConfigStore = defineStore('config', () => {
appConfig.value = {
tabs: mergedTabs.map(tab => ({ ...tab, visible: mergedVisible.includes(tab.key) })),
visibleTabs: mergedVisible,
defaultTab: defaultTab || 'file-system',
defaultTab: allKeys.includes(defaultTab) ? defaultTab : 'file-system',
sidebarSections: Array.isArray(sidebarSections)
? sidebarSections.filter((s: string) => validSections.includes(s))
: defaultSidebar
+73 -16
View File
@@ -4,10 +4,15 @@ import { Message } from '@arco-design/web-vue'
import { formatBytes as formatFileSize } from '@/utils/fileUtils'
import { parseWailsEventData } from '@/utils/wailsEvent'
import {
CheckUpdate, GetUpdateConfig, DownloadUpdate, InstallUpdate
CheckUpdate, GetUpdateConfig, DownloadUpdate, InstallUpdate, InstallUpdateWithHash
} from '../wailsjs/v3-bindings/u-desk/app'
import { On, Off } from '@wailsio/events'
// 本次应用运行内已自动弹过的版本(升级后自动恢复提醒)
let sessionNotifiedVersion = ''
// 下载完成事件携带的远端声明哈希,安装时用于完整性校验
let lastExpectedSha256 = ''
/**
* 更新管理 Store
* 统一管理版本检查、下载、安装等更新相关逻辑
@@ -41,34 +46,65 @@ export const useUpdateStore = defineStore('update', () => {
}
// ==================== 核心方法 ====================
// 用户显式跳过的版本(localStorage):该版本不再自动弹窗,手动检查不受限
const SKIPPED_VERSION_KEY = 'udesk-update-skipped-version'
/**
* 展示更新弹窗。自动路径(静默检查/后端广播)按版本去重(用户已跳过或本次运行已弹过则不弹),手动检查强制弹
*/
const tryShowUpdate = (data: UpdateInfo, force = false) => {
const version = String(data?.latest_version || '')
if (!force && version) {
if (localStorage.getItem(SKIPPED_VERSION_KEY) === version) return
if (sessionNotifiedVersion === version) return
}
updateInfo.value = data
showUpdate.value = true
if (version) {
sessionNotifiedVersion = version
}
if (!force) {
// 自动发现时的系统通知
try {
window.runtime?.SendNotification?.({
title: 'U-Desk',
body: `发现新版本 ${version},点击查看`
})
} catch { /* 通知不可用时忽略 */ }
}
}
/**
* 稍后提醒:记录用户显式跳过的版本,该版本不再自动弹窗(手动检查仍强制弹)
*/
const skipVersion = (version: string) => {
if (version) {
localStorage.setItem(SKIPPED_VERSION_KEY, version)
}
showUpdate.value = false
}
/**
* 检查更新
* @param silent 是否静默模式(不显示消息)
* @param manual 是否手动触发(跳过 auto_check_enabled 门禁)
* @returns 检查是否实际完成(false=后端未就绪或网络失败,调用方可重试)
*/
const checkForUpdates = async (silent = false) => {
if (checking.value) return
const checkForUpdates = async (silent = false, manual = false): Promise<boolean> => {
if (checking.value) return true
checking.value = true
try {
const configResult = await GetUpdateConfig()
if (!configResult.success) return
if (!configResult.success) return false
const { auto_check_enabled } = configResult.data || {}
if (!auto_check_enabled) return
if (!manual && !auto_check_enabled) return true
const result = await CheckUpdate()
if (result.success && result.data?.has_update) {
updateInfo.value = result.data
showUpdate.value = true
// 系统通知:发现新版本
try {
window.runtime?.SendNotification?.({
title: 'U-Desk',
body: `发现新版本 ${result.data.latest_version},点击查看`
})
} catch { /* 通知不可用时忽略 */ }
tryShowUpdate(result.data, !silent)
if (!silent) {
Message.success('发现新版本!')
@@ -76,11 +112,13 @@ export const useUpdateStore = defineStore('update', () => {
} else if (!silent) {
Message.success('已是最新版本')
}
return result.success === true
} catch (error) {
if (!silent) {
console.error('检查更新失败:', error)
Message.error('检查更新失败:' + (error as Error).message)
}
return false
} finally {
checking.value = false
}
@@ -134,7 +172,10 @@ export const useUpdateStore = defineStore('update', () => {
installing.value = true
try {
const result = await InstallUpdate(filePath, true)
// 优先带哈希校验安装(期望哈希来自下载完成事件的远端声明)
const result = lastExpectedSha256
? await InstallUpdateWithHash(filePath, true, lastExpectedSha256, 'sha256')
: await InstallUpdate(filePath, true)
if (result.success) {
Message.success({
content: '安装成功!应用将在几秒后重启...',
@@ -200,6 +241,8 @@ export const useUpdateStore = defineStore('update', () => {
// 完成下载
downloadProgress.value = 100
downloadStatus.value = 'success'
// 记录远端声明的期望哈希,安装时用于校验
lastExpectedSha256 = String(data.expected_sha256 || '')
const fileSize = (data.file_size as number) || 0
@@ -227,12 +270,24 @@ export const useUpdateStore = defineStore('update', () => {
}, remainingTime)
}
/**
* 后端广播"发现新版本"处理(启动即查与周期检查命中时后端都会发)
*/
const onUpdateAvailable = (event: unknown) => {
const data = parseWailsEventData(event) as UpdateInfo
if (!data?.latest_version) {
return
}
tryShowUpdate(data)
}
/**
* 设置事件监听
*/
const setupEventListeners = () => {
On('download-progress', onDownloadProgress)
On('download-complete', onDownloadComplete)
On('update-available', onUpdateAvailable)
}
/**
@@ -241,6 +296,7 @@ export const useUpdateStore = defineStore('update', () => {
const removeEventListeners = () => {
Off('download-progress')
Off('download-complete')
Off('update-available')
}
// ==================== 返回 ====================
@@ -259,6 +315,7 @@ export const useUpdateStore = defineStore('update', () => {
checkForUpdates,
downloadUpdate,
installUpdate,
skipVersion,
setupEventListeners,
removeEventListeners,
formatFileSize,
+53 -32
View File
@@ -15,6 +15,9 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr
import * as application$0 from "../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as api$0 from "./internal/api/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as filesystem$0 from "./internal/filesystem/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -123,6 +126,25 @@ export function EmptyRecycleBin(): $CancellablePromise<void> {
return $Call.ByID(4176312624);
}
/**
* EnsureDrawioViewer 确保本地已缓存 drawio viewer(首次预览时下载),前端打开 .drawio 前调用。
* 通过 drawio-download-progress 事件推送下载进度。
*/
export function EnsureDrawioViewer(): $CancellablePromise<{ [_ in string]?: any }> {
return $Call.ByID(1345697171).then(($result: any) => {
return $$createType2($result);
});
}
/**
* ExportHtmlFileToPDF 将HTML文件导出为PDF
*/
export function ExportHtmlFileToPDF(path: string, saveName: string): $CancellablePromise<{ [_ in string]?: any }> {
return $Call.ByID(3012957793, path, saveName).then(($result: any) => {
return $$createType2($result);
});
}
/**
* ExportPDF 导出PDF文件
*/
@@ -182,15 +204,6 @@ export function GetCommonPaths(): $CancellablePromise<{ [_ in string]?: string }
});
}
/**
* EnsureDrawioViewer 确保 drawio viewer 已下载就绪(用于 .drawio/.dio 预览)
*/
export function EnsureDrawioViewer(): $CancellablePromise<{ [_ in string]?: any }> {
return $Call.ByID(1345697171).then(($result: any) => {
return $$createType2($result);
});
}
/**
* GetCurrentVersion 获取当前版本号
*/
@@ -335,7 +348,7 @@ export function LoadConnectionProfiles(): $CancellablePromise<{ [_ in string]?:
}
/**
* MovePath 移动文件或目录(同盘重命名,跨盘复制后删除)
* MovePath 移动文件或目录(同盘重命名,跨盘复制后删除),跨盘时经 transfer-progress 事件推送进度
*/
export function MovePath(src: string, dst: string): $CancellablePromise<void> {
return $Call.ByID(2028899675, src, dst);
@@ -348,6 +361,9 @@ export function OpenPath(path: string): $CancellablePromise<void> {
return $Call.ByID(1591734570, path);
}
/**
* OssConnect 连接 OSS 存储
*/
export function OssConnect(req: $models.OssConnectRequest): $CancellablePromise<string> {
return $Call.ByID(3667022538, req);
}
@@ -478,6 +494,16 @@ export function PickDirectory(): $CancellablePromise<string> {
return $Call.ByID(1347829059);
}
/**
* ReadClipboardFiles 读取系统剪贴板中的文件/文件夹路径列表。
* 剪贴板为文本/截图等非文件内容时返回空列表,前端据此回落图片粘贴等原有链路。
*/
export function ReadClipboardFiles(): $CancellablePromise<string[]> {
return $Call.ByID(2636780132).then(($result: any) => {
return $$createType7($result);
});
}
/**
* ReadFile 读取文件
*/
@@ -526,8 +552,9 @@ export function RestoreFromRecycleBin(recyclePath: string): $CancellablePromise<
/**
* SaveAppConfig 保存应用配置
* 直接复用 api.SaveAppConfigRequest,避免 main 包副本转发时丢字段
*/
export function SaveAppConfig(req: $models.SaveAppConfigRequest): $CancellablePromise<{ [_ in string]?: any }> {
export function SaveAppConfig(req: api$0.SaveAppConfigRequest): $CancellablePromise<{ [_ in string]?: any }> {
return $Call.ByID(1942219977, req).then(($result: any) => {
return $$createType2($result);
});
@@ -546,13 +573,6 @@ export function SaveConnectionProfile(req: $models.SaveProfileRequest): $Cancell
});
}
/**
* SelectPDFSaveDirectory 选择PDF保存目录
*/
export function SelectPDFSaveDirectory(): $CancellablePromise<string> {
return $Call.ByID(1403263131);
}
/**
* SetMainWindow 设置主窗口引用(由 main.go 在创建窗口后调用)
*/
@@ -624,6 +644,13 @@ export function SftpDownloadSiteForPreview(connID: string, remotePath: string):
return $Call.ByID(1591575570, connID, remotePath);
}
/**
* SftpDownloadToFile 下载远程文件/目录到用户指定目录(流式无上限,重名自动副本),返回本地落地路径
*/
export function SftpDownloadToFile(connID: string, remotePath: string, localDstDir: string): $CancellablePromise<string> {
return $Call.ByID(2935422351, connID, remotePath, localDstDir);
}
/**
* SftpDownloadToTemp 下载远程文件到本地临时目录(用于预览)
*/
@@ -638,20 +665,6 @@ export function SftpDownloadToTempCached(connID: string, remotePath: string, fil
return $Call.ByID(3935472409, connID, remotePath, fileSize, modTime);
}
/**
* SftpDownloadToFile 下载远程文件/目录到用户指定目录(流式无上限,重名自动副本)
*/
export function SftpDownloadToFile(connID: string, remotePath: string, localDstDir: string): $CancellablePromise<string> {
return $Call.ByID(2935422351, connID, remotePath, localDstDir);
}
/**
* SftpUploadFromFile 上传本地文件/目录到远程目录(流式,目录递归,重名自动副本)
*/
export function SftpUploadFromFile(connID: string, localPath: string, remoteDir: string): $CancellablePromise<void> {
return $Call.ByID(3304594335, connID, localPath, remoteDir);
}
/**
* SftpGetCommonPaths 获取 SFTP 远程主机常用路径
*/
@@ -704,6 +717,13 @@ export function SftpRenamePath(req: $models.SftpRenamePathRequest): $Cancellable
});
}
/**
* SftpUploadFromFile 上传本地文件/目录到远程目录(流式,目录递归,重名自动副本)
*/
export function SftpUploadFromFile(connID: string, localPath: string, remoteDir: string): $CancellablePromise<void> {
return $Call.ByID(3304594335, connID, localPath, remoteDir);
}
/**
* SftpWriteBase64File SFTP 写入 base64 编码的二进制文件(粘贴图片等)
*/
@@ -791,3 +811,4 @@ const $$createType3 = filesystem$0.FileOperationResult.createFrom;
const $$createType4 = $Create.Nullable($$createType3);
const $$createType5 = $Create.Array($$createType2);
const $$createType6 = $Create.Map($Create.Any, $Create.Any);
const $$createType7 = $Create.Array($Create.Any);
@@ -11,7 +11,6 @@ export {
OssConnectRequest,
OssRenamePathRequest,
RenamePathRequest,
SaveAppConfigRequest,
SaveBase64FileRequest,
SaveProfileRequest,
SftpConnectRequest,
@@ -2,5 +2,6 @@
// This file is automatically generated. DO NOT EDIT
export {
AppTabDefinition
AppTabDefinition,
SaveAppConfigRequest
} from "./models.js";
@@ -40,3 +40,56 @@ export class AppTabDefinition {
return new AppTabDefinition($$parsedSource as Partial<AppTabDefinition>);
}
}
/**
* SaveAppConfigRequest 保存应用配置请求(前端格式)
*/
export class SaveAppConfigRequest {
"tabs": AppTabDefinition[];
"visibleTabs": string[];
"defaultTab": string;
"sidebarSections": string[];
/** Creates a new SaveAppConfigRequest instance. */
constructor($$source: Partial<SaveAppConfigRequest> = {}) {
if (!("tabs" in $$source)) {
this["tabs"] = [];
}
if (!("visibleTabs" in $$source)) {
this["visibleTabs"] = [];
}
if (!("defaultTab" in $$source)) {
this["defaultTab"] = "";
}
if (!("sidebarSections" in $$source)) {
this["sidebarSections"] = [];
}
Object.assign(this, $$source);
}
/**
* Creates a new SaveAppConfigRequest instance from a string or object.
*/
static createFrom($$source: any = {}): SaveAppConfigRequest {
const $$createField0_0 = $$createType1;
const $$createField1_0 = $$createType2;
const $$createField3_0 = $$createType2;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("tabs" in $$parsedSource) {
$$parsedSource["tabs"] = $$createField0_0($$parsedSource["tabs"]);
}
if ("visibleTabs" in $$parsedSource) {
$$parsedSource["visibleTabs"] = $$createField1_0($$parsedSource["visibleTabs"]);
}
if ("sidebarSections" in $$parsedSource) {
$$parsedSource["sidebarSections"] = $$createField3_0($$parsedSource["sidebarSections"]);
}
return new SaveAppConfigRequest($$parsedSource as Partial<SaveAppConfigRequest>);
}
}
// Private type creation functions
const $$createType0 = AppTabDefinition.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $Create.Array($Create.Any);
@@ -5,10 +5,6 @@
// @ts-ignore: Unused imports
import { Create as $Create } from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as api$0 from "./internal/api/models.js";
/**
* BgmPlaylistItem 播放列表条目
*/
@@ -134,46 +130,6 @@ export class RenamePathRequest {
}
}
/**
* SaveAppConfigRequest 保存应用配置请求
*/
export class SaveAppConfigRequest {
"tabs": api$0.AppTabDefinition[];
"visibleTabs": string[];
"defaultTab": string;
/** Creates a new SaveAppConfigRequest instance. */
constructor($$source: Partial<SaveAppConfigRequest> = {}) {
if (!("tabs" in $$source)) {
this["tabs"] = [];
}
if (!("visibleTabs" in $$source)) {
this["visibleTabs"] = [];
}
if (!("defaultTab" in $$source)) {
this["defaultTab"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new SaveAppConfigRequest instance from a string or object.
*/
static createFrom($$source: any = {}): SaveAppConfigRequest {
const $$createField0_0 = $$createType1;
const $$createField1_0 = $$createType2;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("tabs" in $$parsedSource) {
$$parsedSource["tabs"] = $$createField0_0($$parsedSource["tabs"]);
}
if ("visibleTabs" in $$parsedSource) {
$$parsedSource["visibleTabs"] = $$createField1_0($$parsedSource["visibleTabs"]);
}
return new SaveAppConfigRequest($$parsedSource as Partial<SaveAppConfigRequest>);
}
}
/**
* SaveBase64FileRequest 保存 Base64 编码的二进制文件
*/
@@ -422,8 +378,3 @@ export class WriteFileRequest {
return new WriteFileRequest($$parsedSource as Partial<WriteFileRequest>);
}
}
// Private type creation functions
const $$createType0 = api$0.AppTabDefinition.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $Create.Array($Create.Any);