feat(install): 实现带进度条的游戏安装功能

- 添加进度条组件和相关状态管理
- 实现 Rust 后端解压过程的实时进度上报
- 增加安装路径中文字符校验
-优化日志显示支持时间戳开关- 引入 zip 解压库及异步运行时依赖
- 修复安装流程中的事件通信机制
- 调整 UI 布局适配新增进度条显示
This commit is contained in:
Chaos
2025-11-12 17:23:53 +08:00
parent 4773d3615b
commit 2cb5c42957
5 changed files with 515 additions and 47 deletions

View File

@@ -0,0 +1,149 @@
// 只在 Windows 编译
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// --- 1. 导入所需模块 ---
use thiserror::Error;
use zip::ZipArchive;
use std::fs::{self, File};
use std::io;
use std::path::PathBuf;
use serde::Serialize;
use tauri::Emitter;
use tokio::time::{sleep, Duration};
// --- 2. (新增) 定义事件的有效负载 (Payload) ---
// 这个结构体将作为事件发送给 Svelte
#[derive(Clone, serde::Serialize)]
struct InstallProgress {
message: String,
percentage: f32, // 0.0 到 100.0
}
// --- 3. 错误定义 ---
#[derive(Debug, Error)]
pub enum InstallError {
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error),
#[error("Zip 解压错误: {0}")]
Zip(#[from] zip::result::ZipError),
#[error("找不到 data.bin 文件: {0}")]
DataFileNotFound(String),
}
impl Serialize for InstallError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
#[tauri::command]
// --- 5. (修改) 添加 window 参数 ---
pub async fn install_game(window: tauri::Window, target_dir: String) -> Result<String, InstallError> {
println!("Rust 后端收到命令:目标目录 {}", target_dir);
// --- 6. 核心改动:从文件系统读取 data.bin ---
// --- 6a. (新增) 发送启动事件 ---
window.emit("install_progress", InstallProgress {
message: "安装开始...".into(),
percentage: 0.0
}).ok();
// 1. 获取 app.exe 所在的目录
let exe_path = std::env::current_exe()?;
let exe_dir = exe_path.parent()
.ok_or_else(|| InstallError::Io(io::Error::new(io::ErrorKind::NotFound, "无法获取执行文件所在目录")))?;
// 2. 构造 data.bin 的路径
let data_bin_path = exe_dir.join("data.bin");
println!("正在查找数据文件: {:?}", data_bin_path);
window.emit("install_progress", InstallProgress {
message: format!("正在查找数据文件: {:?}", data_bin_path),
percentage: 1.0 // 1%
}).ok();
if !data_bin_path.exists() {
// 返回错误Svelte 的 catch 块会捕获它
return Err(InstallError::DataFileNotFound(
format!("未在 {:?} 找到 data.bin", data_bin_path)
));
}
// 3. 从硬盘打开文件
let archive_file = File::open(&data_bin_path)?;
println!("成功打开 data.bin。");
window.emit("install_progress", InstallProgress {
message: "成功打开 data.bin。".into(),
percentage: 5.0 // 5%
}).ok();
// 4. ZipArchive 现在读取文件
let mut archive = ZipArchive::new(archive_file)?;
let total_files = archive.len() as f32;
println!("在压缩包中找到 {} 个文件。", total_files);
window.emit("install_progress", InstallProgress {
message: format!("包中找到 {} 个文件。", total_files),
percentage: 10.0 // 10%
}).ok();
// --- 7. 解压循环 (核心改动: 添加 emit) ---
let install_path = PathBuf::from(target_dir);
fs::create_dir_all(&install_path)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = match file.enclosed_name() {
Some(path) => install_path.join(path),
None => continue,
};
// --- 7a. (核心) 计算百分比并发送事件 ---
let current_file_index = i as f32;
let percentage = 10.0 + (current_file_index / total_files) * 85.0; // (10% + 85% = 95%)
let message = format!("安装中 [{}/{}]: {}", i + 1, total_files as u32, file.name());
window.emit("install_progress", InstallProgress { message, percentage })
.ok();
// --- 2. (新增) 关键修复 ---
// 释放线程 1 毫秒,给 Svelte 留出渲染时间
sleep(Duration::from_millis(1)).await;
if (&*file.name()).ends_with('/') {
fs::create_dir_all(&outpath)?;
} else {
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(p)?;
}
}
let mut outfile = File::create(&outpath)?;
io::copy(&mut file, &mut outfile)?;
}
} // --- 循环结束 ---
println!("Rust 后端:解压成功。");
window.emit("install_progress", InstallProgress {
message: "✅ 解压完成。".into(),
percentage: 100.0 // 100%
}).ok();
Ok(format!("成功解压 {} 个文件到指定目录!", total_files as u32))
}