初始化仓库

This commit is contained in:
ASLant committed 2025-11-12 01:19:35 +08:00
1 parent 2bedb35f58
commit 75bf59adeb
21 files changed
+12064 -1

No files matched your search

+3 -1
View File
@@ -1,2 +1,4 @@
# UPX-GUI
UPX套壳GUI
GUI版UPX加壳/脱壳工具
[upx官方仓库](https://github.com/upx/upx)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+29
View File
@@ -0,0 +1,29 @@
# 图标文件
请在此目录放置以下图标文件:
- `32x32.png` - 32x32 像素的 PNG 图标
- `128x128.png` - 128x128 像素的 PNG 图标
- `128x128@2x.png` - 256x256 像素的 PNG 图标(高分辨率)
- `icon.ico` - Windows ICO 格式图标
## 图标要求
- 图标应该是正方形
- 建议使用透明背景
- PNG 图标建议 32 位色深
- ICO 文件可以包含多个尺寸(16x16, 32x32, 48x48, 256x256)
## 图标生成工具
可以使用以下工具生成图标:
- [IconKitchen](https://icon.kitchen/)
- [Favicon.io](https://favicon.io/)
- [RealFaviconGenerator](https://realfavicongenerator.net/)
## 临时方案
如果暂时没有图标,可以从 timer 项目复制图标文件:
```
copy e:\Code_Repository\timer\icons\*.* f:\Code\tools\upx-gui\icons\
```
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

+5364
View File
File diff suppressed because it is too large. Load diff
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "upx-gui"
version = "1.0.0"
description = "UPX 可视化加壳脱壳工具"
authors = ["ASLant"]
edition = "2021"
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2"
tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
encoding_rs = "0.8"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
[profile.release]
strip = true # 移除调试符号
opt-level = "z" # 优化大小
lto = true # 链接时优化
codegen-units = 1 # 更好的优化
panic = "abort" # 减小panic处理代码
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+1
View File
@@ -0,0 +1 @@
{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command Line truncated
+1
View File
@@ -0,0 +1 @@
{}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+303
View File
@@ -0,0 +1,303 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use serde::{Deserialize, Serialize};
use std::process::Command;
use std::path::{Path, PathBuf};
use std::fs;
use encoding_rs::GBK;
#[derive(Debug, Serialize, Deserialize)]
struct UpxOptions {
mode: String,
input_file: String,
output_file: String,
compression_level: String,
backup: bool,
ultra_brute: bool,
}
#[derive(Debug, Serialize, Deserialize)]
struct ScanFolderOptions {
folder_path: String,
include_subfolders: bool,
}
// 获取UPX可执行文件路径
fn get_upx_path() -> PathBuf {
// 尝试1: 使用打包后的upx (在exe同目录下)
if let Ok(exe_path) = std::env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
let bundled_upx = exe_dir.join("upx.exe");
if bundled_upx.exists() {
return bundled_upx;
}
}
}
// 尝试2: 使用开发环境的upx (src-tauri/../upx/upx.exe)
let dev_upx = PathBuf::from("../../upx/upx.exe");
if dev_upx.exists() {
return dev_upx;
}
// 尝试3: 另一个开发环境路径
let dev_upx2 = PathBuf::from("../upx/upx.exe");
if dev_upx2.exists() {
return dev_upx2;
}
// 尝试4: 使用系统PATH中的upx (降级方案)
PathBuf::from("upx")
}
#[tauri::command]
async fn process_upx(options: UpxOptions) -> Result<String, String> {
// 获取UPX可执行文件路径
let upx_path = get_upx_path();
// 检查 UPX 是否可用
let mut upx_check = Command::new(&upx_path);
// 在 Windows 上隐藏控制台窗口
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
upx_check.creation_flags(CREATE_NO_WINDOW);
}
let upx_check = upx_check.arg("--version").output();
if upx_check.is_err() {
return Err("未找到 UPX 工具!".to_string());
}
// 检查输入文件是否存在
if !Path::new(&options.input_file).exists() {
return Err(format!("输入文件不存在: {}", options.input_file));
}
// 检查文件是否可写(如果需要覆盖)
if options.input_file == options.output_file {
let metadata = fs::metadata(&options.input_file)
.map_err(|e| format!("无法读取文件属性: {}", e))?;
if metadata.permissions().readonly() {
return Err(format!("文件为只读,请先修改文件属性"));
}
}
// 备份原文件
if options.backup {
let backup_path = format!("{}.bak", options.input_file);
if let Err(e) = fs::copy(&options.input_file, &backup_path) {
return Err(format!("备份文件失败: {}", e));
}
}
// 构建 UPX 命令
let mut cmd = Command::new(&upx_path);
// 在 Windows 上隐藏控制台窗口
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
// 判断是否覆盖原文件
let is_overwrite = options.input_file == options.output_file;
match options.mode.as_str() {
"compress" => {
// 加壳模式
if options.ultra_brute {
cmd.arg("--ultra-brute");
cmd.arg("--no-lzma"); // 禁用LZMA以避免解压速度过慢
} else if options.compression_level == "best" {
cmd.arg("--best");
} else {
cmd.arg(format!("-{}", options.compression_level));
}
if is_overwrite {
// 直接覆盖原文件,不使用 -o 参数
cmd.arg(&options.input_file);
} else {
// 输出到指定文件
cmd.arg(&options.input_file);
cmd.arg("-o");
cmd.arg(&options.output_file);
}
cmd.arg("--force-overwrite");
}
"decompress" => {
// 脱壳模式
cmd.arg("-d");
if is_overwrite {
// 直接覆盖原文件
cmd.arg(&options.input_file);
} else {
// 输出到指定文件
cmd.arg(&options.input_file);
cmd.arg("-o");
cmd.arg(&options.output_file);
}
cmd.arg("--force-overwrite");
}
_ => {
return Err("未知的操作模式".to_string());
}
}
// 在处理前记录原始文件大小
let original_size = fs::metadata(&options.input_file)
.map(|m| m.len())
.unwrap_or(0);
// 在后台线程执行命令,避免阻塞 UI
let output_file_clone = options.output_file.clone();
let original_size_clone = original_size;
tokio::task::spawn_blocking(move || {
match cmd.output() {
Ok(output) => {
// 在 Windows 上 UPX 输出是 GBK 编码,需要转换为 UTF-8
let (stdout, _, _) = GBK.decode(&output.stdout);
let (stderr, _, _) = GBK.decode(&output.stderr);
if output.status.success() {
// 获取处理后的文件大小
let output_size = fs::metadata(&output_file_clone)
.map(|m| m.len())
.unwrap_or(0);
let ratio = if original_size_clone > 0 {
(output_size as f64 / original_size_clone as f64 * 100.0) as i32
} else {
100
};
let result = format!(
"操作成功!\n输出: {}\n原始大小: {}\n处理后大小: {}\n压缩率: {}%\n\nUPX 输出:\n{}{}",
output_file_clone,
format_bytes(original_size_clone),
format_bytes(output_size),
ratio,
stdout,
stderr
);
Ok(result)
} else {
Err(format!("UPX 执行失败:\n{}{}", stdout, stderr))
}
}
Err(e) => {
Err(format!("执行 UPX 命令失败: {}", e))
}
}
})
.await
.map_err(|e| format!("任务执行错误: {}", e))?
}
// 格式化字节大小
fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} bytes", bytes)
}
}
// 扫描文件夹获取所有exe和dll文件
fn scan_folder_recursive(folder_path: &Path, include_subfolders: bool) -> Vec<String> {
let mut files = Vec::new();
if let Ok(entries) = fs::read_dir(folder_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() && include_subfolders {
// 递归扫描子文件夹
let sub_files = scan_folder_recursive(&path, include_subfolders);
files.extend(sub_files);
} else if path.is_file() {
if let Some(extension) = path.extension() {
let ext = extension.to_string_lossy().to_lowercase();
if ext == "exe" || ext == "dll" {
if let Some(path_str) = path.to_str() {
files.push(path_str.to_string());
}
}
}
}
}
}
files
}
#[tauri::command]
fn scan_folder(options: ScanFolderOptions) -> Result<Vec<String>, String> {
let path = Path::new(&options.folder_path);
if !path.exists() {
return Err(format!("路径不存在: {}", options.folder_path));
}
if !path.is_dir() {
return Err(format!("不是文件夹: {}", options.folder_path));
}
Ok(scan_folder_recursive(path, options.include_subfolders))
}
// 获取UPX版本
#[tauri::command]
fn get_upx_version() -> Result<String, String> {
let upx_path = get_upx_path();
let mut cmd = Command::new(&upx_path);
// 在 Windows 上隐藏控制台窗口
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
match cmd.arg("--version").output() {
Ok(output) => {
let (version_str, _, _) = GBK.decode(&output.stdout);
let lines: Vec<&str> = version_str.lines().collect();
if !lines.is_empty() {
Ok(lines[0].to_string())
} else {
Err("无法获取UPX版本".to_string())
}
}
Err(_) => Err("UPX未找到".to_string())
}
}
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![process_upx, scan_folder, get_upx_version])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+89
View File
@@ -0,0 +1,89 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "UPX GUI",
"version": "1.0.0",
"identifier": "top.aslant.upxgui",
"build": {
"beforeDevCommand": "",
"beforeBuildCommand": "",
"frontendDist": "../ui"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"label": "main",
"title": "UPX GUI",
"width": 1000,
"height": 800,
"minWidth": 900,
"minHeight": 700,
"resizable": true,
"fullscreen": false,
"center": true,
"decorations": false,
"transparent": false,
"visible": true,
"dragDropEnabled": true
}
],
"security": {
"csp": null,
"capabilities": [
{
"identifier": "main-capability",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:default",
"core:window:allow-close",
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:window:allow-toggle-maximize",
"core:window:allow-start-dragging",
"core:window:allow-show",
"dialog:allow-open",
"dialog:allow-save",
"shell:allow-execute"
]
}
]
}
},
"bundle": {
"active": true,
"targets": ["msi", "nsis"],
"icon": [
"../icons/32x32.png",
"../icons/128x128.png",
"../icons/128x128@2x.png",
"../icons/icon.ico"
],
"resources": [
"../upx/upx.exe"
],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"wix": {
"language": ["zh-CN"],
"template": null
},
"nsis": {
"installMode": "currentUser",
"languages": ["SimpChinese"],
"displayLanguageSelector": true,
"compression": "lzma",
"installerIcon": "../icons/icon.ico",
"headerImage": null
}
},
"shortDescription": "UPX 可视化工具",
"longDescription": "一个基于 Tauri 的 UPX 可视化加壳脱壳工具,支持文件压缩和解压",
"copyright": "Copyright © 2025",
"license": "MIT",
"category": "Utility"
}
}
+286
View File
@@ -0,0 +1,286 @@
/* 字体定义 */
@font-face {
font-family: 'HarmonyOS Sans SC';
src: url('../font/HarmonyOS_Sans_SC_Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
/* 全局样式 */
:root {
--border-radius: 0.3rem;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
body {
font-family: 'HarmonyOS Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 呼吸灯效果 */
@keyframes breathing {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.9);
}
}
.breathing-light {
animation: breathing 2s ease-in-out infinite;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1a1a1a;
}
::-webkit-scrollbar-thumb {
background: #4a4a4a;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #5a5a5a;
}
/* 日志输出区域 */
#log-output {
font-family: 'HarmonyOS Sans SC', 'Consolas', 'Monaco', 'Courier New', monospace;
overflow: overlay; /* 使用overlay避免内容被滚动条遮挡 */
}
/* 日志输出区域滚动条 */
#log-output::-webkit-scrollbar {
width: 6px;
}
#log-output::-webkit-scrollbar-track {
background: transparent;
border-radius: 0.3rem;
}
#log-output::-webkit-scrollbar-thumb {
background: rgba(74, 74, 74, 0.6);
border-radius: 0.3rem;
}
#log-output::-webkit-scrollbar-thumb:hover {
background: rgba(90, 90, 90, 0.8);
}
/* 日志高亮样式 */
#log-output .log-highlight {
color: #ffffff;
font-weight: 500;
}
/* 日志行样式 */
.log-line {
margin-bottom: 4px;
line-height: 1.6;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.log-info {
color: #10b981;
}
.log-error {
color: #ef4444;
}
.log-warning {
color: #f59e0b;
}
.log-success {
color: #10b981;
font-weight: 600;
}
/* 按钮动画 */
button {
transition: all 0.2s ease;
}
button:active {
transform: scale(0.98);
}
/* 模式选择按钮激活状态 */
.mode-active {
border-color: #3b82f6 !important;
background-color: #eff6ff !important;
color: #1d4ed8 !important;
}
.mode-inactive {
border-color: #d1d5db !important;
background-color: white !important;
color: #6b7280 !important;
}
/* 禁用选择 */
.select-none {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* 输入框聚焦效果 */
input:focus, select:focus {
outline: none;
}
/* 自定义复选框 */
input[type="checkbox"] {
cursor: pointer;
outline: none;
}
input[type="checkbox"]:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* 滑动条样式 */
input[type="range"].slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 8px;
background: linear-gradient(to right, #93c5fd 0%, #3b82f6 50%, #1d4ed8 100%);
border-radius: 5px;
outline: none;
transition: opacity 0.2s;
font-family: inherit;
}
input[type="range"].slider:hover {
opacity: 0.9;
}
/* 滑块样式 - Webkit浏览器 */
input[type="range"].slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
background: white;
border: 3px solid #3b82f6;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.2s;
}
input[type="range"].slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
}
input[type="range"].slider::-webkit-slider-thumb:active {
transform: scale(1.05);
background: #eff6ff;
}
/* 滑块样式 - Firefox */
input[type="range"].slider::-moz-range-thumb {
width: 24px;
height: 24px;
background: white;
border: 3px solid #3b82f6;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.2s;
}
input[type="range"].slider::-moz-range-thumb:hover {
transform: scale(1.1);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
}
input[type="range"].slider::-moz-range-thumb:active {
transform: scale(1.05);
background: #eff6ff;
}
/* 加载动画 */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
/* 渐入动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.3s ease-out;
}
/* 进度条 */
.progress-bar {
transition: width 0.3s ease;
}
/* 文件拖放区域 */
.drop-zone {
transition: all 0.3s ease;
position: relative;
}
.drop-zone.drag-over {
transform: scale(1.02) !important;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3) !important;
border-width: 3px;
}
#compress-btn.drag-over {
background: linear-gradient(to bottom right, #dbeafe, #bfdbfe) !important;
border-color: #2563eb !important;
}
#decompress-btn.drag-over {
background: linear-gradient(to bottom right, #d1fae5, #a7f3d0) !important;
border-color: #059669 !important;
}
Binary file not shown.
+154
View File
@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UPX GUI</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="css/style.css">
</head>
<body class="h-screen overflow-hidden bg-gray-50" oncontextmenu="return false;">
<!-- 标题栏 -->
<div id="titlebar" class="h-10 bg-white border-b border-gray-200 flex items-center justify-between px-4">
<div data-tauri-drag-region class="flex items-center space-x-3 flex-1 h-full">
<div class="w-2 h-2 rounded-full bg-blue-500 breathing-light"></div>
<span class="text-gray-700 text-xs font-medium select-none">UPX 可视化工具</span>
<span id="upx-version" class="text-gray-400 text-xs select-none"></span>
</div>
<div class="flex items-center space-x-1">
<button id="settings-btn" class="w-9 h-9 hover:bg-gray-100 flex items-center justify-center transition-all duration-200" style="border-radius: 0.3rem;" title="设置">
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
<button id="titlebar-minimize" class="w-9 h-9 hover:bg-gray-100 flex items-center justify-center transition-all duration-200" style="border-radius: 0.3rem;">
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"></path>
</svg>
</button>
<button id="titlebar-maximize" class="w-9 h-9 hover:bg-gray-100 flex items-center justify-center transition-all duration-200" style="border-radius: 0.3rem;">
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path>
</svg>
</button>
<button id="titlebar-close" class="w-9 h-9 hover:bg-red-50 flex items-center justify-center transition-all duration-200 group" style="border-radius: 0.3rem;">
<svg class="w-4 h-4 text-gray-600 group-hover:text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<!-- 主内容区 -->
<div class="h-[calc(100vh-2.5rem)] flex flex-col p-6">
<!-- 操作选择 -->
<div class="bg-white shadow-lg p-6 border border-gray-200 mb-6" style="border-radius: 0.3rem;">
<div class="grid grid-cols-2 gap-6">
<button id="compress-btn" class="drop-zone group relative py-8 px-6 border-2 border-blue-500 bg-gradient-to-br from-blue-50 to-blue-100 hover:from-blue-100 hover:to-blue-200 transition-all shadow-md hover:shadow-xl transform hover:-translate-y-1" style="border-radius: 0.3rem;">
<svg class="w-16 h-16 mx-auto mb-3 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<div class="text-xl text-blue-700 mb-2">加壳压缩</div>
<div class="text-sm text-blue-600">点击选择或拖放文件/文件夹到此处</div>
</button>
<button id="decompress-btn" class="drop-zone group relative py-8 px-6 border-2 border-green-500 bg-gradient-to-br from-green-50 to-green-100 hover:from-green-100 hover:to-green-200 transition-all shadow-md hover:shadow-xl transform hover:-translate-y-1" style="border-radius: 0.3rem;">
<svg class="w-16 h-16 mx-auto mb-3 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
</svg>
<div class="text-xl text-green-700 mb-2">脱壳解压</div>
<div class="text-sm text-green-600">点击选择或拖放文件/文件夹到此处</div>
</button>
</div>
</div>
<!-- 日志输出 -->
<div class="bg-white shadow-lg p-6 border border-gray-200 flex-1 flex flex-col min-h-0" style="border-radius: 0.3rem;">
<h2 class="text-lg font-bold text-gray-800 mb-4">执行日志</h2>
<div id="log-output" class="flex-1 bg-gray-900 text-green-400 font-mono text-sm p-4 overflow-y-auto overflow-x-hidden" style="border-radius: 0.3rem;">
<div class="text-gray-500">等待操作...</div>
</div>
</div>
</div>
<!-- 全局设置弹窗 -->
<div id="settings-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white shadow-2xl p-6 max-w-md w-full mx-4 transform transition-all" style="border-radius: 0.3rem;">
<h2 class="text-xl font-medium text-gray-800 mb-5 pb-3 border-b border-gray-200">全局设置</h2>
<div class="space-y-5">
<div>
<div class="flex items-center justify-between mb-4">
<label class="text-sm font-medium text-gray-700">压缩级别</label>
<span id="level-display" class="text-base font-semibold text-blue-600">级别 9</span>
</div>
<div class="relative">
<input type="range" id="compression-level" min="1" max="10" value="9"
class="w-full h-2 bg-gray-200 appearance-none cursor-pointer slider" style="border-radius: 0.3rem;">
<!-- 级别标记 -->
<div class="flex justify-between mt-2 px-1">
<span class="text-xs text-gray-400">1</span>
<span class="text-xs text-gray-400">2</span>
<span class="text-xs text-gray-400">3</span>
<span class="text-xs text-gray-400">4</span>
<span class="text-xs text-gray-400">5</span>
<span class="text-xs text-gray-400">6</span>
<span class="text-xs text-gray-400">7</span>
<span class="text-xs text-gray-400">8</span>
<span class="text-xs text-gray-400">9</span>
<span class="text-xs text-gray-500 font-medium">best</span>
</div>
</div>
<p id="level-description" class="mt-2 text-xs text-gray-500 text-center">推荐级别,平衡速度和压缩率</p>
</div>
<div class="space-y-2">
<label class="flex items-start space-x-3 cursor-pointer p-2.5 hover:bg-gray-50 transition-colors" style="border-radius: 0.3rem;">
<input type="checkbox" id="overwrite" checked class="w-4 h-4 mt-0.5 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500 cursor-pointer">
<div class="flex-1">
<span class="text-sm font-medium text-gray-800">覆盖原文件</span>
<p class="text-xs text-gray-500 mt-0.5">直接替换原文件,不创建新文件</p>
</div>
</label>
<label class="flex items-start space-x-3 cursor-pointer p-2.5 hover:bg-gray-50 transition-colors" style="border-radius: 0.3rem;">
<input type="checkbox" id="backup" class="w-4 h-4 mt-0.5 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500 cursor-pointer">
<div class="flex-1">
<span class="text-sm font-medium text-gray-800">备份原文件</span>
<p class="text-xs text-gray-500 mt-0.5">保留 .bak 备份文件</p>
</div>
</label>
<label class="flex items-start space-x-3 cursor-pointer p-2.5 hover:bg-gray-50 transition-colors" style="border-radius: 0.3rem;">
<input type="checkbox" id="ultra-brute" class="w-4 h-4 mt-0.5 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500 cursor-pointer">
<div class="flex-1">
<span class="text-sm font-medium text-gray-800">极限压缩</span>
<p class="text-xs text-gray-500 mt-0.5">尝试所有压缩方法,速度较慢但压缩率最高</p>
</div>
</label>
<label class="flex items-start space-x-3 cursor-pointer p-2.5 hover:bg-gray-50 transition-colors" style="border-radius: 0.3rem;">
<input type="checkbox" id="include-subfolders" class="w-4 h-4 mt-0.5 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500 cursor-pointer">
<div class="flex-1">
<span class="text-sm font-medium text-gray-800">包含子文件夹</span>
<p class="text-xs text-gray-500 mt-0.5">处理文件夹时,递归处理所有子目录</p>
</div>
</label>
</div>
</div>
<div class="flex mt-6 pt-4 border-t border-gray-200">
<button id="close-settings" class="w-full py-2.5 px-6 bg-blue-500 text-white text-sm font-medium hover:bg-blue-600 transition-colors shadow-sm" style="border-radius: 0.3rem;">
保存设置
</button>
</div>
</div>
</div>
<script type="module" src="js/main.js"></script>
</body>
</html>
+542
View File
@@ -0,0 +1,542 @@
const { invoke } = window.__TAURI__.core;
const { open, save } = window.__TAURI__.dialog;
const { getCurrentWindow } = window.__TAURI__.window;
const { listen } = window.__TAURI__.event;
// DOM 元素
const compressBtn = document.getElementById('compress-btn');
const decompressBtn = document.getElementById('decompress-btn');
const compressionLevel = document.getElementById('compression-level');
const levelDisplay = document.getElementById('level-display');
const levelDescription = document.getElementById('level-description');
const overwriteCheckbox = document.getElementById('overwrite');
const backupCheckbox = document.getElementById('backup');
const ultraBruteCheckbox = document.getElementById('ultra-brute');
const includeSubfoldersCheckbox = document.getElementById('include-subfolders');
const logOutput = document.getElementById('log-output');
const settingsModal = document.getElementById('settings-modal');
const settingsBtn = document.getElementById('settings-btn');
const closeSettingsBtn = document.getElementById('close-settings');
// 窗口控制按钮
const minimizeBtn = document.getElementById('titlebar-minimize');
const maximizeBtn = document.getElementById('titlebar-maximize');
const closeBtn = document.getElementById('titlebar-close');
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
initWindowControls();
initOperationButtons();
initCompressionLevelSlider();
preventRefresh();
await setupDragAndDrop();
// 获取并显示UPX版本
try {
const version = await invoke('get_upx_version');
// 在标题栏显示版本
const versionElement = document.getElementById('upx-version');
if (versionElement) {
versionElement.textContent = `- ${version}`;
}
addLog(`UPX GUI 已就绪 - ${version}`, 'info');
} catch (error) {
addLog('UPX GUI 已就绪 - 请选择操作', 'info');
}
});
// 屏蔽刷新快捷键
function preventRefresh() {
document.addEventListener('keydown', (e) => {
// F5 或 Ctrl+R 或 Ctrl+Shift+R 刷新
if (e.key === 'F5' ||
(e.ctrlKey && e.key === 'r') ||
(e.ctrlKey && e.shiftKey && e.key === 'R')) {
e.preventDefault();
addLog('刷新功能已禁用', 'warning');
return false;
}
// Ctrl+W 关闭窗口
if (e.ctrlKey && e.key === 'w') {
e.preventDefault();
return false;
}
});
}
// 窗口控制
function initWindowControls() {
const appWindow = getCurrentWindow();
minimizeBtn.addEventListener('click', () => {
appWindow.minimize();
});
maximizeBtn.addEventListener('click', () => {
appWindow.toggleMaximize();
});
closeBtn.addEventListener('click', () => {
appWindow.close();
});
}
// 操作按钮初始化
function initOperationButtons() {
// 设置按钮 - 显示全局设置
settingsBtn.addEventListener('click', () => {
showSettingsModal();
});
// 关闭设置按钮
closeSettingsBtn.addEventListener('click', () => {
hideSettingsModal();
addLog('设置已保存', 'success');
});
// 点击背景关闭弹窗
settingsModal.addEventListener('click', (e) => {
if (e.target === settingsModal) {
hideSettingsModal();
}
});
// 加壳压缩按钮 - 直接执行
compressBtn.addEventListener('click', async () => {
// 检查是否有多个文件
if (window.droppedFiles && window.droppedFiles.length > 0) {
const files = window.droppedFiles;
window.droppedFiles = null;
addLog('开始批量加壳压缩...', 'info');
await processBatchFiles(files, 'compress');
}
// 检查是否有单个文件
else if (window.droppedFile) {
const filePath = window.droppedFile;
window.droppedFile = null;
addLog('开始加壳压缩...', 'info');
await handleCompressWithFile(filePath);
}
// 没有拖放文件,弹出选择
else {
addLog('选择文件进行加壳...', 'info');
await handleCompress();
}
});
// 脱壳解压按钮 - 直接执行
decompressBtn.addEventListener('click', async () => {
// 检查是否有多个文件
if (window.droppedFiles && window.droppedFiles.length > 0) {
const files = window.droppedFiles;
window.droppedFiles = null;
addLog('开始批量脱壳解压...', 'info');
await processBatchFiles(files, 'decompress');
}
// 检查是否有单个文件
else if (window.droppedFile) {
const filePath = window.droppedFile;
window.droppedFile = null;
addLog('开始脱壳解压...', 'info');
await handleDecompressWithFile(filePath);
}
// 没有拖放文件,弹出选择
else {
addLog('选择文件进行脱壳...', 'info');
await handleDecompress();
}
});
}
// 初始化压缩级别滑动条
function initCompressionLevelSlider() {
// 级别描述映射
const levelDescriptions = {
1: '最快速度,压缩率最低',
2: '较快速度,较低压缩率',
3: '快速压缩',
4: '平衡模式',
5: '标准压缩',
6: '良好压缩',
7: '较高压缩率',
8: '高压缩率',
9: '推荐级别,平衡速度和压缩率',
10: '极致压缩,速度最慢'
};
// 更新显示
function updateLevelDisplay(value) {
const level = parseInt(value);
if (level === 10) {
levelDisplay.textContent = '级别 best';
} else {
levelDisplay.textContent = `级别 ${level}`;
}
levelDescription.textContent = levelDescriptions[level] || '';
}
// 初始显示
updateLevelDisplay(compressionLevel.value);
// 监听滑动条变化
compressionLevel.addEventListener('input', (e) => {
updateLevelDisplay(e.target.value);
});
}
// 获取当前压缩级别值
function getCompressionLevel() {
const value = parseInt(compressionLevel.value);
return value === 10 ? 'best' : value.toString();
}
// 显示设置弹窗
function showSettingsModal() {
settingsModal.classList.remove('hidden');
}
// 隐藏设置弹窗
function hideSettingsModal() {
settingsModal.classList.add('hidden');
}
// 扫描文件夹获取所有exe和dll文件
async function scanFolder(folderPath, includeSubfolders) {
try {
const files = await invoke('scan_folder', {
options: {
folder_path: folderPath,
include_subfolders: includeSubfolders
}
});
return files;
} catch (error) {
addLog(`扫描文件夹失败: ${error}`, 'error');
return [];
}
}
// 批量处理文件
async function processBatchFiles(files, mode) {
if (files.length === 0) {
addLog('没有找到可处理的文件', 'warning');
return;
}
addLog(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, 'info');
addLog(`批量处理模式`, 'info');
addLog(`找到 ${files.length} 个文件`, 'info');
addLog(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, 'info');
let successCount = 0;
let failCount = 0;
// 并行处理,每次最多5个文件
const batchSize = 5;
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, Math.min(i + batchSize, files.length));
await Promise.all(batch.map(async (file) => {
try {
if (mode === 'compress') {
await handleCompressWithFile(file);
} else {
await handleDecompressWithFile(file);
}
successCount++;
} catch (error) {
addLog(`处理失败: ${file}`, 'error');
failCount++;
}
}));
}
addLog(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, 'info', true);
addLog(`批量处理完成!`, 'success', true);
addLog(`成功: ${successCount} 个,失败: ${failCount} 个`, 'info', true);
addLog(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, 'info', true);
}
// 判断路径是否为文件夹并扫描
async function checkAndScanPath(path) {
// 尝试扫描文件夹
const files = await scanFolder(path, includeSubfoldersCheckbox.checked);
if (files.length > 0) {
// 是文件夹,返回扫描到的文件
addLog(`扫描文件夹: ${path} (找到 ${files.length} 个文件)`, 'info');
return files;
} else {
// 不是文件夹或没有找到文件,检查是否是exe/dll文件
if (path.toLowerCase().endsWith('.exe') || path.toLowerCase().endsWith('.dll')) {
return [path];
}
return [];
}
}
// 设置文件拖放功能
async function setupDragAndDrop() {
// 监听文件拖放释放事件
await listen('tauri://drag-drop', async (event) => {
const paths = event.payload.paths;
const position = event.payload.position;
if (paths && paths.length > 0) {
// 收集所有文件
const allFiles = [];
for (const path of paths) {
const files = await checkAndScanPath(path);
allFiles.push(...files);
}
if (allFiles.length === 0) {
addLog('未找到 .exe 或 .dll 文件', 'warning');
return;
}
// 判断拖放位置,自动触发对应操作
const dropTarget = getDropTarget(position);
if (dropTarget === 'compress') {
addLog('检测到拖放至加壳区域', 'info');
await processBatchFiles(allFiles, 'compress');
} else if (dropTarget === 'decompress') {
addLog('检测到拖放至脱壳区域', 'info');
await processBatchFiles(allFiles, 'decompress');
} else {
// 拖放到其他位置,存储文件等待用户选择
if (allFiles.length === 1) {
addLog('请点击"加壳压缩"或"脱壳解压"按钮', 'info');
window.droppedFile = allFiles[0];
} else {
addLog(`已选择 ${allFiles.length} 个文件,请点击操作按钮`, 'info');
window.droppedFiles = allFiles;
}
}
}
});
// 为按钮添加视觉反馈
setupVisualFeedback(compressBtn);
setupVisualFeedback(decompressBtn);
}
// 判断拖放位置对应的目标
function getDropTarget(position) {
if (!position) return null;
const compressRect = compressBtn.getBoundingClientRect();
const decompressRect = decompressBtn.getBoundingClientRect();
const x = position.x;
const y = position.y;
// 检查是否在加壳压缩按钮范围内
if (x >= compressRect.left && x <= compressRect.right &&
y >= compressRect.top && y <= compressRect.bottom) {
return 'compress';
}
// 检查是否在脱壳解压按钮范围内
if (x >= decompressRect.left && x <= decompressRect.right &&
y >= decompressRect.top && y <= decompressRect.bottom) {
return 'decompress';
}
return null;
}
// 为元素设置视觉反馈
function setupVisualFeedback(element) {
['dragenter', 'dragover'].forEach(eventName => {
element.addEventListener(eventName, (e) => {
e.preventDefault();
element.classList.add('drag-over');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
element.addEventListener(eventName, () => {
element.classList.remove('drag-over');
}, false);
});
}
// 处理加壳压缩
async function handleCompress() {
try {
// 选择文件(支持多选)
const selected = await open({
multiple: true,
filters: [{
name: '可执行文件',
extensions: ['exe', 'dll']
}]
});
if (!selected || (Array.isArray(selected) && selected.length === 0)) {
addLog('未选择文件', 'warning');
return;
}
// 处理选择结果(可能是单个路径或路径数组)
const files = Array.isArray(selected) ? selected : [selected];
// 批量处理或单文件处理
if (files.length === 1) {
addLog(`选择文件: ${files[0]}`, 'info');
await handleCompressWithFile(files[0]);
} else {
addLog(`选择了 ${files.length} 个文件`, 'info');
await processBatchFiles(files, 'compress');
}
} catch (error) {
addLog(`操作失败: ${error}`, 'error');
}
}
// 使用指定文件处理加壳压缩
async function handleCompressWithFile(inputFile) {
try {
let outputFile;
// 根据覆盖选项决定输出文件
if (overwriteCheckbox.checked) {
outputFile = inputFile;
addLog('将覆盖原文件', 'info');
} else {
// 自动生成输出文件名
const ext = inputFile.substring(inputFile.lastIndexOf('.'));
const baseName = inputFile.substring(0, inputFile.lastIndexOf('.'));
const defaultOutput = `${baseName}_packed${ext}`;
// 选择输出位置
outputFile = await save({
filters: [{
name: '可执行文件',
extensions: ['exe', 'dll']
}],
defaultPath: defaultOutput
});
if (!outputFile) {
addLog('未选择输出位置', 'warning');
return;
}
addLog(`输出文件: ${outputFile}`, 'info');
}
// 执行压缩
await processUpx('compress', inputFile, outputFile);
} catch (error) {
addLog(`操作失败: ${error}`, 'error');
}
}
// 处理脱壳解压
async function handleDecompress() {
try {
// 选择文件(支持多选)
const selected = await open({
multiple: true,
filters: [{
name: '可执行文件',
extensions: ['exe', 'dll']
}]
});
if (!selected || (Array.isArray(selected) && selected.length === 0)) {
addLog('未选择文件', 'warning');
return;
}
// 处理选择结果(可能是单个路径或路径数组)
const files = Array.isArray(selected) ? selected : [selected];
// 批量处理或单文件处理
if (files.length === 1) {
addLog(`选择文件: ${files[0]}`, 'info');
await handleDecompressWithFile(files[0]);
} else {
addLog(`选择了 ${files.length} 个文件`, 'info');
await processBatchFiles(files, 'decompress');
}
} catch (error) {
addLog(`操作失败: ${error}`, 'error');
}
}
// 使用指定文件处理脱壳解压
async function handleDecompressWithFile(inputFile) {
try {
// 脱壳默认覆盖原文件(直接恢复原状)
const outputFile = inputFile;
addLog('将覆盖原文件', 'info');
// 执行解压
await processUpx('decompress', inputFile, outputFile);
} catch (error) {
addLog(`操作失败: ${error}`, 'error');
}
}
// 执行 UPX 处理
async function processUpx(mode, inputFile, outputFile) {
try {
const options = {
mode: mode,
input_file: inputFile,
output_file: outputFile,
compression_level: getCompressionLevel(),
backup: backupCheckbox.checked,
ultra_brute: ultraBruteCheckbox.checked
};
if (ultraBruteCheckbox.checked) {
addLog('已启用极限压缩模式', 'info');
}
addLog(`开始${mode === 'compress' ? '加壳压缩' : '脱壳解压'}...`, 'info');
const result = await invoke('process_upx', { options });
addLog(result, 'success');
addLog('操作完成!', 'success', true);
} catch (error) {
addLog(`处理失败: ${error}`, 'error');
}
}
// 添加日志
function addLog(message, type = 'info', highlight = false) {
const logLine = document.createElement('div');
logLine.className = `log-line log-${type} fade-in${highlight ? ' log-highlight' : ''}`;
const timestamp = new Date().toLocaleTimeString('zh-CN', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
logLine.textContent = `[${timestamp}] ${message}`;
// 清除初始提示
if (logOutput.querySelector('.text-gray-500')) {
logOutput.innerHTML = '';
}
logOutput.appendChild(logLine);
// 自动滚动到底部
logOutput.scrollTop = logOutput.scrollHeight;
}
BIN
View File
Binary file not shown.