mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 09:20:18 +08:00
feat: 添加已安装应用和可更新应用的管理功能,支持卸载和升级操作
This commit is contained in:
@@ -37,10 +37,87 @@ const checkSuperUserCommand = async (): Promise<string> => {
|
|||||||
return superUserCmd;
|
return superUserCmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runCommandCapture = async (execCommand: string, execParams: string[], envOverride?: NodeJS.ProcessEnv) => {
|
||||||
|
return await new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
|
||||||
|
const child = spawn(execCommand, execParams, {
|
||||||
|
shell: true,
|
||||||
|
env: { ...process.env, ...(envOverride || {}) }
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
resolve({ code: -1, stdout, stderr: err.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
resolve({ code: typeof code === 'number' ? code : -1, stdout, stderr });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseInstalledList = (output: string) => {
|
||||||
|
const apps: Array<{ pkgname: string; version: string; arch: string; flags: string; raw: string }> = [];
|
||||||
|
const lines = output.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
if (trimmed.startsWith('Listing')) continue;
|
||||||
|
if (trimmed.startsWith('[INFO]')) continue;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^(\S+)\/\S+,\S+\s+(\S+)\s+(\S+)\s+\[(.+)\]$/);
|
||||||
|
if (!match) continue;
|
||||||
|
apps.push({
|
||||||
|
pkgname: match[1],
|
||||||
|
version: match[2],
|
||||||
|
arch: match[3],
|
||||||
|
flags: match[4],
|
||||||
|
raw: trimmed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseUpgradableList = (output: string) => {
|
||||||
|
const apps: Array<{ pkgname: string; newVersion: string; currentVersion: string; raw: string }> = [];
|
||||||
|
const lines = output.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
if (trimmed.startsWith('Listing')) continue;
|
||||||
|
if (trimmed.startsWith('[INFO]')) continue;
|
||||||
|
if (trimmed.includes('=') && !trimmed.includes('/')) continue;
|
||||||
|
|
||||||
|
if (!trimmed.includes('/')) continue;
|
||||||
|
|
||||||
|
const tokens = trimmed.split(/\s+/);
|
||||||
|
if (tokens.length < 2) continue;
|
||||||
|
const pkgToken = tokens[0];
|
||||||
|
const pkgname = pkgToken.split('/')[0];
|
||||||
|
const newVersion = tokens[1] || '';
|
||||||
|
const currentMatch = trimmed.match(/\[(?:upgradable from|from):\s*([^\]\s]+)\]/i);
|
||||||
|
const currentToken = tokens[5] || '';
|
||||||
|
const currentVersion = currentMatch?.[1] || currentToken.replace('[', '').replace(']', '');
|
||||||
|
|
||||||
|
if (!pkgname) continue;
|
||||||
|
apps.push({ pkgname, newVersion, currentVersion, raw: trimmed });
|
||||||
|
}
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
// Listen for download requests from renderer process
|
// Listen for download requests from renderer process
|
||||||
ipcMain.on('queue-install', async (event, download_json) => {
|
ipcMain.on('queue-install', async (event, download_json) => {
|
||||||
const download = JSON.parse(download_json);
|
const download = JSON.parse(download_json);
|
||||||
const { id, pkgname } = download || {};
|
const { id, pkgname, upgradeOnly } = download || {};
|
||||||
|
|
||||||
if (!id || !pkgname) {
|
if (!id || !pkgname) {
|
||||||
logger.warn('passed arguments missing id or pkgname');
|
logger.warn('passed arguments missing id or pkgname');
|
||||||
@@ -250,4 +327,72 @@ ipcMain.on('remove-installed', async (_event, pkgname: string) => {
|
|||||||
message: JSON.stringify(messageJSONObj)
|
message: JSON.stringify(messageJSONObj)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('list-upgradable', async () => {
|
||||||
|
const listCommand = 'source /opt/apm-store/transhell.sh; load_transhell_debug; amber-pm-debug aptss list --upgradable';
|
||||||
|
const { code, stdout, stderr } = await runCommandCapture(
|
||||||
|
'/bin/bash',
|
||||||
|
['-lc', listCommand],
|
||||||
|
{ LANGUAGE: 'en_US' }
|
||||||
|
);
|
||||||
|
if (code !== 0) {
|
||||||
|
logger.error(`list-upgradable failed: ${stderr || stdout}`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: stderr || stdout || `list-upgradable failed with code ${code}`,
|
||||||
|
apps: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const apps = parseUpgradableList(stdout);
|
||||||
|
return { success: true, apps };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('list-installed', async () => {
|
||||||
|
const superUserCmd = await checkSuperUserCommand();
|
||||||
|
const execCommand = superUserCmd.length > 0 ? superUserCmd : '/usr/bin/apm';
|
||||||
|
const execParams = superUserCmd.length > 0
|
||||||
|
? ['/usr/bin/apm', 'list', '--installed']
|
||||||
|
: ['list', '--installed'];
|
||||||
|
|
||||||
|
const { code, stdout, stderr } = await runCommandCapture(execCommand, execParams);
|
||||||
|
if (code !== 0) {
|
||||||
|
logger.error(`list-installed failed: ${stderr || stdout}`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: stderr || stdout || `list-installed failed with code ${code}`,
|
||||||
|
apps: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const apps = parseInstalledList(stdout);
|
||||||
|
return { success: true, apps };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('uninstall-installed', async (_event, pkgname: string) => {
|
||||||
|
if (!pkgname) {
|
||||||
|
logger.warn('uninstall-installed missing pkgname');
|
||||||
|
return { success: false, message: 'missing pkgname' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const superUserCmd = await checkSuperUserCommand();
|
||||||
|
const execCommand = superUserCmd.length > 0 ? superUserCmd : '/usr/bin/apm';
|
||||||
|
const execParams = superUserCmd.length > 0
|
||||||
|
? ['/usr/bin/apm', 'remove', '-y', pkgname]
|
||||||
|
: ['remove', '-y', pkgname];
|
||||||
|
|
||||||
|
const { code, stdout, stderr } = await runCommandCapture(execCommand, execParams);
|
||||||
|
const success = code === 0;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.info(`卸载完成: ${pkgname}`);
|
||||||
|
} else {
|
||||||
|
logger.error(`卸载失败: ${pkgname} ${stderr || stdout}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
message: success ? '卸载完成' : (stderr || stdout || `卸载失败,退出码 ${code}`)
|
||||||
|
};
|
||||||
});
|
});
|
||||||
133
src/App.vue
133
src/App.vue
@@ -24,6 +24,15 @@
|
|||||||
<DownloadDetail :show="showDownloadDetailModal" :download="currentDownload" @close="closeDownloadDetail"
|
<DownloadDetail :show="showDownloadDetailModal" :download="currentDownload" @close="closeDownloadDetail"
|
||||||
@pause="pauseDownload" @resume="resumeDownload" @cancel="cancelDownload" @retry="retryDownload"
|
@pause="pauseDownload" @resume="resumeDownload" @cancel="cancelDownload" @retry="retryDownload"
|
||||||
@open-app="openDownloadedApp" />
|
@open-app="openDownloadedApp" />
|
||||||
|
|
||||||
|
<InstalledAppsModal :show="showInstalledModal" :apps="installedApps" :loading="installedLoading"
|
||||||
|
:error="installedError" @close="closeInstalledModal" @refresh="refreshInstalledApps"
|
||||||
|
@uninstall="uninstallInstalledApp" />
|
||||||
|
|
||||||
|
<UpdateAppsModal :show="showUpdateModal" :apps="upgradableApps" :loading="updateLoading"
|
||||||
|
:error="updateError" :has-selected="hasSelectedUpgrades" @close="closeUpdateModal"
|
||||||
|
@refresh="refreshUpgradableApps" @toggle-all="toggleAllUpgrades"
|
||||||
|
@upgrade-selected="upgradeSelectedApps" @upgrade-one="upgradeSingleApp" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -38,9 +47,11 @@ import AppDetailModal from './components/AppDetailModal.vue';
|
|||||||
import ScreenPreview from './components/ScreenPreview.vue';
|
import ScreenPreview from './components/ScreenPreview.vue';
|
||||||
import DownloadQueue from './components/DownloadQueue.vue';
|
import DownloadQueue from './components/DownloadQueue.vue';
|
||||||
import DownloadDetail from './components/DownloadDetail.vue';
|
import DownloadDetail from './components/DownloadDetail.vue';
|
||||||
|
import InstalledAppsModal from './components/InstalledAppsModal.vue';
|
||||||
|
import UpdateAppsModal from './components/UpdateAppsModal.vue';
|
||||||
import { APM_STORE_ARCHITECTURE, APM_STORE_BASE_URL, currentApp, currentAppIsInstalled } from './global/storeConfig';
|
import { APM_STORE_ARCHITECTURE, APM_STORE_BASE_URL, currentApp, currentAppIsInstalled } from './global/storeConfig';
|
||||||
import { downloads } from './global/downloadStatus';
|
import { downloads } from './global/downloadStatus';
|
||||||
import { handleInstall, handleRetry, handleRemove } from './modeuls/processInstall';
|
import { handleInstall, handleRetry, handleRemove, handleUpgrade } from './modeuls/processInstall';
|
||||||
|
|
||||||
const logger = pino();
|
const logger = pino();
|
||||||
|
|
||||||
@@ -63,6 +74,14 @@ const screenshots = ref([]);
|
|||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const showDownloadDetailModal = ref(false);
|
const showDownloadDetailModal = ref(false);
|
||||||
const currentDownload = ref(null);
|
const currentDownload = ref(null);
|
||||||
|
const showInstalledModal = ref(false);
|
||||||
|
const installedApps = ref([]);
|
||||||
|
const installedLoading = ref(false);
|
||||||
|
const installedError = ref('');
|
||||||
|
const showUpdateModal = ref(false);
|
||||||
|
const upgradableApps = ref([]);
|
||||||
|
const updateLoading = ref(false);
|
||||||
|
const updateError = ref('');
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const filteredApps = computed(() => {
|
const filteredApps = computed(() => {
|
||||||
@@ -96,6 +115,10 @@ const categoryCounts = computed(() => {
|
|||||||
return counts;
|
return counts;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hasSelectedUpgrades = computed(() => {
|
||||||
|
return upgradableApps.value.some(app => app.selected);
|
||||||
|
});
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
const initTheme = () => {
|
const initTheme = () => {
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem('theme');
|
||||||
@@ -183,17 +206,115 @@ const nextScreen = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
openApmStoreUrl('apmstore://action?cmd=update', {
|
openUpdateModal();
|
||||||
fallbackText: 'apm-update-tool'
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleList = () => {
|
const handleList = () => {
|
||||||
openApmStoreUrl('apmstore://action?cmd=list', {
|
openInstalledModal();
|
||||||
fallbackText: '/usr/bin/apm-installer --list'
|
};
|
||||||
|
|
||||||
|
const openUpdateModal = () => {
|
||||||
|
showUpdateModal.value = true;
|
||||||
|
refreshUpgradableApps();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeUpdateModal = () => {
|
||||||
|
showUpdateModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshUpgradableApps = async () => {
|
||||||
|
updateLoading.value = true;
|
||||||
|
updateError.value = '';
|
||||||
|
try {
|
||||||
|
const result = await window.ipcRenderer.invoke('list-upgradable');
|
||||||
|
if (!result?.success) {
|
||||||
|
upgradableApps.value = [];
|
||||||
|
updateError.value = result?.message || '检查更新失败';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
upgradableApps.value = (result.apps || []).map(app => ({
|
||||||
|
...app,
|
||||||
|
selected: false,
|
||||||
|
upgrading: false
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
upgradableApps.value = [];
|
||||||
|
updateError.value = error?.message || '检查更新失败';
|
||||||
|
} finally {
|
||||||
|
updateLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAllUpgrades = () => {
|
||||||
|
const shouldSelectAll = !hasSelectedUpgrades.value || upgradableApps.value.some(app => !app.selected);
|
||||||
|
upgradableApps.value = upgradableApps.value.map(app => ({
|
||||||
|
...app,
|
||||||
|
selected: shouldSelectAll ? true : false
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const upgradeSingleApp = (app) => {
|
||||||
|
if (!app?.pkgname) return;
|
||||||
|
handleUpgrade(app.pkgname, app.newVersion || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const upgradeSelectedApps = () => {
|
||||||
|
const selectedApps = upgradableApps.value.filter(app => app.selected);
|
||||||
|
selectedApps.forEach(app => {
|
||||||
|
upgradeSingleApp(app);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openInstalledModal = () => {
|
||||||
|
showInstalledModal.value = true;
|
||||||
|
refreshInstalledApps();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeInstalledModal = () => {
|
||||||
|
showInstalledModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshInstalledApps = async () => {
|
||||||
|
installedLoading.value = true;
|
||||||
|
installedError.value = '';
|
||||||
|
try {
|
||||||
|
const result = await window.ipcRenderer.invoke('list-installed');
|
||||||
|
if (!result?.success) {
|
||||||
|
installedApps.value = [];
|
||||||
|
installedError.value = result?.message || '读取已安装应用失败';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installedApps.value = (result.apps || []).map((app) => ({
|
||||||
|
...app,
|
||||||
|
removing: false
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
installedApps.value = [];
|
||||||
|
installedError.value = error?.message || '读取已安装应用失败';
|
||||||
|
} finally {
|
||||||
|
installedLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const uninstallInstalledApp = async (app) => {
|
||||||
|
if (!app?.pkgname) return;
|
||||||
|
const target = installedApps.value.find(item => item.pkgname === app.pkgname);
|
||||||
|
if (target) target.removing = true;
|
||||||
|
try {
|
||||||
|
const result = await window.ipcRenderer.invoke('uninstall-installed', app.pkgname);
|
||||||
|
if (!result?.success) {
|
||||||
|
installedError.value = result?.message || '卸载失败';
|
||||||
|
} else {
|
||||||
|
installedApps.value = installedApps.value.filter(item => item.pkgname !== app.pkgname);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
installedError.value = error?.message || '卸载失败';
|
||||||
|
} finally {
|
||||||
|
const restore = installedApps.value.find(item => item.pkgname === app.pkgname);
|
||||||
|
if (restore) restore.removing = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openApmStoreUrl = (url, { fallbackText } = {}) => {
|
const openApmStoreUrl = (url, { fallbackText } = {}) => {
|
||||||
try {
|
try {
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
|
|||||||
148
src/components/InstalledAppsModal.vue
Normal file
148
src/components/InstalledAppsModal.vue
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<div class="modal-backdrop" :style="{ display: show ? 'flex' : 'none' }" role="dialog" aria-hidden="false">
|
||||||
|
<div class="modal installed-modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title-section">
|
||||||
|
<div class="modal-title">已安装应用</div>
|
||||||
|
<div class="modal-subtitle">来自本机 APM 安装列表</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="action-btn" :disabled="loading" @click="$emit('refresh')">
|
||||||
|
<i class="fas fa-sync-alt"></i> 刷新
|
||||||
|
</button>
|
||||||
|
<button class="close-modal" @click="$emit('close')" aria-label="关闭">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="installed-content">
|
||||||
|
<div v-if="loading" class="installed-empty">正在读取已安装应用…</div>
|
||||||
|
<div v-else-if="error" class="installed-empty error">{{ error }}</div>
|
||||||
|
<div v-else-if="apps.length === 0" class="installed-empty">暂无已安装应用</div>
|
||||||
|
<div v-else class="installed-list">
|
||||||
|
<div v-for="app in apps" :key="app.pkgname" class="installed-item">
|
||||||
|
<div class="installed-info">
|
||||||
|
<div class="installed-name">{{ app.pkgname }}</div>
|
||||||
|
<div class="installed-meta">
|
||||||
|
<span>{{ app.version }}</span>
|
||||||
|
<span class="dot">·</span>
|
||||||
|
<span>{{ app.arch }}</span>
|
||||||
|
<span v-if="app.flags" class="dot">·</span>
|
||||||
|
<span v-if="app.flags">{{ app.flags }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="installed-actions">
|
||||||
|
<button class="action-btn secondary danger" :disabled="app.removing" @click="$emit('uninstall', app)">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
{{ app.removing ? '卸载中…' : '卸载' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
apps: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['close', 'refresh', 'uninstall']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.installed-modal {
|
||||||
|
width: min(900px, calc(100% - 40px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.installed-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installed-empty {
|
||||||
|
padding: 32px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted);
|
||||||
|
background: var(--glass);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.installed-empty.error {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installed-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installed-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--glass);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.installed-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installed-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installed-meta {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installed-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.danger {
|
||||||
|
color: #ef4444;
|
||||||
|
border-color: rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.danger:hover {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
154
src/components/UpdateAppsModal.vue
Normal file
154
src/components/UpdateAppsModal.vue
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<template>
|
||||||
|
<div class="modal-backdrop" :style="{ display: show ? 'flex' : 'none' }" role="dialog" aria-hidden="false">
|
||||||
|
<div class="modal update-modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title-section">
|
||||||
|
<div class="modal-title">软件更新</div>
|
||||||
|
<div class="modal-subtitle">可更新的 APM 应用</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="action-btn" :disabled="loading" @click="$emit('refresh')">
|
||||||
|
<i class="fas fa-sync-alt"></i> 刷新
|
||||||
|
</button>
|
||||||
|
<button class="action-btn secondary" :disabled="loading || apps.length === 0" @click="$emit('toggle-all')">
|
||||||
|
<i class="fas fa-check-square"></i> 全选/全不选
|
||||||
|
</button>
|
||||||
|
<button class="apm-btn" :disabled="loading || !hasSelected" @click="$emit('upgrade-selected')">
|
||||||
|
<i class="fas fa-upload"></i> 更新选中
|
||||||
|
</button>
|
||||||
|
<button class="close-modal" @click="$emit('close')" aria-label="关闭">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="update-content">
|
||||||
|
<div v-if="loading" class="update-empty">正在检查可更新应用…</div>
|
||||||
|
<div v-else-if="error" class="update-empty error">{{ error }}</div>
|
||||||
|
<div v-else-if="apps.length === 0" class="update-empty">暂无可更新应用</div>
|
||||||
|
<div v-else class="update-list">
|
||||||
|
<label v-for="app in apps" :key="app.pkgname" class="update-item">
|
||||||
|
<input type="checkbox" v-model="app.selected" :disabled="app.upgrading" />
|
||||||
|
<div class="update-info">
|
||||||
|
<div class="update-name">{{ app.pkgname }}</div>
|
||||||
|
<div class="update-meta">
|
||||||
|
<span>当前 {{ app.currentVersion || '-' }}</span>
|
||||||
|
<span class="dot">·</span>
|
||||||
|
<span>更新至 {{ app.newVersion || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="update-actions">
|
||||||
|
<button class="action-btn secondary" :disabled="app.upgrading" @click.prevent="$emit('upgrade-one', app)">
|
||||||
|
<i class="fas fa-arrow-up"></i>
|
||||||
|
{{ app.upgrading ? '更新中…' : '更新' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
apps: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
hasSelected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['close', 'refresh', 'toggle-all', 'upgrade-selected', 'upgrade-one']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.update-modal {
|
||||||
|
width: min(920px, calc(100% - 40px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-empty {
|
||||||
|
padding: 32px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted);
|
||||||
|
background: var(--glass);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-empty.error {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--glass);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-item input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-meta {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -30,4 +30,5 @@ export interface DownloadItem {
|
|||||||
}>;
|
}>;
|
||||||
source: string; // 例如 'APM Store'
|
source: string; // 例如 'APM Store'
|
||||||
retry: boolean; // 当前是否为重试下载
|
retry: boolean; // 当前是否为重试下载
|
||||||
|
upgradeOnly?: boolean; // 是否为仅升级任务
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import { downloads } from "../global/downloadStatus";
|
|||||||
import { InstallLog, DownloadItem, DownloadResult } from '../global/typedefinition';
|
import { InstallLog, DownloadItem, DownloadResult } from '../global/typedefinition';
|
||||||
|
|
||||||
let downloadIdCounter = 0;
|
let downloadIdCounter = 0;
|
||||||
|
const fallbackIcon = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3Ctext x="50" y="50" text-anchor="middle" dy=".3em" fill="%23999" font-size="14"%3EAPM%3C/text%3E%3C/svg%3E';
|
||||||
|
|
||||||
export const handleInstall = () => {
|
export const handleInstall = () => {
|
||||||
if (!currentApp.value?.Pkgname) return;
|
if (!currentApp.value?.Pkgname) return;
|
||||||
@@ -53,6 +54,35 @@ export const handleRetry = (download_: DownloadItem) => {
|
|||||||
window.ipcRenderer.send('queue-install', JSON.stringify(download_));
|
window.ipcRenderer.send('queue-install', JSON.stringify(download_));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const handleUpgrade = (pkgname: string, newVersion = '') => {
|
||||||
|
if (!pkgname) return;
|
||||||
|
|
||||||
|
downloadIdCounter += 1;
|
||||||
|
const download: DownloadItem = {
|
||||||
|
id: downloadIdCounter,
|
||||||
|
name: pkgname,
|
||||||
|
pkgname: pkgname,
|
||||||
|
version: newVersion,
|
||||||
|
icon: fallbackIcon,
|
||||||
|
status: 'queued',
|
||||||
|
progress: 0,
|
||||||
|
downloadedSize: 0,
|
||||||
|
totalSize: 0,
|
||||||
|
speed: 0,
|
||||||
|
timeRemaining: 0,
|
||||||
|
startTime: Date.now(),
|
||||||
|
logs: [
|
||||||
|
{ time: Date.now(), message: '开始更新...' }
|
||||||
|
],
|
||||||
|
source: 'APM Update',
|
||||||
|
retry: false,
|
||||||
|
upgradeOnly: true
|
||||||
|
};
|
||||||
|
|
||||||
|
downloads.value.push(download);
|
||||||
|
window.ipcRenderer.send('queue-install', JSON.stringify(download));
|
||||||
|
};
|
||||||
|
|
||||||
export const handleRemove = () => {
|
export const handleRemove = () => {
|
||||||
if (!currentApp.value?.Pkgname) return;
|
if (!currentApp.value?.Pkgname) return;
|
||||||
window.ipcRenderer.send('remove-installed', currentApp.value.Pkgname);
|
window.ipcRenderer.send('remove-installed', currentApp.value.Pkgname);
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
:root {
|
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
padding: 2px 4px;
|
|
||||||
margin: 0 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user