feat: 预制体系统与架构改进 (#303)
* feat(prefab): 实现预制体系统和编辑器 UX 改进 ## 预制体系统 - 新增 PrefabSerializer: 预制体序列化/反序列化 - 新增 PrefabInstanceComponent: 追踪预制体实例来源和修改 - 新增 PrefabService: 预制体核心服务 - 新增 PrefabLoader: 预制体资产加载器 - 新增预制体命令: Create/Instantiate/Apply/Revert/BreakLink ## 预制体编辑模式 - 支持双击 .prefab 文件进入编辑模式 - 预制体编辑模式工具栏 (保存/退出) - 预制体实例指示器和操作菜单 ## 编辑器 UX 改进 - SceneHierarchy 快捷键: F2 重命名, Ctrl+D 复制, ↑↓ 导航 - 支持双击实体名称内联编辑 - 删除实体时显示子节点数量警告 - 右键菜单添加重命名/复制选项及快捷键提示 - 布局持久化和重置功能 ## Bug 修复 - 修复 editor-runtime 组件类重复导致的 TransformComponent 不识别问题 - 修复 .prefab-name 样式覆盖导致预制体工具栏文字不可见 - 修复 Inspector 资源字段高度不正确问题 * feat(editor): 改进编辑器 UX 交互体验 - ContentBrowser: 加载动画 spinner、搜索高亮、改进空状态设计 - SceneHierarchy: 选中项自动滚动到视图、搜索清除按钮 - PropertyInspector: 输入框本地状态管理、Enter/Escape 键处理 - EntityInspector: 组件折叠状态持久化、属性搜索清除按钮 - Viewport: 变换操作实时数值显示 - 国际化: 添加相关文本 (en/zh) * fix(build): 修复 Web 构建资产加载和编辑器 UX 改进 构建系统修复: - 修复 asset-catalog.json 字段名不匹配 (entries vs assets) - 修复 BrowserFileSystemService 支持两种目录格式 - 修复 bundle 策略检测逻辑 (空对象判断) - 修复 module.json 中 assetExtensions 声明和类型推断 行为树修复: - 修复 BehaviorTreeExecutionSystem 使用 loadAsset 替代 loadAssetByPath - 修复 BehaviorTreeAssetType 常量与 module.json 类型名一致 (behavior-tree) 编辑器 UX 改进: - 构建完成对话框添加"打开文件夹"按钮 - 构建完成对话框样式优化 (圆形图标背景、按钮布局) - SceneHierarchy 响应式布局 (窄窗口自动隐藏 Type 列) - SceneHierarchy 隐藏滚动条 错误追踪: - 添加全局错误处理器写入日志文件 (%TEMP%/esengine-editor-crash.log) - 添加 append_to_log Tauri 命令 * feat(render): 修复 UI 渲染和点击特效系统 ## UI 渲染修复 - 修复 GUID 验证 bug,使用统一的 isValidGUID() 函数 - 修复 UI 渲染顺序随机问题,Rust 端使用 IndexMap 替代 HashMap - Web 运行时添加 assetPathResolver 支持 GUID 解析 - UIInteractableComponent.blockEvents 默认值改为 false ## 点击特效系统 - 新增 ClickFxComponent 和 ClickFxSystem - 支持在点击位置播放粒子效果 - 支持多种触发模式和粒子轮换 ## Camera 系统重构 - CameraSystem 从 ecs-engine-bindgen 移至 camera 包 - 新增 CameraManager 统一管理相机 ## 编辑器改进 - 改进属性面板 UI 交互 - 粒子编辑器面板优化 - Transform 命令系统 * feat(render): 实现 Sorting Layer 系统和 Overlay 渲染层 - 新增 SortingLayerManager 管理排序层级 (Background, Default, Foreground, UI, Overlay) - 实现 ISortable 接口,统一 Sprite、UI、Particle 的排序属性 - 修复粒子 Overlay 层被 UI 遮挡问题:添加独立的 Overlay Pass 在 UI 之后渲染 - 更新粒子资产格式:从 sortingOrder 改为 sortingLayer + orderInLayer - 更新粒子编辑器面板支持新的排序属性 - 优化 UI 渲染系统使用新的排序层级 * feat(ci): 集成 SignPath 代码签名服务 - 添加 SignPath 自动签名工作流(Windows) - 配置 release-editor.yml 支持代码签名 - 将构建改为草稿模式,等待签名完成后发布 - 添加证书文件到 .gitignore 防止泄露 * fix(asset): 修复 Web 构建资产路径解析和全局单例移除 ## 资产路径修复 - 修复 Tauri 本地服务器 `/asset?path=...` 路径解析,正确与 root 目录连接 - BrowserPathResolver 支持两种模式: - 'proxy': 使用 /asset?path=... 格式(编辑器 Run in Browser) - 'direct': 使用直接路径 /assets/path.png(独立 Web 构建) - BrowserRuntime 使用 'direct' 模式,无需 Tauri 代理 ## 架构改进 - 移除全局单例 - 移除 globalAssetManager 导出,改用 AssetManagerToken 依赖注入 - 移除 globalPathResolver 导出,改用 PathResolutionService - 移除 globalPathResolutionService 导出 - ParticleUpdateSystem/ClickFxSystem 通过 setAssetManager() 注入依赖 - EngineService 使用 new AssetManager() 替代全局实例 ## 新增服务 - PathResolutionService: 统一路径解析接口 - RuntimeModeService: 运行时模式查询服务 - SerializationContext: EntityRef 序列化上下文 ## 其他改进 - 完善 ServiceToken 注释说明本地定义的意图 - 导出 BrowserPathResolveMode 类型 * fix(build): 添加 world-streaming composite 设置修复类型检查 * fix(build): 移除 world-streaming 引用避免 composite 冲突 * fix(build): 将 const enum 改为 enum 兼容 isolatedModules * fix(build): 添加缺失的 IAssetManager 导入
This commit is contained in:
@@ -106,6 +106,53 @@ pub struct FileChangeEvent {
|
||||
pub paths: Vec<String>,
|
||||
}
|
||||
|
||||
/// Install esbuild globally using npm.
|
||||
/// 使用 npm 全局安装 esbuild。
|
||||
///
|
||||
/// # Returns | 返回
|
||||
/// Progress messages as the installation proceeds.
|
||||
/// 安装过程中的进度消息。
|
||||
#[command]
|
||||
pub async fn install_esbuild(app: AppHandle) -> Result<(), String> {
|
||||
println!("[Environment] Starting esbuild installation...");
|
||||
|
||||
// Emit progress event | 发送进度事件
|
||||
let _ = app.emit("esbuild-install:progress", "Checking npm...");
|
||||
|
||||
// Check if npm is available | 检查 npm 是否可用
|
||||
let npm_cmd = if cfg!(windows) { "npm.cmd" } else { "npm" };
|
||||
let npm_check = Command::new(npm_cmd)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map_err(|_| "npm not found. Please install Node.js first. | 未找到 npm,请先安装 Node.js。".to_string())?;
|
||||
|
||||
if !npm_check.status.success() {
|
||||
return Err("npm not working properly. | npm 无法正常工作。".to_string());
|
||||
}
|
||||
|
||||
let _ = app.emit("esbuild-install:progress", "Installing esbuild globally...");
|
||||
println!("[Environment] Running: npm install -g esbuild");
|
||||
|
||||
// Install esbuild globally | 全局安装 esbuild
|
||||
let output = Command::new(npm_cmd)
|
||||
.args(&["install", "-g", "esbuild"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run npm install | npm install 执行失败: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
println!("[Environment] esbuild installed successfully");
|
||||
let _ = app.emit("esbuild-install:progress", "Installation complete!");
|
||||
let _ = app.emit("esbuild-install:success", true);
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let error_msg = format!("Failed to install esbuild | 安装 esbuild 失败: {}", stderr);
|
||||
println!("[Environment] {}", error_msg);
|
||||
let _ = app.emit("esbuild-install:error", &error_msg);
|
||||
Err(error_msg)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check development environment.
|
||||
/// 检测开发环境。
|
||||
///
|
||||
@@ -123,27 +170,12 @@ pub async fn check_environment() -> Result<EnvironmentCheckResult, String> {
|
||||
|
||||
/// Check esbuild availability and get its status.
|
||||
/// 检查 esbuild 可用性并获取其状态。
|
||||
///
|
||||
/// Only checks for globally installed esbuild (via npm -g).
|
||||
/// 只检测通过 npm 全局安装的 esbuild。
|
||||
fn check_esbuild_status() -> ToolStatus {
|
||||
// Try bundled esbuild first | 首先尝试打包的 esbuild
|
||||
if let Some(bundled_path) = find_bundled_esbuild() {
|
||||
match get_esbuild_version(&bundled_path) {
|
||||
Ok(version) => {
|
||||
return ToolStatus {
|
||||
available: true,
|
||||
version: Some(version),
|
||||
path: Some(bundled_path),
|
||||
source: Some("bundled".to_string()),
|
||||
error: None,
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[Environment] Bundled esbuild found but failed to get version: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try global esbuild | 尝试全局 esbuild
|
||||
let global_esbuild = if cfg!(windows) { "esbuild.cmd" } else { "esbuild" };
|
||||
|
||||
match get_esbuild_version(global_esbuild) {
|
||||
Ok(version) => {
|
||||
ToolStatus {
|
||||
@@ -160,7 +192,7 @@ fn check_esbuild_status() -> ToolStatus {
|
||||
version: None,
|
||||
path: None,
|
||||
source: None,
|
||||
error: Some("esbuild not found | 未找到 esbuild".to_string()),
|
||||
error: Some("esbuild not installed globally. Please install: npm install -g esbuild | 未全局安装 esbuild,请安装: npm install -g esbuild".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -436,6 +468,204 @@ pub async fn watch_scripts(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Watch for file changes in asset directories.
|
||||
/// 监视资产目录中的文件变更。
|
||||
///
|
||||
/// Watches multiple directories (assets, scenes, etc.) for all file types.
|
||||
/// 监视多个目录(assets, scenes 等)中的所有文件类型。
|
||||
///
|
||||
/// Emits "user-code:file-changed" events when files change.
|
||||
/// 当文件发生变更时触发 "user-code:file-changed" 事件。
|
||||
#[command]
|
||||
pub async fn watch_assets(
|
||||
app: AppHandle,
|
||||
watcher_state: State<'_, ScriptWatcherState>,
|
||||
project_path: String,
|
||||
directories: Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
// Create a unique key for this watcher set | 为此监视器集创建唯一键
|
||||
let watcher_key = format!("{}/assets", project_path);
|
||||
|
||||
// Check if already watching | 检查是否已在监视
|
||||
{
|
||||
let watchers = watcher_state.watchers.lock().await;
|
||||
if watchers.contains_key(&watcher_key) {
|
||||
println!("[AssetWatcher] Already watching: {}", watcher_key);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Validate directories exist | 验证目录是否存在
|
||||
let mut watch_paths = Vec::new();
|
||||
for dir in &directories {
|
||||
let watch_path = Path::new(&project_path).join(dir);
|
||||
if watch_path.exists() {
|
||||
watch_paths.push((watch_path, dir.clone()));
|
||||
} else {
|
||||
println!("[AssetWatcher] Directory does not exist, skipping: {}", watch_path.display());
|
||||
}
|
||||
}
|
||||
|
||||
if watch_paths.is_empty() {
|
||||
return Err("No valid directories to watch | 没有有效的目录可监视".to_string());
|
||||
}
|
||||
|
||||
// Create a channel for shutdown signal | 创建关闭信号通道
|
||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
|
||||
// Clone values for the spawned task | 克隆值以供任务使用
|
||||
let project_path_clone = project_path.clone();
|
||||
let app_clone = app.clone();
|
||||
|
||||
// Spawn file watcher task | 启动文件监视任务
|
||||
tokio::spawn(async move {
|
||||
// Create notify watcher | 创建 notify 监视器
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let mut watcher = match RecommendedWatcher::new(
|
||||
move |res: Result<Event, notify::Error>| {
|
||||
if let Ok(event) = res {
|
||||
let _ = tx.send(event);
|
||||
}
|
||||
},
|
||||
Config::default().with_poll_interval(Duration::from_millis(500)),
|
||||
) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
eprintln!("[AssetWatcher] Failed to create watcher: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Start watching all directories | 开始监视所有目录
|
||||
for (path, dir_name) in &watch_paths {
|
||||
if let Err(e) = watcher.watch(path, RecursiveMode::Recursive) {
|
||||
eprintln!("[AssetWatcher] Failed to watch {}: {}", dir_name, e);
|
||||
} else {
|
||||
println!("[AssetWatcher] Started watching: {}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
// Asset file extensions to monitor | 要监视的资产文件扩展名
|
||||
let asset_extensions: std::collections::HashSet<&str> = [
|
||||
// Images
|
||||
"png", "jpg", "jpeg", "webp", "gif", "bmp", "svg",
|
||||
// Audio
|
||||
"mp3", "ogg", "wav", "flac", "m4a",
|
||||
// Data formats
|
||||
"json", "xml", "yaml", "yml", "txt",
|
||||
// Custom asset types
|
||||
"prefab", "ecs", "btree", "particle", "tmx", "tsx",
|
||||
// Scripts (also watch these in assets dir)
|
||||
"ts", "tsx", "js", "jsx",
|
||||
// Materials and shaders
|
||||
"mat", "shader", "glsl", "vert", "frag",
|
||||
// Fonts
|
||||
"ttf", "otf", "woff", "woff2",
|
||||
// 3D assets
|
||||
"gltf", "glb", "obj", "fbx",
|
||||
].into_iter().collect();
|
||||
|
||||
// Debounce state | 防抖状态
|
||||
let mut pending_events: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
||||
let mut last_event_time = std::time::Instant::now();
|
||||
let debounce_duration = Duration::from_millis(300);
|
||||
|
||||
// Event loop | 事件循环
|
||||
loop {
|
||||
// Check for shutdown | 检查关闭信号
|
||||
if shutdown_rx.try_recv().is_ok() {
|
||||
println!("[AssetWatcher] Stopping watcher for: {}", project_path_clone);
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for file events with timeout | 带超时检查文件事件
|
||||
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(event) => {
|
||||
// Filter for asset files | 过滤资产文件
|
||||
let valid_paths: Vec<(String, String)> = event
|
||||
.paths
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
// Skip .meta files | 跳过 .meta 文件
|
||||
if p.to_string_lossy().ends_with(".meta") {
|
||||
return false;
|
||||
}
|
||||
// Check extension | 检查扩展名
|
||||
let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
asset_extensions.contains(ext.to_lowercase().as_str())
|
||||
})
|
||||
.map(|p| {
|
||||
let path_str = p.to_string_lossy().to_string();
|
||||
let change_type = match event.kind {
|
||||
EventKind::Create(_) => "create",
|
||||
EventKind::Modify(_) => "modify",
|
||||
EventKind::Remove(_) => "remove",
|
||||
_ => "modify",
|
||||
};
|
||||
(path_str, change_type.to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !valid_paths.is_empty() {
|
||||
// Only handle create/modify/remove events | 只处理创建/修改/删除事件
|
||||
match event.kind {
|
||||
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {
|
||||
for (path, change_type) in valid_paths {
|
||||
pending_events.insert(path, change_type);
|
||||
}
|
||||
last_event_time = std::time::Instant::now();
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
// Check if we should emit pending events (debounce) | 检查是否应该发送待处理事件(防抖)
|
||||
if !pending_events.is_empty() && last_event_time.elapsed() >= debounce_duration {
|
||||
// Group by change type | 按变更类型分组
|
||||
let mut by_type: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
|
||||
for (path, change_type) in pending_events.drain() {
|
||||
by_type.entry(change_type).or_default().push(path);
|
||||
}
|
||||
|
||||
// Emit events for each type | 为每种类型发送事件
|
||||
for (change_type, paths) in by_type {
|
||||
let file_event = FileChangeEvent {
|
||||
change_type,
|
||||
paths,
|
||||
};
|
||||
|
||||
println!("[AssetWatcher] File change detected (debounced): {:?}", file_event);
|
||||
|
||||
// Emit event to frontend | 向前端发送事件
|
||||
if let Err(e) = app_clone.emit("user-code:file-changed", file_event) {
|
||||
eprintln!("[AssetWatcher] Failed to emit event: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
|
||||
println!("[AssetWatcher] Watcher channel disconnected");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Store watcher handle | 存储监视器句柄
|
||||
{
|
||||
let mut watchers = watcher_state.watchers.lock().await;
|
||||
watchers.insert(
|
||||
watcher_key.clone(),
|
||||
crate::state::WatcherHandle { shutdown_tx },
|
||||
);
|
||||
}
|
||||
|
||||
println!("[AssetWatcher] Watch started for directories: {:?}", directories);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop watching for file changes.
|
||||
/// 停止监视文件变更。
|
||||
#[command]
|
||||
@@ -468,32 +698,9 @@ pub async fn stop_watch_scripts(
|
||||
/// Find esbuild executable path.
|
||||
/// 查找 esbuild 可执行文件路径。
|
||||
///
|
||||
/// Search order | 搜索顺序:
|
||||
/// 1. Bundled esbuild in app resources | 应用资源中打包的 esbuild
|
||||
/// 2. Local node_modules | 本地 node_modules
|
||||
/// 3. Global esbuild | 全局 esbuild
|
||||
fn find_esbuild(project_root: &str) -> Result<String, String> {
|
||||
let project_path = Path::new(project_root);
|
||||
|
||||
// Try bundled esbuild first (in app resources) | 首先尝试打包的 esbuild(在应用资源中)
|
||||
if let Some(bundled) = find_bundled_esbuild() {
|
||||
println!("[Compiler] Using bundled esbuild: {}", bundled);
|
||||
return Ok(bundled);
|
||||
}
|
||||
|
||||
// Try local node_modules | 尝试本地 node_modules
|
||||
let local_esbuild = if cfg!(windows) {
|
||||
project_path.join("node_modules/.bin/esbuild.cmd")
|
||||
} else {
|
||||
project_path.join("node_modules/.bin/esbuild")
|
||||
};
|
||||
|
||||
if local_esbuild.exists() {
|
||||
println!("[Compiler] Using local esbuild: {}", local_esbuild.display());
|
||||
return Ok(local_esbuild.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
// Try global esbuild | 尝试全局 esbuild
|
||||
/// Only uses globally installed esbuild (npm -g).
|
||||
/// 只使用全局安装的 esbuild (npm -g)。
|
||||
fn find_esbuild(_project_root: &str) -> Result<String, String> {
|
||||
let global_esbuild = if cfg!(windows) { "esbuild.cmd" } else { "esbuild" };
|
||||
|
||||
// Check if global esbuild exists | 检查全局 esbuild 是否存在
|
||||
@@ -506,47 +713,10 @@ fn find_esbuild(project_root: &str) -> Result<String, String> {
|
||||
println!("[Compiler] Using global esbuild");
|
||||
Ok(global_esbuild.to_string())
|
||||
},
|
||||
_ => Err("esbuild not found. Please install esbuild: npm install -g esbuild | 未找到 esbuild,请安装: npm install -g esbuild".to_string())
|
||||
_ => Err("esbuild not installed globally. Please install: npm install -g esbuild | 未全局安装 esbuild,请安装: npm install -g esbuild".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Find bundled esbuild in app resources.
|
||||
/// 在应用资源中查找打包的 esbuild。
|
||||
fn find_bundled_esbuild() -> Option<String> {
|
||||
// Get the executable path | 获取可执行文件路径
|
||||
let exe_path = std::env::current_exe().ok()?;
|
||||
let exe_dir = exe_path.parent()?;
|
||||
|
||||
// In development, resources are in src-tauri directory | 开发模式下,资源在 src-tauri 目录
|
||||
// In production, resources are next to the executable | 生产模式下,资源在可执行文件旁边
|
||||
let esbuild_name = if cfg!(windows) { "esbuild.exe" } else { "esbuild" };
|
||||
|
||||
// Try production path (resources next to exe) | 尝试生产路径(资源在 exe 旁边)
|
||||
let prod_path = exe_dir.join("bin").join(esbuild_name);
|
||||
if prod_path.exists() {
|
||||
return Some(prod_path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
// Try development path (in src-tauri/bin) | 尝试开发路径(在 src-tauri/bin 中)
|
||||
// This handles running via `cargo tauri dev`
|
||||
let dev_path = exe_dir
|
||||
.ancestors()
|
||||
.find_map(|p| {
|
||||
let candidate = p.join("src-tauri").join("bin").join(esbuild_name);
|
||||
if candidate.exists() {
|
||||
Some(candidate)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(path) = dev_path {
|
||||
return Some(path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse esbuild error output.
|
||||
/// 解析 esbuild 错误输出。
|
||||
fn parse_esbuild_errors(stderr: &str) -> Vec<CompileError> {
|
||||
|
||||
@@ -65,11 +65,13 @@ pub async fn open_file_dialog(
|
||||
}
|
||||
|
||||
/// Save file dialog (generic)
|
||||
/// 通用保存文件对话框
|
||||
#[tauri::command]
|
||||
pub async fn save_file_dialog(
|
||||
app: AppHandle,
|
||||
title: Option<String>,
|
||||
default_name: Option<String>,
|
||||
default_path: Option<String>,
|
||||
filters: Option<Vec<FileFilter>>,
|
||||
) -> Result<Option<String>, String> {
|
||||
let mut dialog = app.dialog().file();
|
||||
@@ -80,6 +82,14 @@ pub async fn save_file_dialog(
|
||||
dialog = dialog.set_title("Save File");
|
||||
}
|
||||
|
||||
// Set default directory | 设置默认目录
|
||||
if let Some(path) = default_path {
|
||||
let path_buf = std::path::PathBuf::from(&path);
|
||||
if path_buf.exists() {
|
||||
dialog = dialog.set_directory(&path_buf);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = default_name {
|
||||
dialog = dialog.set_file_name(&name);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,31 @@ pub fn read_file_content(path: String) -> Result<String, String> {
|
||||
.map_err(|e| format!("Failed to read file {}: {}", path, e))
|
||||
}
|
||||
|
||||
/// Append text to log file (auto-creates parent directories)
|
||||
/// 追加文本到日志文件(自动创建父目录)
|
||||
#[tauri::command]
|
||||
pub fn append_to_log(path: String, content: String) -> Result<(), String> {
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = Path::new(&path).parent() {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create directory {}: {}", parent.display(), e))?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.map_err(|e| format!("Failed to open log file {}: {}", path, e))?;
|
||||
|
||||
writeln!(file, "{}", content)
|
||||
.map_err(|e| format!("Failed to write to log file {}: {}", path, e))
|
||||
}
|
||||
|
||||
/// Write text content to file (auto-creates parent directories)
|
||||
#[tauri::command]
|
||||
pub fn write_file_content(path: String, content: String) -> Result<(), String> {
|
||||
|
||||
@@ -72,6 +72,38 @@ pub fn open_file_with_default_app(file_path: String) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open folder in system file explorer
|
||||
/// 在系统文件管理器中打开文件夹
|
||||
#[tauri::command]
|
||||
pub fn open_folder(path: String) -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let normalized_path = path.replace('/', "\\");
|
||||
Command::new("explorer")
|
||||
.arg(&normalized_path)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open folder: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Command::new("open")
|
||||
.arg(&path)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open folder: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Command::new("xdg-open")
|
||||
.arg(&path)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open folder: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Show file in system file explorer
|
||||
#[tauri::command]
|
||||
pub fn show_in_folder(file_path: String) -> Result<(), String> {
|
||||
@@ -344,7 +376,6 @@ pub fn get_current_dir() -> Result<String, String> {
|
||||
/// 扫描 dist/engine/ 目录,为所有有 .d.ts 文件的模块添加路径。
|
||||
#[tauri::command]
|
||||
pub fn update_project_tsconfig(app: AppHandle, project_path: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let project = Path::new(&project_path);
|
||||
@@ -558,11 +589,18 @@ pub fn start_local_server(root_path: String, port: u16) -> Result<String, String
|
||||
// Handle different request types
|
||||
let file_path = if url.starts_with("/asset?path=") {
|
||||
// Asset proxy - extract and decode path parameter
|
||||
// 资产代理 - 提取并解码路径参数
|
||||
let query = &url[7..]; // Skip "/asset?"
|
||||
if let Some(path_value) = query.strip_prefix("path=") {
|
||||
urlencoding::decode(path_value)
|
||||
let decoded = urlencoding::decode(path_value)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
// Normalize path: remove ./ prefix and join with root
|
||||
// 规范化路径:移除 ./ 前缀并与根目录连接
|
||||
let normalized = decoded.trim_start_matches("./");
|
||||
PathBuf::from(&root).join(normalized)
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ fn main() {
|
||||
commands::read_file_content,
|
||||
commands::write_file_content,
|
||||
commands::write_binary_file,
|
||||
commands::append_to_log,
|
||||
commands::path_exists,
|
||||
commands::create_directory,
|
||||
commands::create_file,
|
||||
@@ -79,6 +80,7 @@ fn main() {
|
||||
// System operations
|
||||
commands::toggle_devtools,
|
||||
commands::open_file_with_default_app,
|
||||
commands::open_folder,
|
||||
commands::show_in_folder,
|
||||
commands::get_temp_dir,
|
||||
commands::open_with_editor,
|
||||
@@ -92,8 +94,10 @@ fn main() {
|
||||
// User code compilation | 用户代码编译
|
||||
commands::compile_typescript,
|
||||
commands::watch_scripts,
|
||||
commands::watch_assets,
|
||||
commands::stop_watch_scripts,
|
||||
commands::check_environment,
|
||||
commands::install_esbuild,
|
||||
// Build commands | 构建命令
|
||||
commands::prepare_build_directory,
|
||||
commands::copy_directory,
|
||||
|
||||
Reference in New Issue
Block a user