Compare commits

...

14 Commits

Author SHA1 Message Date
Julian Freeman
46c622fd86 optimize fetch time 2026-03-31 18:51:55 -04:00
Julian Freeman
9cdc371c75 fix bug 2026-03-31 18:38:45 -04:00
Julian Freeman
517ee39707 not sync after every install 2026-03-31 18:28:07 -04:00
Julian Freeman
61caeba242 not log long scripta 2026-03-31 17:16:18 -04:00
Julian Freeman
6dde1ea9a7 enable local manifest 2026-03-31 13:37:23 -04:00
Julian Freeman
d775e049d6 block when checking 2026-03-31 13:30:46 -04:00
Julian Freeman
145dad23a5 fix install logix 2026-03-31 13:15:50 -04:00
Julian Freeman
7e550a8d49 add cmd to log 2026-03-31 11:42:07 -04:00
Julian Freeman
cf740b9e3a fix error bug 2026-03-31 08:45:17 -04:00
Julian Freeman
b6248bec45 fix task bugs 2026-03-31 08:37:10 -04:00
Julian Freeman
50489bb9d4 fix download bug 2026-03-31 01:04:32 -04:00
Julian Freeman
6a360dc14b download file 2026-03-30 23:46:57 -04:00
Julian Freeman
7f2dde8c51 fix bug 2026-03-30 23:06:38 -04:00
Julian Freeman
a7b5955540 support custom manifest 2026-03-30 22:31:49 -04:00
6 changed files with 290 additions and 144 deletions

View File

