mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 09:20:18 +08:00
feat(download): 支持重试下载功能并更新相关逻辑
This commit is contained in:
@@ -21,7 +21,7 @@
|
|||||||
- [ ] 完善项目文档
|
- [ ] 完善项目文档
|
||||||
- [x] 可以展示应用列表及其详细信息
|
- [x] 可以展示应用列表及其详细信息
|
||||||
- [ ] 实现应用下载&下载列表管理
|
- [ ] 实现应用下载&下载列表管理
|
||||||
- [x] 实现应用安装
|
- [x] 实现应用安装&重试安装
|
||||||
- [ ] 实现应用卸载
|
- [ ] 实现应用卸载
|
||||||
- [ ] 实现应用更新
|
- [ ] 实现应用更新
|
||||||
- [ ] 支持显示本地是否已经安装
|
- [ ] 支持显示本地是否已经安装
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ const tasks = new Map<number, DownloadTask>();
|
|||||||
let idle = true; // Indicates if the installation manager is idle
|
let idle = true; // Indicates if the installation manager is idle
|
||||||
|
|
||||||
// Listen for download requests from renderer process
|
// Listen for download requests from renderer process
|
||||||
ipcMain.on('queue-install', async (event, download) => {
|
ipcMain.on('queue-install', async (event, download_json) => {
|
||||||
|
const download = JSON.parse(download_json);
|
||||||
const { id, pkgname } = download || {};
|
const { id, pkgname } = download || {};
|
||||||
|
|
||||||
if (!id || !pkgname) {
|
if (!id || !pkgname) {
|
||||||
@@ -29,8 +30,8 @@ ipcMain.on('queue-install', async (event, download) => {
|
|||||||
|
|
||||||
logger.info(`收到下载任务: ${id}, 软件包名称: ${pkgname}`);
|
logger.info(`收到下载任务: ${id}, 软件包名称: ${pkgname}`);
|
||||||
|
|
||||||
// 避免重复添加同一任务
|
// 避免重复添加同一任务,但允许重试下载
|
||||||
if (tasks.has(id)) {
|
if (tasks.has(id) && !download.retry) {
|
||||||
tasks.get(id)?.webContents.send('install-log', {
|
tasks.get(id)?.webContents.send('install-log', {
|
||||||
id,
|
id,
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
|
|||||||
96
src/App.vue
96
src/App.vue
@@ -40,7 +40,7 @@ import DownloadQueue from './components/DownloadQueue.vue';
|
|||||||
import DownloadDetail from './components/DownloadDetail.vue';
|
import DownloadDetail from './components/DownloadDetail.vue';
|
||||||
import { APM_STORE_ARCHITECTURE, APM_STORE_BASE_URL, currentApp } from './global/storeConfig';
|
import { APM_STORE_ARCHITECTURE, APM_STORE_BASE_URL, currentApp } from './global/storeConfig';
|
||||||
import { downloads } from './global/downloadStatus';
|
import { downloads } from './global/downloadStatus';
|
||||||
import { handleInstall } from './js/processInstall';
|
import { handleInstall, handleRetry } from './modeuls/processInstall';
|
||||||
|
|
||||||
const logger = pino();
|
const logger = pino();
|
||||||
|
|
||||||
@@ -249,56 +249,58 @@ const escapeHtml = (s) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 下载管理方法
|
// 下载管理方法
|
||||||
const simulateDownload = (download) => {
|
// 在这里保留这个方便以后参考
|
||||||
// 模拟下载进度(实际应该调用真实的下载 API)
|
// const simulateDownload = (download) => {
|
||||||
const totalSize = Math.random() * 100 + 50; // MB
|
// // 模拟下载进度(实际应该调用真实的下载 API)
|
||||||
download.totalSize = totalSize * 1024 * 1024;
|
// const totalSize = Math.random() * 100 + 50; // MB
|
||||||
|
// download.totalSize = totalSize * 1024 * 1024;
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
// const interval = setInterval(() => {
|
||||||
const downloadObj = downloads.value.find(d => d.id === download.id);
|
// const downloadObj = downloads.value.find(d => d.id === download.id);
|
||||||
if (!downloadObj || downloadObj.status !== 'downloading') {
|
// if (!downloadObj || downloadObj.status !== 'downloading') {
|
||||||
clearInterval(interval);
|
// clearInterval(interval);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 更新进度
|
// // 更新进度
|
||||||
downloadObj.progress = Math.min(downloadObj.progress + Math.random() * 10, 100);
|
// downloadObj.progress = Math.min(downloadObj.progress + Math.random() * 10, 100);
|
||||||
downloadObj.downloadedSize = (downloadObj.progress / 100) * downloadObj.totalSize;
|
// downloadObj.downloadedSize = (downloadObj.progress / 100) * downloadObj.totalSize;
|
||||||
downloadObj.speed = (Math.random() * 5 + 1) * 1024 * 1024; // 1-6 MB/s
|
// downloadObj.speed = (Math.random() * 5 + 1) * 1024 * 1024; // 1-6 MB/s
|
||||||
|
|
||||||
const remainingBytes = downloadObj.totalSize - downloadObj.downloadedSize;
|
// const remainingBytes = downloadObj.totalSize - downloadObj.downloadedSize;
|
||||||
downloadObj.timeRemaining = Math.ceil(remainingBytes / downloadObj.speed);
|
// downloadObj.timeRemaining = Math.ceil(remainingBytes / downloadObj.speed);
|
||||||
|
|
||||||
// 添加日志
|
// // 添加日志
|
||||||
if (downloadObj.progress % 20 === 0 && downloadObj.progress > 0 && downloadObj.progress < 100) {
|
// if (downloadObj.progress % 20 === 0 && downloadObj.progress > 0 && downloadObj.progress < 100) {
|
||||||
downloadObj.logs.push({
|
// downloadObj.logs.push({
|
||||||
time: Date.now(),
|
// time: Date.now(),
|
||||||
message: `下载进度: ${downloadObj.progress.toFixed(0)}%`
|
// message: `下载进度: ${downloadObj.progress.toFixed(0)}%`
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 下载完成
|
// // 下载完成
|
||||||
if (downloadObj.progress >= 100) {
|
// if (downloadObj.progress >= 100) {
|
||||||
clearInterval(interval);
|
// clearInterval(interval);
|
||||||
downloadObj.status = 'installing';
|
// downloadObj.status = 'installing';
|
||||||
downloadObj.logs.push({
|
// downloadObj.logs.push({
|
||||||
time: Date.now(),
|
// time: Date.now(),
|
||||||
message: '下载完成,开始安装...'
|
// message: '下载完成,开始安装...'
|
||||||
});
|
// });
|
||||||
|
|
||||||
// 模拟安装
|
// // 模拟安装
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
downloadObj.status = 'completed';
|
// downloadObj.status = 'completed';
|
||||||
downloadObj.endTime = Date.now();
|
// downloadObj.endTime = Date.now();
|
||||||
downloadObj.logs.push({
|
// downloadObj.logs.push({
|
||||||
time: Date.now(),
|
// time: Date.now(),
|
||||||
message: '安装完成!'
|
// message: '安装完成!'
|
||||||
});
|
// });
|
||||||
}, 2000);
|
// }, 2000);
|
||||||
}
|
// }
|
||||||
}, 500);
|
// }, 500);
|
||||||
};
|
// };
|
||||||
|
|
||||||
|
// 目前 APM 商店不能暂停下载(因为 APM 本身不支持),但保留这些方法以备将来使用
|
||||||
const pauseDownload = (id) => {
|
const pauseDownload = (id) => {
|
||||||
const download = downloads.value.find(d => d.id === id);
|
const download = downloads.value.find(d => d.id === id);
|
||||||
if (download && download.status === 'downloading') {
|
if (download && download.status === 'downloading') {
|
||||||
@@ -310,6 +312,7 @@ const pauseDownload = (id) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 同理
|
||||||
const resumeDownload = (id) => {
|
const resumeDownload = (id) => {
|
||||||
const download = downloads.value.find(d => d.id === id);
|
const download = downloads.value.find(d => d.id === id);
|
||||||
if (download && download.status === 'paused') {
|
if (download && download.status === 'paused') {
|
||||||
@@ -322,6 +325,7 @@ const resumeDownload = (id) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 同理
|
||||||
const cancelDownload = (id) => {
|
const cancelDownload = (id) => {
|
||||||
const index = downloads.value.findIndex(d => d.id === id);
|
const index = downloads.value.findIndex(d => d.id === id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
@@ -342,14 +346,14 @@ const cancelDownload = (id) => {
|
|||||||
const retryDownload = (id) => {
|
const retryDownload = (id) => {
|
||||||
const download = downloads.value.find(d => d.id === id);
|
const download = downloads.value.find(d => d.id === id);
|
||||||
if (download && download.status === 'failed') {
|
if (download && download.status === 'failed') {
|
||||||
download.status = 'downloading';
|
download.status = 'queued';
|
||||||
download.progress = 0;
|
download.progress = 0;
|
||||||
download.downloadedSize = 0;
|
download.downloadedSize = 0;
|
||||||
download.logs.push({
|
download.logs.push({
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
message: '重新开始下载...'
|
message: '重新开始下载...'
|
||||||
});
|
});
|
||||||
simulateDownload(download);
|
handleRetry(download);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="actions-section">
|
<div class="actions-section">
|
||||||
<button
|
<!-- <button
|
||||||
v-if="download.status === 'downloading'"
|
v-if="download.status === 'downloading'"
|
||||||
@click="pause"
|
@click="pause"
|
||||||
class="action-btn secondary"
|
class="action-btn secondary"
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
>
|
>
|
||||||
<i class="fas fa-play"></i>
|
<i class="fas fa-play"></i>
|
||||||
继续下载
|
继续下载
|
||||||
</button>
|
</button> -->
|
||||||
<button
|
<button
|
||||||
v-if="download.status === 'failed'"
|
v-if="download.status === 'failed'"
|
||||||
@click="retry"
|
@click="retry"
|
||||||
@@ -120,14 +120,14 @@
|
|||||||
<i class="fas fa-redo"></i>
|
<i class="fas fa-redo"></i>
|
||||||
重试下载
|
重试下载
|
||||||
</button>
|
</button>
|
||||||
<button
|
<!-- <button
|
||||||
v-if="download.status !== 'completed'"
|
v-if="download.status !== 'completed'"
|
||||||
@click="cancel"
|
@click="cancel"
|
||||||
class="action-btn danger"
|
class="action-btn danger"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
取消下载
|
取消下载
|
||||||
</button>
|
</button> -->
|
||||||
<button
|
<button
|
||||||
v-if="download.status === 'completed'"
|
v-if="download.status === 'completed'"
|
||||||
@click="openApp"
|
@click="openApp"
|
||||||
@@ -168,15 +168,15 @@ const handleOverlayClick = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pause = () => {
|
const pause = () => {
|
||||||
emit('pause', props.download.id);
|
// emit('pause', props.download.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resume = () => {
|
const resume = () => {
|
||||||
emit('resume', props.download.id);
|
// emit('resume', props.download.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
emit('cancel', props.download.id);
|
//emit('cancel', props.download.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const retry = () => {
|
const retry = () => {
|
||||||
@@ -523,9 +523,11 @@ const copyLogs = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
/* .action-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
@@ -539,9 +541,9 @@ const copyLogs = () => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.action-btn.primary {
|
/* .action-btn.primary {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@@ -554,15 +556,23 @@ const copyLogs = () => {
|
|||||||
.action-btn.secondary {
|
.action-btn.secondary {
|
||||||
background: var(--glass);
|
background: var(--glass);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
} */
|
||||||
|
|
||||||
.action-btn.secondary:hover {
|
.action-btn.primary {
|
||||||
background: var(--border);
|
background: rgba(255, 59, 48, 0.1);
|
||||||
|
color: #FF3B30;
|
||||||
|
min-width: 140px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.danger {
|
.action-btn.danger {
|
||||||
background: rgba(255, 59, 48, 0.1);
|
background: rgba(255, 59, 48, 0.1);
|
||||||
color: #FF3B30;
|
color: #FF3B30;
|
||||||
|
width: fill-available;
|
||||||
|
min-width: 140px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.danger:hover {
|
.action-btn.danger:hover {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
<div class="download-info">
|
<div class="download-info">
|
||||||
<div class="download-name">{{ download.name }}</div>
|
<div class="download-name">{{ download.name }}</div>
|
||||||
<div class="download-status-text">
|
<div class="download-status-text">
|
||||||
|
<!-- downloading 这部分APM用不到,留给后续的Spark Store -->
|
||||||
<span v-if="download.status === 'downloading'">
|
<span v-if="download.status === 'downloading'">
|
||||||
下载中 {{ download.progress }}%
|
下载中 {{ download.progress }}%
|
||||||
</span>
|
</span>
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="download-actions">
|
<div class="download-actions">
|
||||||
<button
|
<!-- <button
|
||||||
v-if="download.status === 'downloading'"
|
v-if="download.status === 'downloading'"
|
||||||
@click.stop="pauseDownload(download.id)"
|
@click.stop="pauseDownload(download.id)"
|
||||||
class="action-icon"
|
class="action-icon"
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
title="继续"
|
title="继续"
|
||||||
>
|
>
|
||||||
<i class="fas fa-play"></i>
|
<i class="fas fa-play"></i>
|
||||||
</button>
|
</button> -->
|
||||||
<button
|
<button
|
||||||
v-if="download.status === 'failed'"
|
v-if="download.status === 'failed'"
|
||||||
@click.stop="retryDownload(download.id)"
|
@click.stop="retryDownload(download.id)"
|
||||||
@@ -86,13 +87,13 @@
|
|||||||
>
|
>
|
||||||
<i class="fas fa-redo"></i>
|
<i class="fas fa-redo"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<!-- <button
|
||||||
@click.stop="cancelDownload(download.id)"
|
@click.stop="cancelDownload(download.id)"
|
||||||
class="action-icon"
|
class="action-icon"
|
||||||
title="取消"
|
title="取消"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,15 +134,15 @@ const toggleExpand = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pauseDownload = (id) => {
|
const pauseDownload = (id) => {
|
||||||
emit('pause', id);
|
// emit('pause', id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resumeDownload = (id) => {
|
const resumeDownload = (id) => {
|
||||||
emit('resume', id);
|
// emit('resume', id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelDownload = (id) => {
|
const cancelDownload = (id) => {
|
||||||
emit('cancel', id);
|
// emit('cancel', id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const retryDownload = (id) => {
|
const retryDownload = (id) => {
|
||||||
|
|||||||
@@ -29,4 +29,5 @@ export interface DownloadItem {
|
|||||||
message: string; // 日志消息
|
message: string; // 日志消息
|
||||||
}>;
|
}>;
|
||||||
source: string; // 例如 'APM Store'
|
source: string; // 例如 'APM Store'
|
||||||
|
retry: boolean; // 当前是否为重试下载
|
||||||
}
|
}
|
||||||
@@ -31,23 +31,28 @@ export const handleInstall = () => {
|
|||||||
logs: [
|
logs: [
|
||||||
{ time: Date.now(), message: '开始下载...' }
|
{ time: Date.now(), message: '开始下载...' }
|
||||||
],
|
],
|
||||||
source: 'APM Store'
|
source: 'APM Store',
|
||||||
|
retry: false
|
||||||
};
|
};
|
||||||
|
|
||||||
downloads.value.push(download);
|
downloads.value.push(download);
|
||||||
|
|
||||||
// 模拟下载进度(实际应该调用真实的下载 API)
|
|
||||||
// simulateDownload(download);
|
|
||||||
|
|
||||||
// Send to main process to start download
|
// Send to main process to start download
|
||||||
window.ipcRenderer.send('queue-install', download);
|
window.ipcRenderer.send('queue-install', JSON.stringify(download));
|
||||||
|
|
||||||
const encodedPkg = encodeURIComponent(currentApp.value.Pkgname);
|
// const encodedPkg = encodeURIComponent(currentApp.value.Pkgname);
|
||||||
// openApmStoreUrl(`apmstore://install?pkg=${encodedPkg}`, {
|
// openApmStoreUrl(`apmstore://install?pkg=${encodedPkg}`, {
|
||||||
// fallbackText: `/usr/bin/apm-installer --install ${currentApp.value.Pkgname}`
|
// fallbackText: `/usr/bin/apm-installer --install ${currentApp.value.Pkgname}`
|
||||||
// });
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const handleRetry = (download_: DownloadItem) => {
|
||||||
|
if (!download_?.pkgname) return;
|
||||||
|
download_.retry = true;
|
||||||
|
// Send to main process to start download
|
||||||
|
window.ipcRenderer.send('queue-install', JSON.stringify(download_));
|
||||||
|
};
|
||||||
|
|
||||||
window.ipcRenderer.on('install-status', (_event, log: InstallLog) => {
|
window.ipcRenderer.on('install-status', (_event, log: InstallLog) => {
|
||||||
const downloadObj: any = downloads.value.find(d => d.id === log.id);
|
const downloadObj: any = downloads.value.find(d => d.id === log.id);
|
||||||
downloadObj.status = log.message;
|
downloadObj.status = log.message;
|
||||||
Reference in New Issue
Block a user