diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9908c888..618c0aa7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -35,6 +35,7 @@ function App() { const effectiveBlur = normalizeBlurForPlatform(appearance.blur); const blurFilter = blurToFilter(effectiveBlur); const windowCornerRadius = 14; + const [runtimePlatform, setRuntimePlatform] = useState(''); const [isLinuxRuntime, setIsLinuxRuntime] = useState(false); const [isStoreHydrated, setIsStoreHydrated] = useState(() => useStore.persist.hasHydrated()); const globalProxyInvalidHintShownRef = React.useRef(false); @@ -50,12 +51,18 @@ function App() { Environment() .then((env) => { if (cancelled) return; - setIsLinuxRuntime((env?.platform || '').toLowerCase() === 'linux'); + const platform = String(env?.platform || '').toLowerCase(); + setRuntimePlatform(platform); + setIsLinuxRuntime(platform === 'linux'); }) .catch(() => { if (cancelled) return; const platform = typeof navigator !== 'undefined' ? navigator.platform : ''; - setIsLinuxRuntime(/linux/i.test(platform)); + const normalized = /linux/i.test(platform) + ? 'linux' + : (/mac/i.test(platform) ? 'darwin' : (/win/i.test(platform) ? 'windows' : '')); + setRuntimePlatform(normalized); + setIsLinuxRuntime(normalized === 'linux'); }); return () => { cancelled = true; @@ -241,11 +248,12 @@ function App() { const updateCheckInFlightRef = React.useRef(false); const updateDownloadInFlightRef = React.useRef(false); const updateDownloadedVersionRef = React.useRef(null); + const updateInstallTriggeredVersionRef = React.useRef(null); const updateDownloadMetaRef = React.useRef(null); - const updateDeferredVersionRef = React.useRef(null); const updateNotifiedVersionRef = React.useRef(null); const updateMutedVersionRef = React.useRef(null); const [isAboutOpen, setIsAboutOpen] = useState(false); + const isAboutOpenRef = React.useRef(false); const [aboutLoading, setAboutLoading] = useState(false); const [aboutInfo, setAboutInfo] = useState<{ version: string; author: string; buildTime?: string; repoUrl?: string; issueUrl?: string; releaseUrl?: string } | null>(null); const [aboutUpdateStatus, setAboutUpdateStatus] = useState(''); @@ -299,6 +307,9 @@ function App() { autoRelaunch?: boolean; }; + const isMacRuntime = runtimePlatform === 'darwin' + || (runtimePlatform === '' && typeof navigator !== 'undefined' && /mac/i.test(navigator.platform)); + const formatBytes = (bytes?: number) => { if (!bytes || bytes <= 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; @@ -311,52 +322,18 @@ function App() { return `${value.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`; }; - const promptRestartForUpdate = (info: UpdateInfo, resultData?: UpdateDownloadResultData) => { - const downloadPathHint = resultData?.downloadPath - ? `更新包路径:${resultData.downloadPath}` - : ''; - const installLogHint = resultData?.installLogPath - ? `安装日志:${resultData.installLogPath}` - : ''; - Modal.confirm({ - title: '更新已下载', - content: ( -
-
{`版本 ${info.latestVersion} 已下载完成,是否现在重启完成更新?`}
- {downloadPathHint ?
{downloadPathHint}
: null} - {installLogHint ?
{installLogHint}
: null} -
- ), - okText: '立即重启', - cancelText: '稍后', - onOk: async () => { - updateDeferredVersionRef.current = null; - const res = await (window as any).go.app.App.InstallUpdateAndRestart(); - if (!res?.success) { - message.error('更新安装失败: ' + (res?.message || '未知错误')); - } - }, - onCancel: () => { - updateDeferredVersionRef.current = info.latestVersion; - } - }); - }; - const downloadUpdate = React.useCallback(async (info: UpdateInfo, silent: boolean) => { if (updateDownloadInFlightRef.current) return; if (updateDownloadedVersionRef.current === info.latestVersion) { if (!silent) { const cachedDownloadPath = updateDownloadMetaRef.current?.downloadPath; message.info(cachedDownloadPath ? `更新包已就绪(${info.latestVersion}),路径:${cachedDownloadPath}` : `更新包已就绪(${info.latestVersion})`); - } - if (!silent || updateDeferredVersionRef.current !== info.latestVersion) { - promptRestartForUpdate(info, updateDownloadMetaRef.current || undefined); + showUpdateDownloadProgress(); } return; } updateDownloadInFlightRef.current = true; updateDownloadMetaRef.current = null; - const key = 'update-download'; setUpdateDownloadProgress({ open: true, version: info.latestVersion, @@ -366,33 +343,98 @@ function App() { total: info.assetSize || 0, message: '' }); - message.loading({ content: `正在下载更新 ${info.latestVersion}...`, key, duration: 0 }); const res = await (window as any).go.app.App.DownloadUpdate(); updateDownloadInFlightRef.current = false; if (res?.success) { const resultData = (res?.data || {}) as UpdateDownloadResultData; updateDownloadMetaRef.current = resultData; updateDownloadedVersionRef.current = info.latestVersion; - setUpdateDownloadProgress(prev => ({ ...prev, status: 'done', percent: 100, open: false })); + setUpdateDownloadProgress(prev => { + const total = prev.total > 0 ? prev.total : (info.assetSize || 0); + return { ...prev, status: 'done', percent: 100, downloaded: total, total, message: '', open: false }; + }); + setLastUpdateInfo((prev) => { + if (!prev || prev.latestVersion !== info.latestVersion) { + return { + ...info, + downloaded: true, + downloadPath: resultData?.downloadPath || info.downloadPath, + }; + } + return { + ...prev, + downloaded: true, + downloadPath: resultData?.downloadPath || prev.downloadPath || info.downloadPath, + }; + }); if (resultData?.downloadPath) { - message.success({ content: `更新下载完成,更新包路径:${resultData.downloadPath}`, key, duration: 5 }); + message.success({ content: `更新下载完成,更新包路径:${resultData.downloadPath}`, duration: 5 }); } else { - message.success({ content: '更新下载完成', key, duration: 2 }); - } - setAboutUpdateStatus(`发现新版本 ${info.latestVersion}(已下载,待重启安装)`); - if (!silent || updateDeferredVersionRef.current !== info.latestVersion) { - promptRestartForUpdate(info, resultData); + message.success({ content: '更新下载完成', duration: 2 }); } + setAboutUpdateStatus(`发现新版本 ${info.latestVersion}(已下载,请点击“下载进度”后安装)`); } else { setUpdateDownloadProgress(prev => ({ ...prev, status: 'error', message: res?.message || '未知错误' })); - message.error({ content: '更新下载失败: ' + (res?.message || '未知错误'), key, duration: 4 }); + message.error({ content: '更新下载失败: ' + (res?.message || '未知错误'), duration: 4 }); } }, []); + const showUpdateDownloadProgress = React.useCallback(() => { + setUpdateDownloadProgress((prev) => { + if (prev.status === 'idle') return prev; + return { ...prev, open: true }; + }); + }, []); + + const hideUpdateDownloadProgress = React.useCallback(() => { + setUpdateDownloadProgress((prev) => ({ ...prev, open: false })); + }, []); + + const hasUpdateDownloadProgress = updateDownloadProgress.status === 'start' + || updateDownloadProgress.status === 'downloading' + || updateDownloadProgress.status === 'done' + || updateDownloadProgress.status === 'error'; + const isLatestUpdateDownloaded = Boolean(lastUpdateInfo?.hasUpdate) && ( + Boolean(lastUpdateInfo?.downloaded) + || (Boolean(lastUpdateInfo?.latestVersion) && updateDownloadedVersionRef.current === lastUpdateInfo?.latestVersion) + ); + const isBackgroundProgressForLatestUpdate = Boolean(lastUpdateInfo?.hasUpdate) + && Boolean(lastUpdateInfo?.latestVersion) + && updateDownloadProgress.version === lastUpdateInfo?.latestVersion + && (updateDownloadProgress.status === 'start' + || updateDownloadProgress.status === 'downloading' + || updateDownloadProgress.status === 'error'); + const canShowProgressEntry = (isLatestUpdateDownloaded || isBackgroundProgressForLatestUpdate) + && updateInstallTriggeredVersionRef.current !== (lastUpdateInfo?.latestVersion || null); + + const handleInstallFromProgress = React.useCallback(async () => { + if (updateDownloadProgress.status !== 'done') { + return; + } + if (isMacRuntime) { + const res = await (window as any).go.app.App.OpenDownloadedUpdateDirectory(); + if (!res?.success) { + message.error('打开安装目录失败: ' + (res?.message || '未知错误')); + return; + } + updateInstallTriggeredVersionRef.current = updateDownloadProgress.version || lastUpdateInfo?.latestVersion || null; + hideUpdateDownloadProgress(); + message.success(res?.message || '已打开安装目录,请手动完成替换'); + return; + } + const res = await (window as any).go.app.App.InstallUpdateAndRestart(); + if (!res?.success) { + message.error('更新安装失败: ' + (res?.message || '未知错误')); + return; + } + updateInstallTriggeredVersionRef.current = updateDownloadProgress.version || lastUpdateInfo?.latestVersion || null; + hideUpdateDownloadProgress(); + }, [hideUpdateDownloadProgress, isMacRuntime, lastUpdateInfo?.latestVersion, updateDownloadProgress.status, updateDownloadProgress.version]); + const checkForUpdates = React.useCallback(async (silent: boolean) => { if (updateCheckInFlightRef.current) return; updateCheckInFlightRef.current = true; @@ -410,7 +452,7 @@ function App() { } const info: UpdateInfo = res.data; if (!info) return; - setLastUpdateInfo(info); + const aboutOpen = isAboutOpenRef.current; if (info.hasUpdate) { const localDownloaded = updateDownloadedVersionRef.current === info.latestVersion; const hasDownloaded = Boolean(info.downloaded) || localDownloaded; @@ -422,34 +464,103 @@ function App() { info, downloadPath: downloadPath || undefined, }; + setUpdateDownloadProgress((prev) => { + if (prev.status === 'start' || prev.status === 'downloading') { + return prev; + } + const total = info.assetSize || prev.total || 0; + return { + ...prev, + open: prev.open && prev.version === info.latestVersion, + version: info.latestVersion, + status: 'done', + percent: 100, + downloaded: total, + total, + message: '', + }; + }); + setLastUpdateInfo({ + ...info, + downloaded: true, + downloadPath: downloadPath || undefined, + }); } else { if (updateDownloadedVersionRef.current !== info.latestVersion) { updateDownloadMetaRef.current = null; } + setUpdateDownloadProgress((prev) => { + if (prev.status === 'start' || prev.status === 'downloading') { + return prev; + } + return { + ...prev, + open: false, + version: info.latestVersion, + status: 'idle', + percent: 0, + downloaded: 0, + total: info.assetSize || 0, + message: '', + }; + }); + setLastUpdateInfo(info); } const statusText = hasDownloaded - ? `发现新版本 ${info.latestVersion}(已下载,待重启安装)` + ? `发现新版本 ${info.latestVersion}(已下载,请点击“下载进度”后安装)` : `发现新版本 ${info.latestVersion}(未下载)`; if (!silent) { message.info(`发现新版本 ${info.latestVersion}`); setAboutUpdateStatus(statusText); } - if (silent && isAboutOpen) { + if (silent && aboutOpen) { setAboutUpdateStatus(statusText); } - if (silent && !isAboutOpen && updateMutedVersionRef.current !== info.latestVersion && updateNotifiedVersionRef.current !== info.latestVersion) { + if (silent && !aboutOpen && updateMutedVersionRef.current !== info.latestVersion && updateNotifiedVersionRef.current !== info.latestVersion) { updateNotifiedVersionRef.current = info.latestVersion; setIsAboutOpen(true); } } else if (!silent) { + setUpdateDownloadProgress((prev) => { + if (prev.status === 'start' || prev.status === 'downloading') { + return prev; + } + return { + open: false, + version: '', + status: 'idle', + percent: 0, + downloaded: 0, + total: 0, + message: '', + }; + }); + setLastUpdateInfo(info); const text = `当前已是最新版本(${info.currentVersion || '未知'})`; message.success(text); setAboutUpdateStatus(text); - } else if (silent && isAboutOpen) { + } else if (silent && aboutOpen) { + setUpdateDownloadProgress((prev) => { + if (prev.status === 'start' || prev.status === 'downloading') { + return prev; + } + return { + open: false, + version: '', + status: 'idle', + percent: 0, + downloaded: 0, + total: 0, + message: '', + }; + }); + setLastUpdateInfo(info); const text = `当前已是最新版本(${info.currentVersion || '未知'})`; setAboutUpdateStatus(text); + } else { + setLastUpdateInfo(info); } - }, [downloadUpdate]); + }, []); const loadAboutInfo = React.useCallback(async () => { setAboutLoading(true); @@ -717,10 +828,20 @@ function App() { document.body.setAttribute('data-theme', darkMode ? 'dark' : 'light'); }, [darkMode]); + useEffect(() => { + isAboutOpenRef.current = isAboutOpen; + }, [isAboutOpen]); + useEffect(() => { if (isAboutOpen) { if (lastUpdateInfo?.hasUpdate) { - setAboutUpdateStatus(`发现新版本 ${lastUpdateInfo.latestVersion}(未下载)`); + const localDownloaded = updateDownloadedVersionRef.current === lastUpdateInfo.latestVersion; + const hasDownloaded = Boolean(lastUpdateInfo.downloaded) || localDownloaded; + setAboutUpdateStatus( + hasDownloaded + ? `发现新版本 ${lastUpdateInfo.latestVersion}(已下载,请点击“下载进度”后安装)` + : `发现新版本 ${lastUpdateInfo.latestVersion}(未下载)` + ); } else if (lastUpdateInfo) { setAboutUpdateStatus(`当前已是最新版本(${lastUpdateInfo.currentVersion || '未知'})`); } else { @@ -758,7 +879,7 @@ function App() { : (total > 0 ? (downloaded / total) * 100 : 0); const percent = Math.max(0, Math.min(100, percentRaw)); setUpdateDownloadProgress(prev => ({ - open: nextStatus === 'start' || nextStatus === 'downloading' || nextStatus === 'error', + open: prev.open, version: prev.version, status: nextStatus, percent, @@ -1004,7 +1125,10 @@ function App() { open={isAboutOpen} onCancel={() => setIsAboutOpen(false)} footer={[ - lastUpdateInfo?.hasUpdate ? ( + canShowProgressEntry ? ( + + ) : null, + lastUpdateInfo?.hasUpdate && !isLatestUpdateDownloaded ? ( ) : null, lastUpdateInfo?.hasUpdate ? ( @@ -1188,38 +1312,25 @@ function App() { { - if (updateDownloadProgress.status === 'error') { - setUpdateDownloadProgress({ - open: false, - version: '', - status: 'idle', - percent: 0, - downloaded: 0, - total: 0, - message: '' - }); - } - }} - footer={updateDownloadProgress.status === 'error' ? [ + closable + maskClosable + keyboard + onCancel={hideUpdateDownloadProgress} + footer={updateDownloadProgress.status === 'start' || updateDownloadProgress.status === 'downloading' ? [ - ] : null} + ] : (updateDownloadProgress.status === 'done' ? [ + , + + ] : (updateDownloadProgress.status === 'error' ? [ + + ] : null))} >
; +export function OpenDownloadedUpdateDirectory():Promise; + export function OpenSQLFile():Promise; export function PreviewImportFile(arg1:string):Promise; diff --git a/frontend/wailsjs/go/app/App.js b/frontend/wailsjs/go/app/App.js index e6def2b8..0d8296c4 100755 --- a/frontend/wailsjs/go/app/App.js +++ b/frontend/wailsjs/go/app/App.js @@ -194,6 +194,10 @@ export function MySQLShowCreateTable(arg1, arg2, arg3) { return window['go']['app']['App']['MySQLShowCreateTable'](arg1, arg2, arg3); } +export function OpenDownloadedUpdateDirectory() { + return window['go']['app']['App']['OpenDownloadedUpdateDirectory'](); +} + export function OpenSQLFile() { return window['go']['app']['App']['OpenSQLFile'](); } diff --git a/internal/app/methods_update.go b/internal/app/methods_update.go index 5707b6e8..bd98ace4 100644 --- a/internal/app/methods_update.go +++ b/internal/app/methods_update.go @@ -233,6 +233,49 @@ func (a *App) InstallUpdateAndRestart() connection.QueryResult { } } +func (a *App) OpenDownloadedUpdateDirectory() connection.QueryResult { + a.updateMu.Lock() + staged := a.updateState.staged + a.updateMu.Unlock() + if staged == nil { + return connection.QueryResult{Success: false, Message: "未找到已下载的更新包"} + } + assetPath := strings.TrimSpace(staged.FilePath) + if assetPath == "" { + return connection.QueryResult{Success: false, Message: "更新包路径为空"} + } + dirPath := strings.TrimSpace(filepath.Dir(assetPath)) + if dirPath == "" || dirPath == "." { + return connection.QueryResult{Success: false, Message: "无法解析更新目录"} + } + if stat, err := os.Stat(dirPath); err != nil || !stat.IsDir() { + return connection.QueryResult{Success: false, Message: "更新目录不存在或不可访问"} + } + + var cmd *exec.Cmd + switch stdRuntime.GOOS { + case "darwin": + cmd = exec.Command("open", dirPath) + case "windows": + cmd = exec.Command("explorer", dirPath) + case "linux": + cmd = exec.Command("xdg-open", dirPath) + default: + return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前平台暂不支持打开目录:%s", stdRuntime.GOOS)} + } + if err := cmd.Start(); err != nil { + logger.Error(err, "打开更新目录失败") + return connection.QueryResult{Success: false, Message: fmt.Sprintf("打开更新目录失败:%v", err)} + } + return connection.QueryResult{ + Success: true, + Message: fmt.Sprintf("已打开安装目录:%s", dirPath), + Data: map[string]any{ + "path": dirPath, + }, + } +} + func (a *App) downloadAndStageUpdate(info UpdateInfo) connection.QueryResult { workspaceDir := strings.TrimSpace(resolveUpdateWorkspaceDir(info.LatestVersion)) if workspaceDir == "" {