修复: 版本更新与设置面板走查问题
- 更新链路: 检查记录远端SHA256,下载完成事件回传,安装时带哈希校验 - 续传: 服务器忽略Range返回200时截断重下防追加损坏,文件名去query - 定时器: 自动检查可重入,配置变更免重启生效,关闭即停,下载加互斥 - 前端: 支持跳过此版本并按版本去重,手动检查不受门禁限制,启动未就绪延时重试 - 更新面板: 新增自动检查开关/间隔/地址配置与进度条状态映射 - 版本历史: cache参数强制刷新与加载超时重试兜底 - 配置: 侧边栏分区全链路持久化,保存请求复用api类型防字段丢失 - 侧边栏: 区块定位改显式类名,不再依赖排序位置 - 绑定: 重新生成并与镜像目录同步
This commit is contained in:
@@ -30,6 +30,7 @@ type App struct {
|
||||
mainWindow *application.WebviewWindow
|
||||
updateAPI *api.UpdateAPI
|
||||
updateTicker *time.Ticker
|
||||
updateCheckStop chan struct{}
|
||||
configAPI *api.ConfigAPI
|
||||
pdfAPI *api.PdfAPI
|
||||
filesystem *filesystem.FileSystemService
|
||||
@@ -172,6 +173,7 @@ func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions)
|
||||
a.mainWindow.EmitEvent(name, data...)
|
||||
}
|
||||
})
|
||||
a.updateAPI.SetConfigChangedCallback(a.resetAutoUpdateCheck)
|
||||
a.startAutoUpdateCheck()
|
||||
}
|
||||
}()
|
||||
@@ -200,9 +202,7 @@ func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions)
|
||||
|
||||
// ServiceShutdown Wails v3 服务关闭生命周期(替代 v2 的 Shutdown)
|
||||
func (a *App) ServiceShutdown() error {
|
||||
if a.updateTicker != nil {
|
||||
a.updateTicker.Stop()
|
||||
}
|
||||
a.stopUpdateTicker()
|
||||
if a.unregisterHotkey != nil {
|
||||
a.unregisterHotkey()
|
||||
}
|
||||
@@ -273,13 +273,6 @@ type RenamePathRequest struct {
|
||||
NewPath string `json:"newPath"`
|
||||
}
|
||||
|
||||
// SaveAppConfigRequest 保存应用配置请求
|
||||
type SaveAppConfigRequest struct {
|
||||
Tabs []api.AppTabDefinition `json:"tabs"`
|
||||
VisibleTabs []string `json:"visibleTabs"`
|
||||
DefaultTab string `json:"defaultTab"`
|
||||
}
|
||||
|
||||
// SftpConnectRequest SFTP 连接请求
|
||||
type SftpConnectRequest struct {
|
||||
Host string `json:"host"`
|
||||
|
||||
+3
-8
@@ -26,7 +26,8 @@ func (a *App) GetAppConfig() (map[string]interface{}, error) {
|
||||
}
|
||||
|
||||
// SaveAppConfig 保存应用配置
|
||||
func (a *App) SaveAppConfig(req SaveAppConfigRequest) (map[string]interface{}, error) {
|
||||
// 直接复用 api.SaveAppConfigRequest,避免 main 包副本转发时丢字段
|
||||
func (a *App) SaveAppConfig(req api.SaveAppConfigRequest) (map[string]interface{}, error) {
|
||||
if a.configAPI == nil {
|
||||
return nil, fmt.Errorf("配置服务正在初始化中")
|
||||
}
|
||||
@@ -41,13 +42,7 @@ func (a *App) SaveAppConfig(req SaveAppConfigRequest) (map[string]interface{}, e
|
||||
}
|
||||
}
|
||||
|
||||
apiReq := api.SaveAppConfigRequest{
|
||||
Tabs: req.Tabs,
|
||||
VisibleTabs: req.VisibleTabs,
|
||||
DefaultTab: req.DefaultTab,
|
||||
}
|
||||
|
||||
result, err := a.configAPI.SaveAppConfig(apiReq)
|
||||
result, err := a.configAPI.SaveAppConfig(req)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
+42
-3
@@ -92,7 +92,7 @@ func (a *App) VerifyUpdateFile(filePath string, expectedHash string, hashType st
|
||||
return api.VerifyUpdateFile(filePath, expectedHash, hashType)
|
||||
}
|
||||
|
||||
// startAutoUpdateCheck 启动自动更新检查
|
||||
// startAutoUpdateCheck 启动自动更新检查(可重入:先停止旧定时器再按当前配置新建)
|
||||
func (a *App) startAutoUpdateCheck() {
|
||||
updateAPI, err := a.requireUpdateAPI()
|
||||
if err != nil {
|
||||
@@ -116,6 +116,8 @@ func (a *App) startAutoUpdateCheck() {
|
||||
|
||||
autoCheckEnabled, ok := configData["auto_check_enabled"].(bool)
|
||||
if !ok || !autoCheckEnabled {
|
||||
// 关闭自动检查时停止存量定时器
|
||||
a.stopUpdateTicker()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -124,16 +126,53 @@ func (a *App) startAutoUpdateCheck() {
|
||||
interval = 5
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
if a.updateCheckStop != nil {
|
||||
close(a.updateCheckStop)
|
||||
}
|
||||
if a.updateTicker != nil {
|
||||
a.updateTicker.Stop()
|
||||
}
|
||||
stop := make(chan struct{})
|
||||
a.updateCheckStop = stop
|
||||
a.updateTicker = time.NewTicker(time.Duration(interval) * time.Minute)
|
||||
ticker := a.updateTicker
|
||||
a.mu.Unlock()
|
||||
|
||||
go a.checkUpdate()
|
||||
|
||||
a.updateTicker = time.NewTicker(time.Duration(interval) * time.Minute)
|
||||
go func() {
|
||||
for range a.updateTicker.C {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
a.checkUpdate()
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// stopUpdateTicker 停止自动更新检查定时器及其 goroutine
|
||||
func (a *App) stopUpdateTicker() {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.updateCheckStop != nil {
|
||||
close(a.updateCheckStop)
|
||||
a.updateCheckStop = nil
|
||||
}
|
||||
if a.updateTicker != nil {
|
||||
a.updateTicker.Stop()
|
||||
a.updateTicker = nil
|
||||
}
|
||||
}
|
||||
|
||||
// resetAutoUpdateCheck 重置自动更新检查(更新配置变更后调用,免重启生效)
|
||||
func (a *App) resetAutoUpdateCheck() {
|
||||
a.stopUpdateTicker()
|
||||
a.startAutoUpdateCheck()
|
||||
}
|
||||
|
||||
// checkUpdate 执行更新检查
|
||||
func (a *App) checkUpdate() {
|
||||
defer func() {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,8 +196,7 @@ const getProgressModalContent = () => {
|
||||
]
|
||||
}
|
||||
|
||||
// 安装中状态
|
||||
if (updateStore.installing) {
|
||||
// 安装中状态:下载完成后进入安装阶段,弹窗保持打开直到进程重启
|
||||
return [
|
||||
h('div', { style: { marginBottom: '16px' } }, [
|
||||
h('div', { style: { marginBottom: '8px', fontSize: '14px', color: 'var(--color-text-2)' } }, '正在安装更新...')
|
||||
@@ -197,15 +205,6 @@ const getProgressModalContent = () => {
|
||||
]
|
||||
}
|
||||
|
||||
// 完成状态
|
||||
return [
|
||||
h('div', { style: { marginBottom: '16px', textAlign: 'center' } }, [
|
||||
h('div', { style: { fontSize: '16px', color: 'rgb(var(--success-6))', marginBottom: '8px' } }, '✓ 更新完成')
|
||||
]),
|
||||
h('div', { style: { fontSize: '14px', color: 'var(--color-text-2)' } }, '应用将在几秒后自动重启...')
|
||||
]
|
||||
}
|
||||
|
||||
// 更新进度弹窗内容
|
||||
const updateProgressModal = async () => {
|
||||
if (!progressModalInstance) return
|
||||
@@ -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) {
|
||||
// 下载/安装状态收尾:下载失败收起进度弹窗;安装流程结束(重启前或失败)关闭弹窗
|
||||
watch(
|
||||
() => [updateStore.installing, updateStore.downloadStatus],
|
||||
([installing, status], [prevInstalling]) => {
|
||||
if (status === 'exception') {
|
||||
closeProgressModal()
|
||||
Message.error('下载失败:' + data.error)
|
||||
return
|
||||
}
|
||||
|
||||
if (!data.success || !data.file_path) {
|
||||
closeProgressModal()
|
||||
Message.error('下载完成但数据不完整')
|
||||
return
|
||||
}
|
||||
|
||||
// 等待安装完成
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
if (prevInstalling && !installing) {
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -27,6 +27,7 @@ type GetAppConfigResponse struct {
|
||||
Tabs []AppTabDefinition `json:"tabs"`
|
||||
VisibleTabs []string `json:"visibleTabs"`
|
||||
DefaultTab string `json:"defaultTab"`
|
||||
SidebarSections []string `json:"sidebarSections"`
|
||||
}
|
||||
|
||||
// AppTabDefinition 应用 Tab 定义(前端格式)
|
||||
@@ -42,6 +43,7 @@ type SaveAppConfigRequest struct {
|
||||
Tabs []AppTabDefinition `json:"tabs"`
|
||||
VisibleTabs []string `json:"visibleTabs"`
|
||||
DefaultTab string `json:"defaultTab"`
|
||||
SidebarSections []string `json:"sidebarSections"`
|
||||
}
|
||||
|
||||
// GetAppConfig 获取应用配置
|
||||
@@ -76,6 +78,7 @@ func (api *ConfigAPI) GetAppConfig() (map[string]interface{}, error) {
|
||||
Tabs: tabs,
|
||||
VisibleTabs: tabConfig.VisibleTabs,
|
||||
DefaultTab: tabConfig.DefaultTab,
|
||||
SidebarSections: tabConfig.SidebarSections,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -119,6 +122,7 @@ func (api *ConfigAPI) SaveAppConfig(req SaveAppConfigRequest) (map[string]interf
|
||||
AvailableTabs: availableTabs,
|
||||
VisibleTabs: req.VisibleTabs,
|
||||
DefaultTab: req.DefaultTab,
|
||||
SidebarSections: req.SidebarSections,
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
@@ -204,5 +208,6 @@ func (api *ConfigAPI) MigrateTabConfig() error {
|
||||
AvailableTabs: newTabs,
|
||||
VisibleTabs: newVisible,
|
||||
DefaultTab: defaultTab,
|
||||
SidebarSections: config.SidebarSections,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"u-desk/internal/service"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"u-desk/internal/service"
|
||||
)
|
||||
|
||||
// UpdateAPI 版本更新 API
|
||||
@@ -11,6 +13,9 @@ type UpdateAPI struct {
|
||||
updateService *service.UpdateService
|
||||
ctx context.Context
|
||||
eventEmitter func(name string, data ...any)
|
||||
configChanged func() // 更新配置变更回调(重置自动检查定时器)
|
||||
expectedSHA256 string // 最近一次检查得到的远端包 SHA256
|
||||
downloading atomic.Bool // 下载进行中标志(并发互斥)
|
||||
}
|
||||
|
||||
// NewUpdateAPI 创建版本更新 API
|
||||
@@ -25,6 +30,11 @@ func (api *UpdateAPI) SetEventEmitter(emitter func(name string, data ...any)) {
|
||||
api.eventEmitter = emitter
|
||||
}
|
||||
|
||||
// SetConfigChangedCallback 设置配置变更回调(由 App 注入,用于重置自动检查定时器)
|
||||
func (api *UpdateAPI) SetConfigChangedCallback(cb func()) {
|
||||
api.configChanged = cb
|
||||
}
|
||||
|
||||
// SetContext 设置上下文(用于事件推送)
|
||||
func (api *UpdateAPI) SetContext(ctx context.Context) {
|
||||
api.ctx = ctx
|
||||
@@ -54,6 +64,9 @@ func (api *UpdateAPI) CheckUpdate() (map[string]interface{}, error) {
|
||||
return errorResponse(err.Error()), nil
|
||||
}
|
||||
|
||||
// 记录远端包哈希,供下载完成事件比对
|
||||
api.expectedSHA256 = result.SHA256
|
||||
|
||||
return successResponse(result), nil
|
||||
}
|
||||
|
||||
@@ -99,6 +112,11 @@ func (api *UpdateAPI) SetUpdateConfig(autoCheckEnabled bool, checkIntervalMinute
|
||||
return errorResponse(err.Error()), nil
|
||||
}
|
||||
|
||||
// 配置已变更,通知重置自动检查定时器(免重启生效)
|
||||
if api.configChanged != nil {
|
||||
api.configChanged()
|
||||
}
|
||||
|
||||
return successResponse(map[string]interface{}{
|
||||
"message": "配置保存成功",
|
||||
}), nil
|
||||
@@ -110,7 +128,14 @@ func (api *UpdateAPI) DownloadUpdate(downloadURL string) (map[string]interface{}
|
||||
return errorResponse("下载地址不能为空"), nil
|
||||
}
|
||||
|
||||
// 并发互斥:已有下载任务进行中则拒绝
|
||||
if !api.downloading.CompareAndSwap(false, true) {
|
||||
return errorResponse("已有下载任务进行中"), nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer api.downloading.Store(false)
|
||||
|
||||
progressCallback := func(progress float64, speed float64, downloaded int64, total int64) {
|
||||
progressInfo := map[string]interface{}{
|
||||
"progress": progress,
|
||||
@@ -132,6 +157,8 @@ func (api *UpdateAPI) DownloadUpdate(downloadURL string) (map[string]interface{}
|
||||
"success": true,
|
||||
"file_path": result.FilePath,
|
||||
"file_size": result.FileSize,
|
||||
"sha256": result.SHA256Hash,
|
||||
"expected_sha256": api.expectedSHA256,
|
||||
}
|
||||
api.emit("download-complete", resultInfo)
|
||||
}
|
||||
|
||||
@@ -36,8 +36,12 @@ type TabConfig struct {
|
||||
AvailableTabs []TabDefinition `json:"available_tabs"`
|
||||
VisibleTabs []string `json:"visible_tabs"`
|
||||
DefaultTab string `json:"default_tab"`
|
||||
SidebarSections []string `json:"sidebar_sections"`
|
||||
}
|
||||
|
||||
// 默认侧边栏分区
|
||||
var defaultSidebarSections = []string{"server", "favorites", "help"}
|
||||
|
||||
// 默认 Tab 配置
|
||||
var defaultTabConfig = TabConfig{
|
||||
AvailableTabs: []TabDefinition{
|
||||
@@ -47,6 +51,7 @@ var defaultTabConfig = TabConfig{
|
||||
},
|
||||
VisibleTabs: []string{"file-system", "markdown-editor", "version"},
|
||||
DefaultTab: "file-system",
|
||||
SidebarSections: defaultSidebarSections,
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -79,6 +84,11 @@ func (s *ConfigService) GetTabConfig() (*TabConfig, error) {
|
||||
return &defaultTabConfig, nil
|
||||
}
|
||||
|
||||
// 侧边栏分区为空时回退默认值(兼容旧配置)
|
||||
if len(tabConfig.SidebarSections) == 0 {
|
||||
tabConfig.SidebarSections = defaultSidebarSections
|
||||
}
|
||||
|
||||
return &tabConfig, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ type RemoteVersionInfo struct {
|
||||
ForceUpdate bool `json:"force_update"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
SHA256 string `json:"sha256"`
|
||||
}
|
||||
|
||||
// UpdateCheckResult 更新检查结果
|
||||
@@ -42,6 +43,7 @@ type UpdateCheckResult struct {
|
||||
ForceUpdate bool `json:"force_update"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
SHA256 string `json:"sha256"`
|
||||
}
|
||||
|
||||
// InstallResult 安装结果
|
||||
@@ -99,6 +101,7 @@ func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) {
|
||||
ForceUpdate: remoteInfo.ForceUpdate,
|
||||
ReleaseDate: remoteInfo.ReleaseDate,
|
||||
FileSize: remoteInfo.FileSize,
|
||||
SHA256: remoteInfo.SHA256,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"u-desk/internal/common"
|
||||
@@ -41,9 +42,13 @@ func DownloadUpdate(downloadURL string, progressCallback DownloadProgress) (*Dow
|
||||
return nil, fmt.Errorf("创建下载目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 从 URL 提取文件名
|
||||
filename := filepath.Base(downloadURL)
|
||||
if filename == "" || filename == "." {
|
||||
// 从 URL 提取文件名(去除 query 参数与 # 片段)
|
||||
cleanURL := downloadURL
|
||||
if idx := strings.IndexAny(cleanURL, "?#"); idx != -1 {
|
||||
cleanURL = cleanURL[:idx]
|
||||
}
|
||||
filename := filepath.Base(cleanURL)
|
||||
if filename == "" || filename == "." || filename == "/" {
|
||||
filename = fmt.Sprintf("update-%d.exe", time.Now().Unix())
|
||||
}
|
||||
|
||||
@@ -127,6 +132,15 @@ func DownloadUpdate(downloadURL string, progressCallback DownloadProgress) (*Dow
|
||||
return nil, fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 服务器忽略 Range 返回 200 时,本地已下载部分不能追加,必须截断从头写,否则文件损坏
|
||||
if resp.StatusCode == http.StatusOK && downloadedSize > 0 {
|
||||
if err := file.Truncate(0); err != nil {
|
||||
return nil, fmt.Errorf("重置下载文件失败: %v", err)
|
||||
}
|
||||
downloadedSize = 0
|
||||
log.Printf("[下载] 服务器不支持断点续传(返回 200),从头下载")
|
||||
}
|
||||
|
||||
// 获取文件总大小
|
||||
contentLength := resp.ContentLength
|
||||
gotTotalFromRange := false
|
||||
|
||||
Reference in New Issue
Block a user