Compare commits
14 Commits
5717b94c90
...
46c622fd86
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46c622fd86 | ||
|
|
9cdc371c75 | ||
|
|
517ee39707 | ||
|
|
61caeba242 | ||
|
|
6dde1ea9a7 | ||
|
|
d775e049d6 | ||
|
|
145dad23a5 | ||
|
|
7e550a8d49 | ||
|
|
cf740b9e3a | ||
|
|
b6248bec45 | ||
|
|
50489bb9d4 | ||
|
|
6a360dc14b | ||
|
|
7f2dde8c51 | ||
|
|
a7b5955540 |
@@ -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),
|
||||
};
|
||||
emit_log(&handle, &log_id, &display_cmd, "Starting...", "info");
|
||||
let mut args = vec!["install".to_string()];
|
||||
let display_cmd: String;
|
||||
let mut temp_manifest_path: Option<PathBuf> = None;
|
||||
|
||||
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(),
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 v-if="software.available_version && !software.recommended_version" class="version-tag available">
|
||||
最新: {{ software.available_version }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- 情况 3: WinGet 检测到的仓库最新版本 -->
|
||||
<span class="version-tag available" v-if="software.available_version">
|
||||
最新: {{ software.available_version }}
|
||||
</span>
|
||||
<!-- 情况 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;
|
||||
|
||||
@@ -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,
|
||||
status: displayStatus,
|
||||
actionLabel
|
||||
available_version: availableVersion,
|
||||
status: displayStatus,
|
||||
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
|
||||
}
|
||||
if (status === 'success') {
|
||||
this.lastFetched = 0;
|
||||
this.selectedEssentialIds = this.selectedEssentialIds.filter(i => i !== id);
|
||||
this.selectedUpdateIds = this.selectedUpdateIds.filter(i => i !== id);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user