feat: 优化更新提示对话框,支持文件路径复制和后台下载功能,优化安装流程

feat: #100
This commit is contained in:
alger
2025-03-31 23:01:03 +08:00
parent 7e826311fe
commit 23b2340169
4 changed files with 250 additions and 46 deletions

View File

@@ -39,7 +39,18 @@ export default {
downloadFailed: 'Download failed, please try again or download manually', downloadFailed: 'Download failed, please try again or download manually',
startFailed: 'Start download failed, please try again or download manually', startFailed: 'Start download failed, please try again or download manually',
noDownloadUrl: noDownloadUrl:
'No suitable installation package found for the current system, please download manually' 'No suitable installation package found for the current system, please download manually',
installConfirmTitle: 'Install Update',
installConfirmContent: 'Do you want to close the application and install the update?',
manualInstallTip:
'If the installer does not open automatically after closing the application, please find the file in your download folder and open it manually.',
yesInstall: 'Install Now',
noThanks: 'Later',
fileLocation: 'File Location',
copy: 'Copy Path',
copySuccess: 'Path copied to clipboard',
copyFailed: 'Copy failed',
backgroundDownload: 'Background Download'
}, },
coffee: { coffee: {
title: 'Buy me a coffee', title: 'Buy me a coffee',

View File

@@ -38,7 +38,17 @@ export default {
nowUpdate: '立即更新', nowUpdate: '立即更新',
downloadFailed: '下载失败,请重试或手动下载', downloadFailed: '下载失败,请重试或手动下载',
startFailed: '启动下载失败,请重试或手动下载', startFailed: '启动下载失败,请重试或手动下载',
noDownloadUrl: '未找到适合当前系统的安装包,请手动下载' noDownloadUrl: '未找到适合当前系统的安装包,请手动下载',
installConfirmTitle: '安装更新',
installConfirmContent: '是否关闭应用并安装更新?',
manualInstallTip: '如果关闭应用后没有正常弹出安装程序,请至下载文件夹查找文件并手动打开。',
yesInstall: '立即安装',
noThanks: '稍后安装',
fileLocation: '文件位置',
copy: '复制路径',
copySuccess: '路径已复制到剪贴板',
copyFailed: '复制失败',
backgroundDownload: '后台下载'
}, },
coffee: { coffee: {
title: '请我喝咖啡', title: '请我喝咖啡',

View File

@@ -1,5 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import { exec } from 'child_process'; import { spawn } from 'child_process';
import { app, BrowserWindow, ipcMain } from 'electron'; import { app, BrowserWindow, ipcMain } from 'electron';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
@@ -53,38 +53,49 @@ export function setupUpdateHandlers(_mainWindow: BrowserWindow) {
const { platform } = process; const { platform } = process;
// 关闭当前应用 // 先启动安装程序,再退出应用
app.quit(); try {
if (platform === 'win32') {
// 根据不同平台执行安装 // 使用spawn替代exec并使用detached选项确保子进程独立运行
if (platform === 'win32') { const child = spawn(filePath, [], {
exec(`"${filePath}"`, (error) => { detached: true,
if (error) { stdio: 'ignore'
console.error('Error starting installer:', error);
}
});
} else if (platform === 'darwin') {
// 挂载 DMG 文件
exec(`open "${filePath}"`, (error) => {
if (error) {
console.error('Error opening DMG:', error);
}
});
} else if (platform === 'linux') {
const ext = path.extname(filePath);
if (ext === '.AppImage') {
exec(`chmod +x "${filePath}" && "${filePath}"`, (error) => {
if (error) {
console.error('Error running AppImage:', error);
}
}); });
} else if (ext === '.deb') { child.unref();
exec(`pkexec dpkg -i "${filePath}"`, (error) => { } else if (platform === 'darwin') {
if (error) { // 挂载 DMG 文件
console.error('Error installing deb package:', error); const child = spawn('open', [filePath], {
} detached: true,
stdio: 'ignore'
}); });
child.unref();
} else if (platform === 'linux') {
const ext = path.extname(filePath);
if (ext === '.AppImage') {
// 先添加执行权限
fs.chmodSync(filePath, '755');
const child = spawn(filePath, [], {
detached: true,
stdio: 'ignore'
});
child.unref();
} else if (ext === '.deb') {
const child = spawn('pkexec', ['dpkg', '-i', filePath], {
detached: true,
stdio: 'ignore'
});
child.unref();
}
} }
// 给安装程序一点时间启动
setTimeout(() => {
app.quit();
}, 500);
} catch (error) {
console.error('启动安装程序失败:', error);
// 尽管出错,仍然尝试退出应用
app.quit();
} }
}); });
} }