@@ -26,6 +26,9 @@ pub struct EssentialsRepo {
pub struct InstallTask {
pub id: String,
pub version: Option<String>,
#[serde(default)]
pub use_manifest: bool,
pub manifest_url: Option<String>,
}
impl Default for AppSettings {
@@ -165,8 +168,11 @@ async fn get_updates(app: AppHandle) -> Vec<Software> {
}
#[tauri::command]
async fn install_software(id: String, version: Option<String>, state: State<'_, AppState>) -> Result<(), String> {
state.install_tx.send(InstallTask { id, version }).await.map_err(|e| e.to_string())
async fn install_software(
task: InstallTask,
state: State<'_, AppState>
) -> Result<(), String> {
state.install_tx.send(task).await.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -194,41 +200,85 @@ pub fn run() {
let size_re = Regex::new(r"([\d\.]+)\s*[a-zA-Z]+\s*/\s*([\d\.]+)\s*[a-zA-Z]+").unwrap();
while let Some(task) = rx.recv().await {
let id = task.id;
let version = task.version;
let task_id = task.id.clone();
let task_version = task.version.clone();
let use_manifest = task.use_manifest;
let manifest_url = task.manifest_url.clone();
let log_id = format!("install-{}", id);
let log_id = format!("install-{}", task_id);
// 1. 发送正在安装状态
let _ = handle.emit("install-status", InstallProgress {
id: id.clone(),
id: task_id.clone(),
status: "installing".to_string(),
progress: 0.0,
});
let display_cmd = match &version {
Some(v) => format!("Winget Install: {} (v{})", id, v),
None => format!("Winget Install: {}", id),
let mut args = vec!["install".to_string()];
let display_cmd: String;
let mut temp_manifest_path: Option<PathBuf> = None;
if use_manifest && manifest_url.is_some() {
let url = manifest_url.unwrap();
display_cmd = format!("Winget Install (Manifest): {} from {}", task_id, url);
emit_log(&handle, &log_id, &display_cmd, "Downloading remote manifest...", "info");
let client = reqwest::Client::new();
match client.get(&url).timeout(std::time::Duration::from_secs(15)).send().await {
Ok(resp) if resp.status().is_success() => {
if let Ok(content) = resp.text().await {
let temp_dir = std::env::temp_dir();
let file_name = format!("winget_tmp_{}.yaml", chrono::Local::now().timestamp_millis());
let local_path = temp_dir.join(file_name);
if fs::write(&local_path, content).is_ok() {
args.push("--manifest".to_string());
args.push(local_path.to_string_lossy().to_string());
temp_manifest_path = Some(local_path);
}
}
}
_ => {}
}
if temp_manifest_path.is_none() {
emit_log(&handle, &log_id, "Error", "Failed to download or save manifest.", "error");
let _ = handle.emit("install-status", InstallProgress {
id: task_id.clone(),
status: "error".to_string(),
progress: 0.0,
});
continue;
}
} else {
args.push("--id".to_string());
args.push(task_id.clone());
args.push("-e".to_string());
if let Some(v) = &task_version {
if !v.is_empty() {
args.push("--version".to_string());
args.push(v.clone());
}
}
display_cmd = match &task_version {
Some(v) if !v.is_empty() => format!("Winget Install: {} (v{})", task_id, v),
_ => format!("Winget Install: {}", task_id),
};
emit_log(&handle, &log_id, &display_cmd, "Starting...", "info");
}
let id_for_cmd = id.clone();
let h = handle.clone();
let mut args = vec![
"install".to_string(),
"--id".to_string(), id_for_cmd.clone(),
"-e".to_string(),
args.extend([
"--silent".to_string(),
"--accept-package-agreements".to_string(),
"--accept-source-agreements".to_string(),
"--disable-interactivity".to_string(),
];
]);
if let Some(v) = version {
if !v.is_empty() {
args.push("--version".to_string());
args.push(v);
}
}
let full_command = format!("winget {}", args.join(" "));
emit_log(&handle, &log_id, &display_cmd, &format!("Executing: {}\n---", full_command), "info");
let h = handle.clone();
let current_id = task_id.clone();
let child = Command::new("winget")
.args(&args)
@@ -241,31 +291,29 @@ pub fn run() {
Ok(mut child_proc) => {
if let Some(stdout) = child_proc.stdout.take() {
let reader = BufReader::new(stdout);
// 使用 split(b'\r') 是为了捕捉 PowerShell 中的动态进度行
for line_res in reader.split(b'\r') {
if let Ok(line_bytes) = line_res {
let line_str = String::from_utf8_lossy(&line_bytes).to_string();
let clean_line = line_str.trim();
if clean_line.is_empty() { continue; }
let mut is_progress = false;
if let Some(caps) = perc_re.captures(clean_line) {
if let Ok(p_val) = caps[1].parse::<f32>() {
let _ = h.emit("install-status", InstallProgress {
id: id_for_cmd.clone(),
id: current_id.clone(),
status: "installing".to_string(),
progress: p_val / 100.0,
});
is_progress = true;
}
}
else if let Some(caps) = size_re.captures(clean_line) {
} else if let Some(caps) = size_re.captures(clean_line) {
let current = caps[1].parse::<f32>().unwrap_or(0.0);
let total = caps[2].parse::<f32>().unwrap_or(1.0);
if total > 0.0 {
let _ = h.emit("install-status", InstallProgress {
id: id_for_cmd.clone(),
id: current_id.clone(),
status: "installing".to_string(),
progress: (current / total).min(1.0),
});
@@ -273,14 +321,12 @@ pub fn run() {
}
}
// 净化日志:过滤进度行、单字符动画行以及退格符
if !is_progress && clean_line.chars().count() > 1 {
emit_log(&h, &log_id, "", clean_line, "info");
}
}
}
}
let exit_status = child_proc.wait().map(|s| s.success()).unwrap_or(false);
if exit_status { "success" } else { "error" }
},
@@ -290,13 +336,19 @@ pub fn run() {
}
};
// 2. 发送最终完成/失败状态
let _ = handle.emit("install-status", InstallProgress {
id: id.clone(),
id: task_id.clone(),
status: status_result.to_string(),
progress: 1.0,
});
emit_log(&handle, &log_id, "Result", &format!("Execution finished: {}", status_result), if status_result == "success" { "success" } else { "error" });
// 3. 清理临时清单文件
if let Some(path) = temp_manifest_path {
let _ = fs::remove_file(path);
}
}
});

View File

@@ -16,10 +16,14 @@ pub struct Software {
pub status: String, // "idle", "pending", "installing", "success", "error"
#[serde(default = "default_progress")]
pub progress: f32,
#[serde(default = "default_false")]
pub use_manifest: bool,
pub manifest_url: Option<String>,
}
fn default_status() -> String { "idle".to_string() }
fn default_progress() -> f32 { 0.0 }
fn default_false() -> bool { false }
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
@@ -64,6 +68,7 @@ pub fn ensure_winget_dependencies(handle: &AppHandle) -> Result<(), String> {
Install-Module -Name Microsoft.WinGet.Client -Force -AllowClobber -Scope CurrentUser -Confirm:$false
winget source update --accept-source-agreements
winget settings --enable LocalManifestFiles
"#;
let output = Command::new("powershell")
@@ -223,7 +228,7 @@ pub fn list_updates(handle: &AppHandle) -> Vec<Software> {
}
fn execute_powershell(handle: &AppHandle, log_id: &str, cmd_title: &str, script: &str) -> Vec<Software> {
emit_log(handle, log_id, cmd_title, "Executing PowerShell...", "info");
emit_log(handle, log_id, cmd_title, "Fetching data from Winget...", "info");
let output = Command::new("powershell")
.args(["-NoProfile", "-Command", script])
@@ -276,5 +281,7 @@ fn map_package(p: WingetPackage) -> Software {
icon_url: p.icon_url,
status: "idle".to_string(),
progress: 0.0,
use_manifest: false,
manifest_url: None,
}
}

View File

@@ -4,7 +4,8 @@
:class="{
'installed-mode': software.status === 'installed',
'is-selected': isSelected && software.status !== 'installed',
'is-busy': software.status === 'pending' || software.status === 'installing'
'is-busy': software.status === 'pending' || software.status === 'installing',
'is-disabled': disabled && software.status === 'idle'
}"
@click="handleCardClick"
>
@@ -14,7 +15,7 @@
class="checkbox"
:class="{
'checked': isSelected,
'disabled': software.status === 'installed' || software.status === 'pending' || software.status === 'installing'
'disabled': disabled || software.status === 'installed' || software.status === 'pending' || software.status === 'installing'
}"
>
<span v-if="isSelected"></span>
@@ -34,29 +35,29 @@
<span class="id-badge">{{ software.id }}</span>
</div>
<div class="version-info">
<!-- 情况 1: 已安装软件 (包含待更新状态) -->
<template v-if="isInstalled">
<span class="version-tag">当前: {{ software.version || '--' }}</span>
<!-- 仅在装机必备且当前版本低于推荐版本时显示 -->
<span
v-if="software.recommended_version && isVersionLower(software.version, software.recommended_version)"
class="version-tag recommended"
>
<!-- 情况 1: 已安装且有推荐/最新版本 -->
<template v-if="software.version">
<span class="version-tag">当前: {{ software.version }}</span>
<span v-if="software.recommended_version && software.actionLabel === '更新'" class="version-tag recommended">
推荐: {{ software.recommended_version }}
</span>
</template>
<!-- 情况 2: 未安装软件 -->
<template v-else>
<span class="version-tag recommended">
推荐: {{ software.recommended_version || '最新版' }}
</span>
</template>
<!-- 情况 3: WinGet 检测到的仓库最新版本 -->
<span class="version-tag available" v-if="software.available_version">
<span v-if="software.available_version && !software.recommended_version" class="version-tag available">
最新: {{ software.available_version }}
</span>
</template>
<!-- 情况 2: 未安装 -->
<template v-else>
<span v-if="software.recommended_version" class="version-tag recommended">
推荐: {{ software.recommended_version }}
</span>
<span v-else class="version-tag recommended">
推荐: 最新版
</span>
<span v-if="software.available_version" class="version-tag available">
最新: {{ software.available_version }}
</span>
</template>
</div>
</div>
</div>
@@ -65,8 +66,9 @@
<div class="action-wrapper">
<button
v-if="software.status === 'idle'"
@click.stop="$emit('install', software.id)"
@click.stop="$emit('install', software.id, (software as any).targetVersion)"
class="action-btn install-btn"
:disabled="disabled"
>
{{ actionLabel }}
</button>
@@ -102,9 +104,14 @@
<span class="check-icon"></span> 已完成
</div>
<div v-else-if="software.status === 'error'" class="status-error">
失败
</div>
<button
v-else-if="software.status === 'error'"
@click.stop="$emit('install', software.id, (software as any).targetVersion)"
class="action-btn retry-btn"
:disabled="disabled"
>
重试
</button>
</div>
</div>
</div>
@@ -119,14 +126,18 @@ const props = defineProps<{
name: string;
description?: string;
version?: string;
recommended_version?: string;
available_version?: string;
icon_url?: string;
status: string;
progress: number;
actionLabel?: string;
targetVersion?: string;
},
actionLabel?: string,
selectable?: boolean,
isSelected?: boolean
isSelected?: boolean,
disabled?: boolean
}>();
const emit = defineEmits(['install', 'toggleSelect']);
@@ -136,35 +147,6 @@ const displayProgress = computed(() => {
return Math.round(props.software.progress * 100) + '%';
});
const isInstalled = computed(() => {
return props.software.status === 'installed' ||
(props.software.status === 'idle' && props.actionLabel === '更新') ||
(props.software.status === 'idle' && !props.actionLabel && props.software.installed_version);
});
const isVersionLower = (current: string | undefined | null, target: string | undefined | null) => {
if (!current || !target) return false;
if (current === target) return false;
// 简易的版本比对逻辑:按点分割比对数字
const v1 = current.split('.');
const v2 = target.split('.');
const len = Math.max(v1.length, v2.length);
for (let i = 0; i < len; i++) {
const n1 = parseInt(v1[i] || '0', 10);
const n2 = parseInt(v2[i] || '0', 10);
if (isNaN(n1) || isNaN(n2)) {
// 如果是非数字字符串比对,直接返回字符串比对结果
if (v1[i] !== v2[i]) return v1[i] < v2[i];
continue;
}
if (n1 < n2) return true;
if (n1 > n2) return false;
}
return false;
};
const placeholderColor = computed(() => {
const colors = ['#FF9500', '#FF3B30', '#34C759', '#007AFF', '#5856D6', '#AF52DE'];
let hash = 0;
@@ -211,6 +193,11 @@ const handleCardClick = () => {
opacity: 0.9;
}
.software-card.is-disabled {
opacity: 0.6;
pointer-events: none;
}
.selection-area {
margin-right: 16px;
flex-shrink: 0;
@@ -361,6 +348,16 @@ const handleCardClick = () => {
color: white;
}
.retry-btn {
background-color: rgba(255, 59, 48, 0.05);
color: #FF3B30;
}
.retry-btn:hover {
background-color: #FF3B30;
color: white;
}
.installed-btn {
background-color: #F2F2F7;
color: #AEAEB2;

View File

@@ -4,6 +4,23 @@ import { listen } from '@tauri-apps/api/event'
const sortByName = (a: any, b: any) => a.name.localeCompare(b.name, 'zh-CN', { sensitivity: 'accent' });
// 版本比对工具函数
const compareVersions = (v1: string | null | undefined, v2: string | null | undefined) => {
if (!v1 || !v2) return 0;
if (v1 === v2) return 0;
const cleanV = (v: string) => v.replace(/^v/i, '').split(/[-+]/)[0].split('.');
const p1 = cleanV(v1);
const p2 = cleanV(v2);
const len = Math.max(p1.length, p2.length);
for (let i = 0; i < len; i++) {
const n1 = parseInt(p1[i] || '0', 10);
const n2 = parseInt(p2[i] || '0', 10);
if (n1 < n2) return -1;
if (n1 > n2) return 1;
}
return 0;
};
export interface LogEntry {
id: string; // 日志唯一标识
timestamp: string;
@@ -24,51 +41,79 @@ export const useSoftwareStore = defineStore('software', {
settings: {
repo_url: 'https://karlblue.github.io/winget-repo'
},
activeTasks: {} as Record<string, { status: string, progress: number, targetVersion?: string }>,
loading: false,
isInitialized: false,
initStatus: '正在检查系统环境...',
lastFetched: 0
lastFetched: 0,
refreshTimer: null as any,
batchQueue: [] as string[]
}),
getters: {
// ... (mergedEssentials, sortedUpdates, sortedAllSoftware, isBusy getters stay the same)
mergedEssentials: (state) => {
return state.essentials.map(item => {
const installedInfo = state.allSoftware.find(s => s.id.toLowerCase() === item.id.toLowerCase());
const wingetUpdate = state.updates.find(s => s.id.toLowerCase() === item.id.toLowerCase());
const task = state.activeTasks[item.id];
const isInstalled = !!installedInfo;
const hasUpdate = state.updates.some(s => s.id.toLowerCase() === item.id.toLowerCase());
const currentVersion = installedInfo?.version;
const recommendedVersion = item.version; // 清单里的推荐版本
const availableVersion = wingetUpdate?.available_version; // Winget 查到的最新版
let displayStatus = item.status;
let displayStatus = task ? task.status : 'idle';
let actionLabel = '安装';
// 统一字段version 始终代表当前安装的版本recommended_version 代表清单推荐的版本
const currentVersion = installedInfo ? installedInfo.version : null;
const recommendedVersion = item.version;
let targetVersion = recommendedVersion || availableVersion;
if (isInstalled) {
if (hasUpdate) {
actionLabel = '更新';
} else if (displayStatus === 'idle') {
displayStatus = 'installed';
// 逻辑:已安装 >= 推荐 -> 已安装(禁用)
// 逻辑:已安装 < 推荐 -> 更新
const comp = compareVersions(currentVersion, recommendedVersion);
if (comp >= 0) {
displayStatus = task ? task.status : 'installed';
actionLabel = '已安装';
targetVersion = undefined; // 禁用安装
} else {
actionLabel = '更新';
targetVersion = recommendedVersion;
}
} else {
actionLabel = '安装';
targetVersion = recommendedVersion || availableVersion;
}
return {
...item,
version: currentVersion,
recommended_version: recommendedVersion,
available_version: availableVersion,
status: displayStatus,
actionLabel
progress: task ? task.progress : 0,
actionLabel,
targetVersion // 传递给视图,用于点击安装
};
});
},
sortedUpdates: (state) => [...state.updates].sort(sortByName),
sortedUpdates: (state) => {
return [...state.updates].map(item => {
const task = state.activeTasks[item.id];
return {
...item,
status: task ? task.status : 'idle',
progress: task ? task.progress : 0,
actionLabel: '更新',
targetVersion: item.available_version // 更新页面永远追求最新版
};
}).sort(sortByName);
},
isBusy: (state) => {
const allItems = [...state.essentials, ...state.updates, ...state.allSoftware];
return allItems.some(item => item.status === 'pending' || item.status === 'installing');
return state.loading || Object.values(state.activeTasks).some(task =>
task.status === 'pending' || task.status === 'installing'
);
}
},
actions: {
// ... (initializeApp, saveSettings, syncEssentials stay the same)
async initializeApp() {
if (this.isInitialized) return;
this.initStatus = '正在加载应用配置...';
@@ -98,7 +143,6 @@ export const useSoftwareStore = defineStore('software', {
}
},
// ... (Selection methods stay the same)
toggleSelection(id: string, type: 'essential' | 'update') {
if (this.isBusy) return;
const list = type === 'essential' ? this.selectedEssentialIds : this.selectedUpdateIds;
@@ -108,7 +152,7 @@ export const useSoftwareStore = defineStore('software', {
},
selectAll(type: 'essential' | 'update') {
if (type === 'essential') {
const selectable = this.mergedEssentials.filter(s => s.status !== 'installed');
const selectable = this.mergedEssentials.filter(s => s.actionLabel !== '已安装');
this.selectedEssentialIds = selectable.map(s => s.id);
} else {
this.selectedUpdateIds = this.updates.map(s => s.id);
@@ -120,7 +164,7 @@ export const useSoftwareStore = defineStore('software', {
},
invertSelection(type: 'essential' | 'update') {
if (type === 'essential') {
const selectable = this.mergedEssentials.filter(s => s.status !== 'installed').map(s => s.id);
const selectable = this.mergedEssentials.filter(s => s.actionLabel !== '已安装').map(s => s.id);
this.selectedEssentialIds = selectable.filter(id => !this.selectedEssentialIds.includes(id));
} else {
const selectable = this.updates.map(s => s.id);
@@ -130,8 +174,6 @@ export const useSoftwareStore = defineStore('software', {
async fetchEssentials() {
let repo = await invoke('get_essentials') as any;
// 如果本地没有文件,则尝试联网获取一次
if (!repo) {
try {
await invoke('sync_essentials');
@@ -140,7 +182,6 @@ export const useSoftwareStore = defineStore('software', {
console.error('Initial sync failed:', err);
}
}
if (repo) {
this.essentials = repo.essentials;
this.essentialsVersion = repo.version;
@@ -173,15 +214,11 @@ export const useSoftwareStore = defineStore('software', {
async fetchAllData() {
this.loading = true;
try {
// 先确保加载了必备清单(内部处理本地缺失逻辑)
await this.fetchEssentials();
// 然后同步本地软件安装/更新状态,不再强制联网下载 JSON
const [all, updates] = await Promise.all([
invoke('get_installed_software'),
invoke('get_updates')
]);
this.allSoftware = all as any[];
this.updates = updates as any[];
this.lastFetched = Date.now();
@@ -190,13 +227,50 @@ export const useSoftwareStore = defineStore('software', {
this.loading = false;
}
},
async install(id: string) {
async install(id: string, targetVersion?: string) {
const software = this.findSoftware(id)
if (software) {
software.status = 'pending';
await invoke('install_software', { id, version: software.version })
this.activeTasks[id] = { status: 'pending', progress: 0, targetVersion };
try {
await invoke('install_software', {
task: {
id,
version: targetVersion,
use_manifest: software.use_manifest || false,
manifest_url: software.manifest_url || null
}
})
} catch (err) {
console.error('Invoke install failed:', err);
this.activeTasks[id] = { status: 'error', progress: 0 };
}
}
},
// 注册批量任务
startBatch(ids: string[]) {
this.batchQueue = [...ids];
},
// 引入防抖刷新:仅在批量任务全部处理完后的 2 秒执行全量扫描
scheduleDataRefresh() {
if (this.refreshTimer) clearTimeout(this.refreshTimer);
this.refreshTimer = setTimeout(async () => {
await this.fetchAllData();
// 刷新完成后清理所有已终结(成功或失败)的任务状态快照
Object.keys(this.activeTasks).forEach(id => {
const status = this.activeTasks[id].status;
if (status === 'success' || status === 'error') {
delete this.activeTasks[id];
}
});
this.refreshTimer = null;
}, 2000);
},
findSoftware(id: string) {
return this.essentials.find(s => s.id === id) ||
this.updates.find(s => s.id === id) ||
@@ -208,33 +282,37 @@ export const useSoftwareStore = defineStore('software', {
listen('install-status', (event: any) => {
const { id, status, progress } = event.payload
const software = this.findSoftware(id)
if (software) {
software.status = status
software.progress = progress
}
const task = this.activeTasks[id];
this.activeTasks[id] = { status, progress, targetVersion: task?.targetVersion };
// 当任务达到终态(成功或失败)时
if (status === 'success' || status === 'error') {
if (status === 'success') {
this.lastFetched = 0;
// 立即更新勾选状态,提升响应感
this.selectedEssentialIds = this.selectedEssentialIds.filter(i => i !== id);
this.selectedUpdateIds = this.selectedUpdateIds.filter(i => i !== id);
}
// 检查是否属于正在进行的批量任务
const index = this.batchQueue.indexOf(id);
if (index !== -1) {
this.batchQueue.splice(index, 1);
// 如果这是批量任务中的最后一个,则触发延迟刷新
if (this.batchQueue.length === 0) {
this.scheduleDataRefresh();
}
}
}
})
// 日志监听:根据 ID 追加内容
listen('log-event', (event: any) => {
const payload = event.payload as LogEntry;
const existingLog = this.logs.find(l => l.id === payload.id);
if (existingLog) {
// 如果是增量更新
if (payload.output) {
existingLog.output += '\n' + payload.output;
}
if (payload.status !== 'info') {
existingLog.status = payload.status;
}
if (payload.output) existingLog.output += '\n' + payload.output;
if (payload.status !== 'info') existingLog.status = payload.status;
} else {
// 如果是新日志
this.logs.unshift(payload);
if (this.logs.length > 100) this.logs.pop();
}

View File

@@ -58,6 +58,7 @@
:action-label="item.actionLabel"
:selectable="true"
:is-selected="store.selectedEssentialIds.includes(item.id)"
:disabled="store.isBusy"
@install="store.install"
@toggle-select="id => store.toggleSelection(id, 'essential')"
/>
@@ -77,8 +78,13 @@ const selectableItems = computed(() => {
});
const installSelected = () => {
store.selectedEssentialIds.forEach(id => {
store.install(id);
const ids = [...store.selectedEssentialIds];
store.startBatch(ids);
ids.forEach(id => {
const item = store.mergedEssentials.find(s => s.id === id);
if (item) {
store.install(id, item.targetVersion);
}
});
};

View File

@@ -62,6 +62,7 @@
action-label="更新"
:selectable="true"
:is-selected="store.selectedUpdateIds.includes(item.id)"
:disabled="store.isBusy"
@install="store.install"
@toggle-select="id => store.toggleSelection(id, 'update')"
/>
@@ -77,8 +78,13 @@ import { onMounted } from 'vue';
const store = useSoftwareStore();
const updateSelected = () => {
store.selectedUpdateIds.forEach(id => {
store.install(id);
const ids = [...store.selectedUpdateIds];
store.startBatch(ids);
ids.forEach(id => {
const item = store.sortedUpdates.find(s => s.id === id);
if (item && item.targetVersion) {
store.install(id, item.targetVersion);
}
});
};