修复: 版本更新与设置面板走查问题
- 更新链路: 检查记录远端SHA256,下载完成事件回传,安装时带哈希校验 - 续传: 服务器忽略Range返回200时截断重下防追加损坏,文件名去query - 定时器: 自动检查可重入,配置变更免重启生效,关闭即停,下载加互斥 - 前端: 支持跳过此版本并按版本去重,手动检查不受门禁限制,启动未就绪延时重试 - 更新面板: 新增自动检查开关/间隔/地址配置与进度条状态映射 - 版本历史: cache参数强制刷新与加载超时重试兜底 - 配置: 侧边栏分区全链路持久化,保存请求复用api类型防字段丢失 - 侧边栏: 区块定位改显式类名,不再依赖排序位置 - 绑定: 重新生成并与镜像目录同步
This commit is contained in:
@@ -30,6 +30,7 @@ type App struct {
|
|||||||
mainWindow *application.WebviewWindow
|
mainWindow *application.WebviewWindow
|
||||||
updateAPI *api.UpdateAPI
|
updateAPI *api.UpdateAPI
|
||||||
updateTicker *time.Ticker
|
updateTicker *time.Ticker
|
||||||
|
updateCheckStop chan struct{}
|
||||||
configAPI *api.ConfigAPI
|
configAPI *api.ConfigAPI
|
||||||
pdfAPI *api.PdfAPI
|
pdfAPI *api.PdfAPI
|
||||||
filesystem *filesystem.FileSystemService
|
filesystem *filesystem.FileSystemService
|
||||||
@@ -172,6 +173,7 @@ func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions)
|
|||||||
a.mainWindow.EmitEvent(name, data...)
|
a.mainWindow.EmitEvent(name, data...)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
a.updateAPI.SetConfigChangedCallback(a.resetAutoUpdateCheck)
|
||||||
a.startAutoUpdateCheck()
|
a.startAutoUpdateCheck()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -200,9 +202,7 @@ func (a *App) ServiceStartup(ctx context.Context, _ application.ServiceOptions)
|
|||||||
|
|
||||||
// ServiceShutdown Wails v3 服务关闭生命周期(替代 v2 的 Shutdown)
|
// ServiceShutdown Wails v3 服务关闭生命周期(替代 v2 的 Shutdown)
|
||||||
func (a *App) ServiceShutdown() error {
|
func (a *App) ServiceShutdown() error {
|
||||||
if a.updateTicker != nil {
|
a.stopUpdateTicker()
|
||||||
a.updateTicker.Stop()
|
|
||||||
}
|
|
||||||
if a.unregisterHotkey != nil {
|
if a.unregisterHotkey != nil {
|
||||||
a.unregisterHotkey()
|
a.unregisterHotkey()
|
||||||
}
|
}
|
||||||
@@ -273,13 +273,6 @@ type RenamePathRequest struct {
|
|||||||
NewPath string `json:"newPath"`
|
NewPath string `json:"newPath"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveAppConfigRequest 保存应用配置请求
|
|
||||||
type SaveAppConfigRequest struct {
|
|
||||||
Tabs []api.AppTabDefinition `json:"tabs"`
|
|
||||||
VisibleTabs []string `json:"visibleTabs"`
|
|
||||||
DefaultTab string `json:"defaultTab"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SftpConnectRequest SFTP 连接请求
|
// SftpConnectRequest SFTP 连接请求
|
||||||
type SftpConnectRequest struct {
|
type SftpConnectRequest struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
|
|||||||
+3
-8
@@ -26,7 +26,8 @@ func (a *App) GetAppConfig() (map[string]interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SaveAppConfig 保存应用配置
|
// 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 {
|
if a.configAPI == nil {
|
||||||
return nil, fmt.Errorf("配置服务正在初始化中")
|
return nil, fmt.Errorf("配置服务正在初始化中")
|
||||||
}
|
}
|
||||||
@@ -41,13 +42,7 @@ func (a *App) SaveAppConfig(req SaveAppConfigRequest) (map[string]interface{}, e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apiReq := api.SaveAppConfigRequest{
|
result, err := a.configAPI.SaveAppConfig(req)
|
||||||
Tabs: req.Tabs,
|
|
||||||
VisibleTabs: req.VisibleTabs,
|
|
||||||
DefaultTab: req.DefaultTab,
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := a.configAPI.SaveAppConfig(apiReq)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-4
@@ -92,7 +92,7 @@ func (a *App) VerifyUpdateFile(filePath string, expectedHash string, hashType st
|
|||||||
return api.VerifyUpdateFile(filePath, expectedHash, hashType)
|
return api.VerifyUpdateFile(filePath, expectedHash, hashType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startAutoUpdateCheck 启动自动更新检查
|
// startAutoUpdateCheck 启动自动更新检查(可重入:先停止旧定时器再按当前配置新建)
|
||||||
func (a *App) startAutoUpdateCheck() {
|
func (a *App) startAutoUpdateCheck() {
|
||||||
updateAPI, err := a.requireUpdateAPI()
|
updateAPI, err := a.requireUpdateAPI()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,6 +116,8 @@ func (a *App) startAutoUpdateCheck() {
|
|||||||
|
|
||||||
autoCheckEnabled, ok := configData["auto_check_enabled"].(bool)
|
autoCheckEnabled, ok := configData["auto_check_enabled"].(bool)
|
||||||
if !ok || !autoCheckEnabled {
|
if !ok || !autoCheckEnabled {
|
||||||
|
// 关闭自动检查时停止存量定时器
|
||||||
|
a.stopUpdateTicker()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,16 +126,53 @@ func (a *App) startAutoUpdateCheck() {
|
|||||||
interval = 5
|
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()
|
go a.checkUpdate()
|
||||||
|
|
||||||
a.updateTicker = time.NewTicker(time.Duration(interval) * time.Minute)
|
|
||||||
go func() {
|
go func() {
|
||||||
for range a.updateTicker.C {
|
for {
|
||||||
a.checkUpdate()
|
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 执行更新检查
|
// checkUpdate 执行更新检查
|
||||||
func (a *App) checkUpdate() {
|
func (a *App) checkUpdate() {
|
||||||
defer func() {
|
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";
|
import * as application$0 from "../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore: Unused imports
|
// @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";
|
import * as filesystem$0 from "./internal/filesystem/models.js";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
@@ -549,8 +552,9 @@ export function RestoreFromRecycleBin(recyclePath: string): $CancellablePromise<
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* SaveAppConfig 保存应用配置
|
* 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 $Call.ByID(1942219977, req).then(($result: any) => {
|
||||||
return $$createType2($result);
|
return $$createType2($result);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export {
|
|||||||
OssConnectRequest,
|
OssConnectRequest,
|
||||||
OssRenamePathRequest,
|
OssRenamePathRequest,
|
||||||
RenamePathRequest,
|
RenamePathRequest,
|
||||||
SaveAppConfigRequest,
|
|
||||||
SaveBase64FileRequest,
|
SaveBase64FileRequest,
|
||||||
SaveProfileRequest,
|
SaveProfileRequest,
|
||||||
SftpConnectRequest,
|
SftpConnectRequest,
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AppTabDefinition
|
AppTabDefinition,
|
||||||
|
SaveAppConfigRequest
|
||||||
} from "./models.js";
|
} from "./models.js";
|
||||||
|
|||||||
@@ -40,3 +40,56 @@ export class AppTabDefinition {
|
|||||||
return new AppTabDefinition($$parsedSource as Partial<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
|
// @ts-ignore: Unused imports
|
||||||
import { Create as $Create } from "@wailsio/runtime";
|
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 播放列表条目
|
* 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 编码的二进制文件
|
* SaveBase64FileRequest 保存 Base64 编码的二进制文件
|
||||||
*/
|
*/
|
||||||
@@ -422,8 +378,3 @@ export class WriteFileRequest {
|
|||||||
return new WriteFileRequest($$parsedSource as Partial<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
|
<UpdateNotification
|
||||||
v-model="updateStore.showUpdate"
|
v-model="updateStore.showUpdate"
|
||||||
:update-info="updateStore.updateInfo"
|
:update-info="updateStore.updateInfo"
|
||||||
@install="updateStore.installUpdate"
|
|
||||||
/>
|
/>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
</template>
|
</template>
|
||||||
@@ -179,9 +178,12 @@ onMounted(() => {
|
|||||||
// 禁用浏览器默认右键菜单(桌面应用体验)
|
// 禁用浏览器默认右键菜单(桌面应用体验)
|
||||||
document.addEventListener('contextmenu', e => e.preventDefault())
|
document.addEventListener('contextmenu', e => e.preventDefault())
|
||||||
|
|
||||||
// 延迟检查更新(启动后 3 秒,静默模式)
|
// 延迟检查更新(启动后 3 秒,静默模式;后端更新模块异步初始化,未就绪则 15 秒后重试一次)
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
updateStore.checkForUpdates(true)
|
const ok = await updateStore.checkForUpdates(true)
|
||||||
|
if (!ok) {
|
||||||
|
setTimeout(() => { updateStore.checkForUpdates(true) }, 15000)
|
||||||
|
}
|
||||||
}, 3000)
|
}, 3000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div v-show="config.visible" class="sidebar" :style="{ width: width + 'px' }">
|
<div v-show="config.visible" class="sidebar" :style="{ width: width + 'px' }">
|
||||||
<template v-for="section in sectionOrder" :key="section">
|
<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">
|
<div class="section-header" @click="serverCollapsed = !serverCollapsed">
|
||||||
<span class="section-title">🖥️ 服务器</span>
|
<span class="section-title">🖥️ 服务器</span>
|
||||||
<icon-down v-if="!serverCollapsed" class="section-toggle" />
|
<icon-down v-if="!serverCollapsed" class="section-toggle" />
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
</div>
|
</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">
|
<div class="section-header" @click="helpCollapsed = !helpCollapsed">
|
||||||
<span class="section-title">📖 帮助</span>
|
<span class="section-title">📖 帮助</span>
|
||||||
<icon-down v-if="!helpCollapsed" class="section-toggle" />
|
<icon-down v-if="!helpCollapsed" class="section-toggle" />
|
||||||
@@ -694,13 +694,13 @@ function onSrvCtxVisible(index: number, visible: boolean) {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 服务器区块不收缩 */
|
/* 服务器区块不收缩(按显式类名定位,不依赖排序位置) */
|
||||||
.sidebar-section:first-child {
|
.server-section {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 服务器区块不需要 max-height 限制 */
|
/* 服务器区块不需要 max-height 限制 */
|
||||||
.sidebar-section:first-child > .section-content-wrap {
|
.server-section > .section-content-wrap {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -736,8 +736,8 @@ function onSrvCtxVisible(index: number, visible: boolean) {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 帮助区块固定在底部 */
|
/* 帮助区块不收缩(按显式类名定位,不依赖排序位置) */
|
||||||
.sidebar-section:last-child {
|
.help-section {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, watch, onMounted, onUnmounted, h, nextTick } from 'vue'
|
import { computed, ref, watch, onMounted, onUnmounted, h, nextTick } from 'vue'
|
||||||
import { Modal, Message, Progress } from '@arco-design/web-vue'
|
import { Modal, Message, Progress, Button } from '@arco-design/web-vue'
|
||||||
import { useUpdateStore } from '../stores/update'
|
import { useUpdateStore } from '../stores/update'
|
||||||
import { parseMarkdown, isMarkedReady, preloadMarked } from '../utils/markedExtensions'
|
import { parseMarkdown, isMarkedReady, preloadMarked } from '../utils/markedExtensions'
|
||||||
import { sanitizeHtml } from '@/utils/fileUtils'
|
import { sanitizeHtml } from '@/utils/fileUtils'
|
||||||
import { parseWailsEventData } from '@/utils/wailsEvent'
|
import { GetCurrentVersion } from '../wailsjs/v3-bindings/u-desk/app'
|
||||||
import { DownloadUpdate } from '../wailsjs/v3-bindings/u-desk/app'
|
|
||||||
import { On, Off } from '@wailsio/events'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -33,7 +31,9 @@ let confirmModalInstance = null
|
|||||||
let progressModalInstance = null
|
let progressModalInstance = null
|
||||||
|
|
||||||
// Computed
|
// 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 latestVersion = computed(() => props.updateInfo?.latest_version || '')
|
||||||
const changelog = computed(() => props.updateInfo?.changelog || '')
|
const changelog = computed(() => props.updateInfo?.changelog || '')
|
||||||
const forceUpdate = computed(() => props.updateInfo?.force_update || false)
|
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
|
return elements
|
||||||
},
|
},
|
||||||
okText: '立即更新',
|
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 [
|
return [
|
||||||
h('div', { style: { marginBottom: '16px', textAlign: 'center' } }, [
|
h('div', { style: { marginBottom: '16px' } }, [
|
||||||
h('div', { style: { fontSize: '16px', color: 'rgb(var(--success-6))', marginBottom: '8px' } }, '✓ 更新完成')
|
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()
|
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 () => {
|
const handleDownload = async () => {
|
||||||
await showProgressModal()
|
await showProgressModal()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await DownloadUpdate(props.updateInfo.download_url)
|
await updateStore.downloadUpdate()
|
||||||
if (result.success) return
|
|
||||||
|
|
||||||
closeProgressModal()
|
|
||||||
Message.error(result.message || '下载启动失败')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('下载失败:', error)
|
console.error('下载失败:', error)
|
||||||
closeProgressModal()
|
closeProgressModal()
|
||||||
@@ -291,35 +297,34 @@ const handleDownload = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载完成处理(本地覆盖:关闭弹窗)
|
// 下载/安装状态收尾:下载失败收起进度弹窗;安装流程结束(重启前或失败)关闭弹窗
|
||||||
const onDownloadComplete = async (event) => {
|
watch(
|
||||||
const data = parseWailsEventData(event)
|
() => [updateStore.installing, updateStore.downloadStatus],
|
||||||
|
([installing, status], [prevInstalling]) => {
|
||||||
if (data.error) {
|
if (status === 'exception') {
|
||||||
closeProgressModal()
|
closeProgressModal()
|
||||||
Message.error('下载失败:' + data.error)
|
return
|
||||||
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(() => {
|
onMounted(async () => {
|
||||||
On('download-complete', onDownloadComplete)
|
try {
|
||||||
|
const result = await GetCurrentVersion()
|
||||||
|
if (result.success) {
|
||||||
|
backendVersion.value = result.data?.version || ''
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 后端未就绪时忽略,留空兜底
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
Off('download-complete')
|
|
||||||
closeModals()
|
closeModals()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -45,6 +45,30 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</a-space>
|
</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
|
<a-alert
|
||||||
v-if="updateInfo"
|
v-if="updateInfo"
|
||||||
@@ -67,7 +91,7 @@
|
|||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-download />
|
<icon-download />
|
||||||
</template>
|
</template>
|
||||||
{{ downloadProgress > 0 ? '下载中...' : '下载更新' }}
|
{{ downloadButtonText }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
v-if="downloadedFile"
|
v-if="downloadedFile"
|
||||||
@@ -89,7 +113,7 @@
|
|||||||
<div v-if="downloadProgress > 0 || downloading" class="download-progress">
|
<div v-if="downloadProgress > 0 || downloading" class="download-progress">
|
||||||
<a-progress
|
<a-progress
|
||||||
:percent="downloadProgress"
|
:percent="downloadProgress"
|
||||||
:status="downloadStatus"
|
:status="progressBarStatus"
|
||||||
/>
|
/>
|
||||||
<div class="progress-info">
|
<div class="progress-info">
|
||||||
<span>{{ updateStore.formatFileSize(progressInfo.downloaded) }} / {{ updateStore.formatFileSize(progressInfo.total) }}</span>
|
<span>{{ updateStore.formatFileSize(progressInfo.downloaded) }} / {{ updateStore.formatFileSize(progressInfo.total) }}</span>
|
||||||
@@ -113,7 +137,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { Message, Modal } from '@arco-design/web-vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { IconHistory } from '@arco-design/web-vue/es/icon'
|
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 { parseMarkdown, isMarkedReady, preloadMarked } from '../utils/markedExtensions'
|
||||||
import { sanitizeHtml } from '@/utils/fileUtils'
|
import { sanitizeHtml } from '@/utils/fileUtils'
|
||||||
import { parseWailsEventData } from '@/utils/wailsEvent'
|
import { parseWailsEventData } from '@/utils/wailsEvent'
|
||||||
import { GetCurrentVersion, GetUpdateConfig, InstallUpdate } from '../wailsjs/v3-bindings/u-desk/app'
|
import { GetCurrentVersion, GetUpdateConfig, SetUpdateConfig, InstallUpdate } from '../wailsjs/v3-bindings/u-desk/app'
|
||||||
import { On, Off } from '@wailsio/events'
|
import { Events } from '@wailsio/runtime'
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
defineEmits(['open-version-history'])
|
defineEmits(['open-version-history'])
|
||||||
@@ -139,6 +163,26 @@ const lastCheckTime = ref('-')
|
|||||||
const installResult = ref(null)
|
const installResult = ref(null)
|
||||||
const downloadedFile = 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) */
|
/** 渲染 changelog(Markdown → HTML) */
|
||||||
function renderChangelog(text: string): string {
|
function renderChangelog(text: string): string {
|
||||||
if (!text) return ''
|
if (!text) return ''
|
||||||
@@ -163,13 +207,39 @@ const loadConfig = async () => {
|
|||||||
const result = await GetUpdateConfig()
|
const result = await GetUpdateConfig()
|
||||||
if (!result.success) return
|
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
|
lastCheckTime.value = last_check_time
|
||||||
|
autoCheckEnabled.value = auto_check_enabled
|
||||||
|
checkIntervalMinutes.value = check_interval_minutes
|
||||||
|
checkURL.value = check_url
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载配置失败:', 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 () => {
|
const handleCheckUpdate = async () => {
|
||||||
installResult.value = null
|
installResult.value = null
|
||||||
@@ -241,11 +311,11 @@ onMounted(async () => {
|
|||||||
await loadConfig()
|
await loadConfig()
|
||||||
|
|
||||||
// 监听下载完成事件(仅用于记录文件路径)
|
// 监听下载完成事件(仅用于记录文件路径)
|
||||||
On('download-complete', onDownloadComplete)
|
Events.On('download-complete', onDownloadComplete)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
Off('download-complete')
|
Events.Off('download-complete')
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -353,4 +423,24 @@ onUnmounted(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-text-3);
|
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>
|
</style>
|
||||||
|
|||||||
@@ -17,14 +17,20 @@
|
|||||||
<!-- iframe 嵌入远程页面 -->
|
<!-- iframe 嵌入远程页面 -->
|
||||||
<div class="iframe-wrapper">
|
<div class="iframe-wrapper">
|
||||||
<iframe
|
<iframe
|
||||||
ref="iframeRef"
|
|
||||||
:src="iframeSrc"
|
:src="iframeSrc"
|
||||||
class="version-iframe"
|
class="version-iframe"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
@load="onIframeLoad"
|
@load="onIframeLoad"
|
||||||
/>
|
/>
|
||||||
<div v-if="loading" class="iframe-loading">
|
<div v-if="loadFailed" class="iframe-loading">
|
||||||
<a-spin size="32" />
|
<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>
|
<p>加载中...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,33 +38,50 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { 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 BASE_URL = 'https://c.1216.top/u-desk/version.html'
|
||||||
|
const LOAD_TIMEOUT = 10000
|
||||||
|
|
||||||
// ==================== 状态 ====================
|
// ==================== 状态 ====================
|
||||||
const currentVersion = ref('')
|
const currentVersion = ref('')
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const refreshing = ref(false)
|
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(() => {
|
const iframeSrc = computed(() => {
|
||||||
if (currentVersion.value) {
|
const version = currentVersion.value ? `v=${currentVersion.value}&` : ''
|
||||||
return `${BASE_URL}?v=${currentVersion.value}&vis=u-desk`
|
return `${BASE_URL}?${version}vis=u-desk&_t=${cacheBuster.value}`
|
||||||
}
|
|
||||||
return `${BASE_URL}?vis=u-desk`
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ==================== 操作 ====================
|
// ==================== 操作 ====================
|
||||||
|
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 {
|
function handleRefresh(): void {
|
||||||
refreshing.value = true
|
refreshing.value = true
|
||||||
loading.value = true
|
loading.value = true
|
||||||
if (iframeRef.value) {
|
// 更新 cache-buster 触发 iframe 重载(赋相同值不会重新加载)
|
||||||
iframeRef.value.src = iframeSrc.value
|
cacheBuster.value = Date.now()
|
||||||
}
|
startLoadTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenExternal(): void {
|
function handleOpenExternal(): void {
|
||||||
@@ -66,21 +89,28 @@ function handleOpenExternal(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onIframeLoad(): void {
|
function onIframeLoad(): void {
|
||||||
|
clearLoadTimer()
|
||||||
loading.value = false
|
loading.value = false
|
||||||
refreshing.value = false
|
refreshing.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 生命周期 ====================
|
// ==================== 生命周期 ====================
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
startLoadTimer()
|
||||||
try {
|
try {
|
||||||
const result = await window.go?.main?.App?.GetCurrentVersion?.()
|
const result = await GetCurrentVersion()
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
currentVersion.value = result.data?.version || ''
|
currentVersion.value = result.data?.version || ''
|
||||||
|
startLoadTimer()
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Wails 未就绪时忽略
|
// 后端未就绪时忽略,不带版本参数加载
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearLoadTimer()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
appConfig.value = {
|
appConfig.value = {
|
||||||
tabs: mergedTabs.map(tab => ({ ...tab, visible: mergedVisible.includes(tab.key) })),
|
tabs: mergedTabs.map(tab => ({ ...tab, visible: mergedVisible.includes(tab.key) })),
|
||||||
visibleTabs: mergedVisible,
|
visibleTabs: mergedVisible,
|
||||||
defaultTab: defaultTab || 'file-system',
|
defaultTab: allKeys.includes(defaultTab) ? defaultTab : 'file-system',
|
||||||
sidebarSections: Array.isArray(sidebarSections)
|
sidebarSections: Array.isArray(sidebarSections)
|
||||||
? sidebarSections.filter((s: string) => validSections.includes(s))
|
? sidebarSections.filter((s: string) => validSections.includes(s))
|
||||||
: defaultSidebar
|
: defaultSidebar
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ import { Message } from '@arco-design/web-vue'
|
|||||||
import { formatBytes as formatFileSize } from '@/utils/fileUtils'
|
import { formatBytes as formatFileSize } from '@/utils/fileUtils'
|
||||||
import { parseWailsEventData } from '@/utils/wailsEvent'
|
import { parseWailsEventData } from '@/utils/wailsEvent'
|
||||||
import {
|
import {
|
||||||
CheckUpdate, GetUpdateConfig, DownloadUpdate, InstallUpdate
|
CheckUpdate, GetUpdateConfig, DownloadUpdate, InstallUpdate, InstallUpdateWithHash
|
||||||
} from '../wailsjs/v3-bindings/u-desk/app'
|
} from '../wailsjs/v3-bindings/u-desk/app'
|
||||||
import { On, Off } from '@wailsio/events'
|
import { On, Off } from '@wailsio/events'
|
||||||
|
|
||||||
|
// 本次应用运行内已自动弹过的版本(升级后自动恢复提醒)
|
||||||
|
let sessionNotifiedVersion = ''
|
||||||
|
// 下载完成事件携带的远端声明哈希,安装时用于完整性校验
|
||||||
|
let lastExpectedSha256 = ''
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新管理 Store
|
* 更新管理 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 silent 是否静默模式(不显示消息)
|
||||||
|
* @param manual 是否手动触发(跳过 auto_check_enabled 门禁)
|
||||||
|
* @returns 检查是否实际完成(false=后端未就绪或网络失败,调用方可重试)
|
||||||
*/
|
*/
|
||||||
const checkForUpdates = async (silent = false) => {
|
const checkForUpdates = async (silent = false, manual = false): Promise<boolean> => {
|
||||||
if (checking.value) return
|
if (checking.value) return true
|
||||||
|
|
||||||
checking.value = true
|
checking.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const configResult = await GetUpdateConfig()
|
const configResult = await GetUpdateConfig()
|
||||||
if (!configResult.success) return
|
if (!configResult.success) return false
|
||||||
|
|
||||||
const { auto_check_enabled } = configResult.data || {}
|
const { auto_check_enabled } = configResult.data || {}
|
||||||
if (!auto_check_enabled) return
|
if (!manual && !auto_check_enabled) return true
|
||||||
|
|
||||||
const result = await CheckUpdate()
|
const result = await CheckUpdate()
|
||||||
if (result.success && result.data?.has_update) {
|
if (result.success && result.data?.has_update) {
|
||||||
updateInfo.value = result.data
|
tryShowUpdate(result.data, !silent)
|
||||||
showUpdate.value = true
|
|
||||||
|
|
||||||
// 系统通知:发现新版本
|
|
||||||
try {
|
|
||||||
window.runtime?.SendNotification?.({
|
|
||||||
title: 'U-Desk',
|
|
||||||
body: `发现新版本 ${result.data.latest_version},点击查看`
|
|
||||||
})
|
|
||||||
} catch { /* 通知不可用时忽略 */ }
|
|
||||||
|
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
Message.success('发现新版本!')
|
Message.success('发现新版本!')
|
||||||
@@ -76,11 +112,13 @@ export const useUpdateStore = defineStore('update', () => {
|
|||||||
} else if (!silent) {
|
} else if (!silent) {
|
||||||
Message.success('已是最新版本')
|
Message.success('已是最新版本')
|
||||||
}
|
}
|
||||||
|
return result.success === true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
console.error('检查更新失败:', error)
|
console.error('检查更新失败:', error)
|
||||||
Message.error('检查更新失败:' + (error as Error).message)
|
Message.error('检查更新失败:' + (error as Error).message)
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
checking.value = false
|
checking.value = false
|
||||||
}
|
}
|
||||||
@@ -134,7 +172,10 @@ export const useUpdateStore = defineStore('update', () => {
|
|||||||
installing.value = true
|
installing.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await InstallUpdate(filePath, true)
|
// 优先带哈希校验安装(期望哈希来自下载完成事件的远端声明)
|
||||||
|
const result = lastExpectedSha256
|
||||||
|
? await InstallUpdateWithHash(filePath, true, lastExpectedSha256, 'sha256')
|
||||||
|
: await InstallUpdate(filePath, true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
Message.success({
|
Message.success({
|
||||||
content: '安装成功!应用将在几秒后重启...',
|
content: '安装成功!应用将在几秒后重启...',
|
||||||
@@ -200,6 +241,8 @@ export const useUpdateStore = defineStore('update', () => {
|
|||||||
// 完成下载
|
// 完成下载
|
||||||
downloadProgress.value = 100
|
downloadProgress.value = 100
|
||||||
downloadStatus.value = 'success'
|
downloadStatus.value = 'success'
|
||||||
|
// 记录远端声明的期望哈希,安装时用于校验
|
||||||
|
lastExpectedSha256 = String(data.expected_sha256 || '')
|
||||||
|
|
||||||
const fileSize = (data.file_size as number) || 0
|
const fileSize = (data.file_size as number) || 0
|
||||||
|
|
||||||
@@ -227,12 +270,24 @@ export const useUpdateStore = defineStore('update', () => {
|
|||||||
}, remainingTime)
|
}, remainingTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后端广播"发现新版本"处理(启动即查与周期检查命中时后端都会发)
|
||||||
|
*/
|
||||||
|
const onUpdateAvailable = (event: unknown) => {
|
||||||
|
const data = parseWailsEventData(event) as UpdateInfo
|
||||||
|
if (!data?.latest_version) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tryShowUpdate(data)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置事件监听
|
* 设置事件监听
|
||||||
*/
|
*/
|
||||||
const setupEventListeners = () => {
|
const setupEventListeners = () => {
|
||||||
On('download-progress', onDownloadProgress)
|
On('download-progress', onDownloadProgress)
|
||||||
On('download-complete', onDownloadComplete)
|
On('download-complete', onDownloadComplete)
|
||||||
|
On('update-available', onUpdateAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -241,6 +296,7 @@ export const useUpdateStore = defineStore('update', () => {
|
|||||||
const removeEventListeners = () => {
|
const removeEventListeners = () => {
|
||||||
Off('download-progress')
|
Off('download-progress')
|
||||||
Off('download-complete')
|
Off('download-complete')
|
||||||
|
Off('update-available')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 返回 ====================
|
// ==================== 返回 ====================
|
||||||
@@ -259,6 +315,7 @@ export const useUpdateStore = defineStore('update', () => {
|
|||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
downloadUpdate,
|
downloadUpdate,
|
||||||
installUpdate,
|
installUpdate,
|
||||||
|
skipVersion,
|
||||||
setupEventListeners,
|
setupEventListeners,
|
||||||
removeEventListeners,
|
removeEventListeners,
|
||||||
formatFileSize,
|
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";
|
import * as application$0 from "../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore: Unused imports
|
// @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";
|
import * as filesystem$0 from "./internal/filesystem/models.js";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
@@ -123,6 +126,25 @@ export function EmptyRecycleBin(): $CancellablePromise<void> {
|
|||||||
return $Call.ByID(4176312624);
|
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文件
|
* 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 获取当前版本号
|
* GetCurrentVersion 获取当前版本号
|
||||||
*/
|
*/
|
||||||
@@ -335,7 +348,7 @@ export function LoadConnectionProfiles(): $CancellablePromise<{ [_ in string]?:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MovePath 移动文件或目录(同盘重命名,跨盘复制后删除)
|
* MovePath 移动文件或目录(同盘重命名,跨盘复制后删除),跨盘时经 transfer-progress 事件推送进度
|
||||||
*/
|
*/
|
||||||
export function MovePath(src: string, dst: string): $CancellablePromise<void> {
|
export function MovePath(src: string, dst: string): $CancellablePromise<void> {
|
||||||
return $Call.ByID(2028899675, src, dst);
|
return $Call.ByID(2028899675, src, dst);
|
||||||
@@ -348,6 +361,9 @@ export function OpenPath(path: string): $CancellablePromise<void> {
|
|||||||
return $Call.ByID(1591734570, path);
|
return $Call.ByID(1591734570, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OssConnect 连接 OSS 存储
|
||||||
|
*/
|
||||||
export function OssConnect(req: $models.OssConnectRequest): $CancellablePromise<string> {
|
export function OssConnect(req: $models.OssConnectRequest): $CancellablePromise<string> {
|
||||||
return $Call.ByID(3667022538, req);
|
return $Call.ByID(3667022538, req);
|
||||||
}
|
}
|
||||||
@@ -478,6 +494,16 @@ export function PickDirectory(): $CancellablePromise<string> {
|
|||||||
return $Call.ByID(1347829059);
|
return $Call.ByID(1347829059);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ReadClipboardFiles 读取系统剪贴板中的文件/文件夹路径列表。
|
||||||
|
* 剪贴板为文本/截图等非文件内容时返回空列表,前端据此回落图片粘贴等原有链路。
|
||||||
|
*/
|
||||||
|
export function ReadClipboardFiles(): $CancellablePromise<string[]> {
|
||||||
|
return $Call.ByID(2636780132).then(($result: any) => {
|
||||||
|
return $$createType7($result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ReadFile 读取文件
|
* ReadFile 读取文件
|
||||||
*/
|
*/
|
||||||
@@ -526,8 +552,9 @@ export function RestoreFromRecycleBin(recyclePath: string): $CancellablePromise<
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* SaveAppConfig 保存应用配置
|
* 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 $Call.ByID(1942219977, req).then(($result: any) => {
|
||||||
return $$createType2($result);
|
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 在创建窗口后调用)
|
* SetMainWindow 设置主窗口引用(由 main.go 在创建窗口后调用)
|
||||||
*/
|
*/
|
||||||
@@ -624,6 +644,13 @@ export function SftpDownloadSiteForPreview(connID: string, remotePath: string):
|
|||||||
return $Call.ByID(1591575570, connID, remotePath);
|
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 下载远程文件到本地临时目录(用于预览)
|
* SftpDownloadToTemp 下载远程文件到本地临时目录(用于预览)
|
||||||
*/
|
*/
|
||||||
@@ -638,20 +665,6 @@ export function SftpDownloadToTempCached(connID: string, remotePath: string, fil
|
|||||||
return $Call.ByID(3935472409, connID, remotePath, fileSize, modTime);
|
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 远程主机常用路径
|
* 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 编码的二进制文件(粘贴图片等)
|
* SftpWriteBase64File SFTP 写入 base64 编码的二进制文件(粘贴图片等)
|
||||||
*/
|
*/
|
||||||
@@ -791,3 +811,4 @@ const $$createType3 = filesystem$0.FileOperationResult.createFrom;
|
|||||||
const $$createType4 = $Create.Nullable($$createType3);
|
const $$createType4 = $Create.Nullable($$createType3);
|
||||||
const $$createType5 = $Create.Array($$createType2);
|
const $$createType5 = $Create.Array($$createType2);
|
||||||
const $$createType6 = $Create.Map($Create.Any, $Create.Any);
|
const $$createType6 = $Create.Map($Create.Any, $Create.Any);
|
||||||
|
const $$createType7 = $Create.Array($Create.Any);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export {
|
|||||||
OssConnectRequest,
|
OssConnectRequest,
|
||||||
OssRenamePathRequest,
|
OssRenamePathRequest,
|
||||||
RenamePathRequest,
|
RenamePathRequest,
|
||||||
SaveAppConfigRequest,
|
|
||||||
SaveBase64FileRequest,
|
SaveBase64FileRequest,
|
||||||
SaveProfileRequest,
|
SaveProfileRequest,
|
||||||
SftpConnectRequest,
|
SftpConnectRequest,
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AppTabDefinition
|
AppTabDefinition,
|
||||||
|
SaveAppConfigRequest
|
||||||
} from "./models.js";
|
} from "./models.js";
|
||||||
|
|||||||
@@ -40,3 +40,56 @@ export class AppTabDefinition {
|
|||||||
return new AppTabDefinition($$parsedSource as Partial<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
|
// @ts-ignore: Unused imports
|
||||||
import { Create as $Create } from "@wailsio/runtime";
|
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 播放列表条目
|
* 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 编码的二进制文件
|
* SaveBase64FileRequest 保存 Base64 编码的二进制文件
|
||||||
*/
|
*/
|
||||||
@@ -422,8 +378,3 @@ export class WriteFileRequest {
|
|||||||
return new WriteFileRequest($$parsedSource as Partial<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);
|
|
||||||
|
|||||||
+20
-15
@@ -24,9 +24,10 @@ func NewConfigAPI() (*ConfigAPI, error) {
|
|||||||
|
|
||||||
// GetAppConfigResponse 获取应用配置响应
|
// GetAppConfigResponse 获取应用配置响应
|
||||||
type GetAppConfigResponse struct {
|
type GetAppConfigResponse struct {
|
||||||
Tabs []AppTabDefinition `json:"tabs"`
|
Tabs []AppTabDefinition `json:"tabs"`
|
||||||
VisibleTabs []string `json:"visibleTabs"`
|
VisibleTabs []string `json:"visibleTabs"`
|
||||||
DefaultTab string `json:"defaultTab"`
|
DefaultTab string `json:"defaultTab"`
|
||||||
|
SidebarSections []string `json:"sidebarSections"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppTabDefinition 应用 Tab 定义(前端格式)
|
// AppTabDefinition 应用 Tab 定义(前端格式)
|
||||||
@@ -39,9 +40,10 @@ type AppTabDefinition struct {
|
|||||||
|
|
||||||
// SaveAppConfigRequest 保存应用配置请求(前端格式)
|
// SaveAppConfigRequest 保存应用配置请求(前端格式)
|
||||||
type SaveAppConfigRequest struct {
|
type SaveAppConfigRequest struct {
|
||||||
Tabs []AppTabDefinition `json:"tabs"`
|
Tabs []AppTabDefinition `json:"tabs"`
|
||||||
VisibleTabs []string `json:"visibleTabs"`
|
VisibleTabs []string `json:"visibleTabs"`
|
||||||
DefaultTab string `json:"defaultTab"`
|
DefaultTab string `json:"defaultTab"`
|
||||||
|
SidebarSections []string `json:"sidebarSections"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAppConfig 获取应用配置
|
// GetAppConfig 获取应用配置
|
||||||
@@ -73,9 +75,10 @@ func (api *ConfigAPI) GetAppConfig() (map[string]interface{}, error) {
|
|||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": GetAppConfigResponse{
|
"data": GetAppConfigResponse{
|
||||||
Tabs: tabs,
|
Tabs: tabs,
|
||||||
VisibleTabs: tabConfig.VisibleTabs,
|
VisibleTabs: tabConfig.VisibleTabs,
|
||||||
DefaultTab: tabConfig.DefaultTab,
|
DefaultTab: tabConfig.DefaultTab,
|
||||||
|
SidebarSections: tabConfig.SidebarSections,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -116,9 +119,10 @@ func (api *ConfigAPI) SaveAppConfig(req SaveAppConfigRequest) (map[string]interf
|
|||||||
}
|
}
|
||||||
|
|
||||||
tabConfig := &service.TabConfig{
|
tabConfig := &service.TabConfig{
|
||||||
AvailableTabs: availableTabs,
|
AvailableTabs: availableTabs,
|
||||||
VisibleTabs: req.VisibleTabs,
|
VisibleTabs: req.VisibleTabs,
|
||||||
DefaultTab: req.DefaultTab,
|
DefaultTab: req.DefaultTab,
|
||||||
|
SidebarSections: req.SidebarSections,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
@@ -201,8 +205,9 @@ func (api *ConfigAPI) MigrateTabConfig() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return api.configService.SaveTabConfig(&service.TabConfig{
|
return api.configService.SaveTabConfig(&service.TabConfig{
|
||||||
AvailableTabs: newTabs,
|
AvailableTabs: newTabs,
|
||||||
VisibleTabs: newVisible,
|
VisibleTabs: newVisible,
|
||||||
DefaultTab: defaultTab,
|
DefaultTab: defaultTab,
|
||||||
|
SidebarSections: config.SidebarSections,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,20 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"u-desk/internal/service"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"u-desk/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateAPI 版本更新 API
|
// UpdateAPI 版本更新 API
|
||||||
type UpdateAPI struct {
|
type UpdateAPI struct {
|
||||||
updateService *service.UpdateService
|
updateService *service.UpdateService
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
eventEmitter func(name string, data ...any)
|
eventEmitter func(name string, data ...any)
|
||||||
|
configChanged func() // 更新配置变更回调(重置自动检查定时器)
|
||||||
|
expectedSHA256 string // 最近一次检查得到的远端包 SHA256
|
||||||
|
downloading atomic.Bool // 下载进行中标志(并发互斥)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUpdateAPI 创建版本更新 API
|
// NewUpdateAPI 创建版本更新 API
|
||||||
@@ -25,6 +30,11 @@ func (api *UpdateAPI) SetEventEmitter(emitter func(name string, data ...any)) {
|
|||||||
api.eventEmitter = emitter
|
api.eventEmitter = emitter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetConfigChangedCallback 设置配置变更回调(由 App 注入,用于重置自动检查定时器)
|
||||||
|
func (api *UpdateAPI) SetConfigChangedCallback(cb func()) {
|
||||||
|
api.configChanged = cb
|
||||||
|
}
|
||||||
|
|
||||||
// SetContext 设置上下文(用于事件推送)
|
// SetContext 设置上下文(用于事件推送)
|
||||||
func (api *UpdateAPI) SetContext(ctx context.Context) {
|
func (api *UpdateAPI) SetContext(ctx context.Context) {
|
||||||
api.ctx = ctx
|
api.ctx = ctx
|
||||||
@@ -54,6 +64,9 @@ func (api *UpdateAPI) CheckUpdate() (map[string]interface{}, error) {
|
|||||||
return errorResponse(err.Error()), nil
|
return errorResponse(err.Error()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录远端包哈希,供下载完成事件比对
|
||||||
|
api.expectedSHA256 = result.SHA256
|
||||||
|
|
||||||
return successResponse(result), nil
|
return successResponse(result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +112,11 @@ func (api *UpdateAPI) SetUpdateConfig(autoCheckEnabled bool, checkIntervalMinute
|
|||||||
return errorResponse(err.Error()), nil
|
return errorResponse(err.Error()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 配置已变更,通知重置自动检查定时器(免重启生效)
|
||||||
|
if api.configChanged != nil {
|
||||||
|
api.configChanged()
|
||||||
|
}
|
||||||
|
|
||||||
return successResponse(map[string]interface{}{
|
return successResponse(map[string]interface{}{
|
||||||
"message": "配置保存成功",
|
"message": "配置保存成功",
|
||||||
}), nil
|
}), nil
|
||||||
@@ -110,7 +128,14 @@ func (api *UpdateAPI) DownloadUpdate(downloadURL string) (map[string]interface{}
|
|||||||
return errorResponse("下载地址不能为空"), nil
|
return errorResponse("下载地址不能为空"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 并发互斥:已有下载任务进行中则拒绝
|
||||||
|
if !api.downloading.CompareAndSwap(false, true) {
|
||||||
|
return errorResponse("已有下载任务进行中"), nil
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
defer api.downloading.Store(false)
|
||||||
|
|
||||||
progressCallback := func(progress float64, speed float64, downloaded int64, total int64) {
|
progressCallback := func(progress float64, speed float64, downloaded int64, total int64) {
|
||||||
progressInfo := map[string]interface{}{
|
progressInfo := map[string]interface{}{
|
||||||
"progress": progress,
|
"progress": progress,
|
||||||
@@ -129,9 +154,11 @@ func (api *UpdateAPI) DownloadUpdate(downloadURL string) (map[string]interface{}
|
|||||||
api.emit("download-complete", errorInfo)
|
api.emit("download-complete", errorInfo)
|
||||||
} else {
|
} else {
|
||||||
resultInfo := map[string]interface{}{
|
resultInfo := map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
"file_path": result.FilePath,
|
"file_path": result.FilePath,
|
||||||
"file_size": result.FileSize,
|
"file_size": result.FileSize,
|
||||||
|
"sha256": result.SHA256Hash,
|
||||||
|
"expected_sha256": api.expectedSHA256,
|
||||||
}
|
}
|
||||||
api.emit("download-complete", resultInfo)
|
api.emit("download-complete", resultInfo)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,15 @@ type TabDefinition struct {
|
|||||||
|
|
||||||
// TabConfig Tab 配置
|
// TabConfig Tab 配置
|
||||||
type TabConfig struct {
|
type TabConfig struct {
|
||||||
AvailableTabs []TabDefinition `json:"available_tabs"`
|
AvailableTabs []TabDefinition `json:"available_tabs"`
|
||||||
VisibleTabs []string `json:"visible_tabs"`
|
VisibleTabs []string `json:"visible_tabs"`
|
||||||
DefaultTab string `json:"default_tab"`
|
DefaultTab string `json:"default_tab"`
|
||||||
|
SidebarSections []string `json:"sidebar_sections"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 默认侧边栏分区
|
||||||
|
var defaultSidebarSections = []string{"server", "favorites", "help"}
|
||||||
|
|
||||||
// 默认 Tab 配置
|
// 默认 Tab 配置
|
||||||
var defaultTabConfig = TabConfig{
|
var defaultTabConfig = TabConfig{
|
||||||
AvailableTabs: []TabDefinition{
|
AvailableTabs: []TabDefinition{
|
||||||
@@ -45,8 +49,9 @@ var defaultTabConfig = TabConfig{
|
|||||||
{Key: "markdown-editor", Title: "Markdown", Enabled: true},
|
{Key: "markdown-editor", Title: "Markdown", Enabled: true},
|
||||||
{Key: "version", Title: "版本历史", Enabled: true},
|
{Key: "version", Title: "版本历史", Enabled: true},
|
||||||
},
|
},
|
||||||
VisibleTabs: []string{"file-system", "markdown-editor", "version"},
|
VisibleTabs: []string{"file-system", "markdown-editor", "version"},
|
||||||
DefaultTab: "file-system",
|
DefaultTab: "file-system",
|
||||||
|
SidebarSections: defaultSidebarSections,
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -79,6 +84,11 @@ func (s *ConfigService) GetTabConfig() (*TabConfig, error) {
|
|||||||
return &defaultTabConfig, nil
|
return &defaultTabConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 侧边栏分区为空时回退默认值(兼容旧配置)
|
||||||
|
if len(tabConfig.SidebarSections) == 0 {
|
||||||
|
tabConfig.SidebarSections = defaultSidebarSections
|
||||||
|
}
|
||||||
|
|
||||||
return &tabConfig, nil
|
return &tabConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type RemoteVersionInfo struct {
|
|||||||
ForceUpdate bool `json:"force_update"`
|
ForceUpdate bool `json:"force_update"`
|
||||||
ReleaseDate string `json:"release_date"`
|
ReleaseDate string `json:"release_date"`
|
||||||
FileSize int64 `json:"file_size"`
|
FileSize int64 `json:"file_size"`
|
||||||
|
SHA256 string `json:"sha256"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCheckResult 更新检查结果
|
// UpdateCheckResult 更新检查结果
|
||||||
@@ -42,6 +43,7 @@ type UpdateCheckResult struct {
|
|||||||
ForceUpdate bool `json:"force_update"`
|
ForceUpdate bool `json:"force_update"`
|
||||||
ReleaseDate string `json:"release_date"`
|
ReleaseDate string `json:"release_date"`
|
||||||
FileSize int64 `json:"file_size"`
|
FileSize int64 `json:"file_size"`
|
||||||
|
SHA256 string `json:"sha256"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallResult 安装结果
|
// InstallResult 安装结果
|
||||||
@@ -99,6 +101,7 @@ func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) {
|
|||||||
ForceUpdate: remoteInfo.ForceUpdate,
|
ForceUpdate: remoteInfo.ForceUpdate,
|
||||||
ReleaseDate: remoteInfo.ReleaseDate,
|
ReleaseDate: remoteInfo.ReleaseDate,
|
||||||
FileSize: remoteInfo.FileSize,
|
FileSize: remoteInfo.FileSize,
|
||||||
|
SHA256: remoteInfo.SHA256,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"u-desk/internal/common"
|
"u-desk/internal/common"
|
||||||
@@ -41,9 +42,13 @@ func DownloadUpdate(downloadURL string, progressCallback DownloadProgress) (*Dow
|
|||||||
return nil, fmt.Errorf("创建下载目录失败: %v", err)
|
return nil, fmt.Errorf("创建下载目录失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 URL 提取文件名
|
// 从 URL 提取文件名(去除 query 参数与 # 片段)
|
||||||
filename := filepath.Base(downloadURL)
|
cleanURL := downloadURL
|
||||||
if filename == "" || filename == "." {
|
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())
|
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)
|
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
|
contentLength := resp.ContentLength
|
||||||
gotTotalFromRange := false
|
gotTotalFromRange := false
|
||||||
|
|||||||
Reference in New Issue
Block a user