View File

@@ -35,23 +35,22 @@
</div> </div>
</div> </div>
<div class="modal-actions" :class="{ 'mt-6': !downloading }"> <div class="modal-actions" :class="{ 'mt-6': !downloading }">
<n-button <n-button class="cancel-btn" :disabled="downloading" @click="closeModal">
class="cancel-btn"
:disabled="downloading"
:loading="downloading"
@click="closeModal"
>
{{ t('comp.update.cancel') }} {{ t('comp.update.cancel') }}
</n-button> </n-button>
<n-button <n-button
v-if="!downloading"
type="primary" type="primary"
class="update-btn" class="update-btn"
:loading="downloading"
:disabled="downloading" :disabled="downloading"
@click="handleUpdate" @click="handleUpdate"
> >
{{ downloadBtnText }} {{ downloadBtnText }}
</n-button> </n-button>
<!-- 后台下载 -->
<n-button v-else class="update-btn" type="primary" @click="closeModal">
{{ t('comp.update.backgroundDownload') }}
</n-button>
</div> </div>
<div v-if="!downloading" class="modal-desc mt-4 text-center"> <div v-if="!downloading" class="modal-desc mt-4 text-center">
<p class="text-xs text-gray-400"> <p class="text-xs text-gray-400">
@@ -71,7 +70,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { marked } from 'marked'; import { marked } from 'marked';
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { computed, h, onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSettingsStore } from '@/store/modules/settings'; import { useSettingsStore } from '@/store/modules/settings';
@@ -80,6 +79,8 @@ import { checkUpdate, getProxyNodes, UpdateResult } from '@/utils/update';
import config from '../../../../package.json'; import config from '../../../../package.json';
const { t } = useI18n(); const { t } = useI18n();
const dialog = useDialog();
const message = useMessage();
// 配置 marked // 配置 marked
marked.setOptions({ marked.setOptions({
@@ -136,6 +137,11 @@ const downloadBtnText = computed(() => {
return t('comp.update.nowUpdate'); return t('comp.update.nowUpdate');
}); });
// 下载完成后的文件路径
const downloadedFilePath = ref('');
// 防止对话框重复弹出
const isDialogShown = ref(false);
// 处理下载状态更新 // 处理下载状态更新
const handleDownloadProgress = (_event: any, progress: number, status: string) => { const handleDownloadProgress = (_event: any, progress: number, status: string) => {
downloadProgress.value = progress; downloadProgress.value = progress;
@@ -145,16 +151,73 @@ const handleDownloadProgress = (_event: any, progress: number, status: string) =
// 处理下载完成 // 处理下载完成
const handleDownloadComplete = (_event: any, success: boolean, filePath: string) => { const handleDownloadComplete = (_event: any, success: boolean, filePath: string) => {
downloading.value = false; downloading.value = false;
if (success) { closeModal();
window.electron.ipcRenderer.send('install-update', filePath);
} else { if (success && !isDialogShown.value) {
window.$message.error(t('comp.update.downloadFailed')); downloadedFilePath.value = filePath;
isDialogShown.value = true;
// 复制文件路径到剪贴板
const copyFilePath = () => {
navigator.clipboard
.writeText(filePath)
.then(() => {
message.success(t('comp.update.copySuccess'));
})
.catch(() => {
message.error(t('comp.update.copyFailed'));
});
};
// 使用naive-ui的对话框询问用户是否安装
const dialogRef = dialog.create({
title: t('comp.update.installConfirmTitle'),
content: () =>
h('div', { class: 'update-dialog-content' }, [
h('p', { class: 'content-text' }, t('comp.update.installConfirmContent')),
h('div', { class: 'divider' }),
h('p', { class: 'manual-tip' }, t('comp.update.manualInstallTip')),
h('div', { class: 'file-path-container' }, [
h('div', { class: 'file-path-box' }, [
h('p', { class: 'file-path-label' }, t('comp.update.fileLocation')),
h('div', { class: 'file-path-value' }, filePath)
]),
h(
'div',
{
class: 'copy-btn',
onClick: copyFilePath
},
[h('i', { class: 'ri-file-copy-line' }), h('span', t('comp.update.copy'))]
)
])
]),
positiveText: t('comp.update.yesInstall'),
negativeText: t('comp.update.noThanks'),
onPositiveClick: () => {
window.electron.ipcRenderer.send('install-update', filePath);
},
onNegativeClick: () => {
closeModal();
// 关闭当前窗口
dialogRef.destroy();
},
onClose: () => {
isDialogShown.value = false;
}
});
} else if (!success) {
message.error(t('comp.update.downloadFailed'));
} }
}; };
// 监听下载事件 // 监听下载事件
onMounted(() => { onMounted(() => {
checkForUpdates(); checkForUpdates();
// 确保事件监听器只注册一次
window.electron.ipcRenderer.removeListener('download-progress', handleDownloadProgress);
window.electron.ipcRenderer.removeListener('download-complete', handleDownloadComplete);
window.electron.ipcRenderer.on('download-progress', handleDownloadProgress); window.electron.ipcRenderer.on('download-progress', handleDownloadProgress);
window.electron.ipcRenderer.on('download-complete', handleDownloadComplete); window.electron.ipcRenderer.on('download-complete', handleDownloadComplete);
}); });
@@ -163,6 +226,7 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
window.electron.ipcRenderer.removeListener('download-progress', handleDownloadProgress); window.electron.ipcRenderer.removeListener('download-progress', handleDownloadProgress);
window.electron.ipcRenderer.removeListener('download-complete', handleDownloadComplete); window.electron.ipcRenderer.removeListener('download-complete', handleDownloadComplete);
isDialogShown.value = false;
}); });
const handleUpdate = async () => { const handleUpdate = async () => {
@@ -215,6 +279,7 @@ const handleUpdate = async () => {
try { try {
downloading.value = true; downloading.value = true;
downloadStatus.value = t('comp.update.prepareDownload'); downloadStatus.value = t('comp.update.prepareDownload');
isDialogShown.value = false;
// 获取代理节点列表 // 获取代理节点列表
const proxyHosts = await getProxyNodes(); const proxyHosts = await getProxyNodes();
@@ -224,11 +289,11 @@ const handleUpdate = async () => {
window.electron.ipcRenderer.send('start-download', proxyDownloadUrl); window.electron.ipcRenderer.send('start-download', proxyDownloadUrl);
} catch (error) { } catch (error) {
downloading.value = false; downloading.value = false;
window.$message.error(t('comp.update.startFailed')); message.error(t('comp.update.startFailed'));
console.error('下载失败:', error); console.error('下载失败:', error);
} }
} else { } else {
window.$message.error(t('comp.update.noDownloadUrl')); message.error(t('comp.update.noDownloadUrl'));
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank'); window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
} }
}; };
@@ -355,3 +420,110 @@ const handleUpdate = async () => {
} }
} }
</style> </style>
<style lang="scss">
/* 对话框内容样式 */
.update-dialog-content {
display: flex;
flex-direction: column;
gap: 12px;
.content-text {
font-size: 16px;
font-weight: 500;
}
.divider {
width: 100%;
height: 1px;
background-color: #e5e7eb;
margin: 4px 0;
}
.manual-tip {
font-size: 14px;
color: #6b7280;
}
.file-path-container {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
.file-path-box {
flex: 1;
.file-path-label {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.file-path-value {
padding: 8px;
border-radius: 4px;
background-color: #f3f4f6;
font-size: 12px;
font-family: monospace;
color: #1f2937;
word-break: break-all;
}
}
.copy-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
border-radius: 4px;
background-color: #e5e7eb;
color: #4b5563;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: #d1d5db;
}
i {
font-size: 14px;
}
}
}
}
/* 深色模式样式 */
.dark .update-dialog-content {
.divider {
background-color: #374151;
}
.manual-tip {
color: #9ca3af;
}
.file-path-container {
.file-path-box {
.file-path-label {
color: #9ca3af;
}
.file-path-value {
background-color: #1f2937;
color: #d1d5db;
}
}
.copy-btn {
background-color: #374151;
color: #d1d5db;
&:hover {
background-color: #4b5563;
}
}
}
}
</style>