chore: 添加 ESLint 和 Prettier 代码格式化配置

- 新增 ESLint 和 Prettier 配置文件
- 配置代码规范:4空格缩进、单引号、无分号
- 更新文档:README.md 和 CLAUDE.md
- 格式化所有前端代码

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ASLantandClaude Opus 4.5 committed 2026-01-13 13:10:29 +08:00
1 parent d585477def
commit b24bdc5891
12 files changed
+1275 -614

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
node_modules/
src-tauri/
upx/
icons/
img/
*.min.js
*.min.css
dist/
target/
+27
View File
@@ -0,0 +1,27 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["html"],
"rules": {
"indent": ["error", 4],
"quotes": ["error", "single", { "avoidEscape": true }],
"semi": ["error", "never"],
"no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"no-constant-condition": ["warn", { "checkLoops": false }],
"no-console": "off"
},
"globals": {
"invoke": "readonly",
"open": "readonly",
"save": "readonly",
"getCurrentWindow": "readonly",
"listen": "readonly"
}
}
+5
View File
@@ -17,6 +17,11 @@ target
.claude/
*-cwd
# Node.js
node_modules/
package-lock.json
dist/
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+10
View File
@@ -0,0 +1,10 @@
node_modules/
src-tauri/
upx/
icons/
img/
*.min.js
*.min.css
dist/
target/
package-lock.json
+10
View File
@@ -0,0 +1,10 @@
{
"printWidth": 100,
"tabWidth": 4,
"useTabs": false,
"semi": false,
"singleQuote": true,
"htmlWhitespaceSensitivity": "ignore",
"endOfLine": "lf",
"trailingComma": "es5"
}
+22
View File
@@ -29,6 +29,12 @@ cargo tauri build
# 编译产物位置
# src-tauri/target/release/bundle/
# 生成 MSI 和 NSIS 两种安装包
# 前端代码检查
npm run lint # 检查代码问题
npm run lint:fix # 自动修复
npm run format # 格式化代码
npm run check # 完整检查
```
## 代码结构
@@ -70,3 +76,19 @@ batchSize = Math.max(2, Math.min(cpuCores * 2, 16))
- 使用 `window.__TAURI__` 全局对象(需启用 `withGlobalTauri: true`)
- 命令调用:`invoke('command_name', { options })`
- 拖放事件:`listen('tauri://drag-drop', handler)`
## 代码规范
### 前端 (JavaScript/HTML/CSS)
- **缩进**:4 空格
- **字符串**:单引号优先
- **分号**:不使用分号结尾
- **命名**:
- 常量:`UPPER_SNAKE_CASE`
- 函数/变量:`camelCase`
- DOM ID:`kebab-case`
- **修改前端代码后必须运行**:`npm run format`
### 后端 (Rust)
- 使用 `cargo fmt` 格式化
- 使用 `cargo clippy` 检查代码质量
+34 -2
View File
@@ -76,6 +76,9 @@
### 安装依赖
```bash
# 安装前端开发依赖(ESLint + Prettier)
npm install
# 安装 Tauri CLI
cargo install tauri-cli
@@ -83,6 +86,32 @@ cargo install tauri-cli
npm install -g @tauri-apps/cli
```
### 代码检查与格式化
项目使用 ESLint 和 Prettier 进行代码质量控制:
```bash
# 检查代码问题
npm run lint
# 自动修复代码问题
npm run lint:fix
# 格式化代码
npm run format
# 检查代码格式
npm run format:check
# 完整检查(lint + format)
npm run check
```
**代码规范**:
- 缩进:4 空格
- 字符串:单引号
- 结尾:无分号
### 项目结构
```
@@ -94,8 +123,11 @@ UPX-GUI/
├── src-tauri/ # Rust 后端
│ ├── src/ # Rust 源码
│ └── tauri.conf.json # Tauri 配置
└── upx/ # UPX 工具
└── upx.exe # UPX 可执行文件
├── upx/ # UPX 工具
│ └── upx.exe # UPX 可执行文件
├── .eslintrc.json # ESLint 配置
├── .prettierrc.json # Prettier 配置
└── package.json # npm 依赖配置
```
## 编译方法
+18
View File
@@ -0,0 +1,18 @@
{
"name": "upx-gui",
"version": "1.0.0",
"description": "UPX 可视化加壳/脱壳工具",
"scripts": {
"lint": "eslint \"ui/**/*.{js,html}\" --max-warnings 0",
"lint:fix": "eslint \"ui/**/*.{js,html}\" --fix",
"format": "prettier --write \"ui/**/*.{js,html,css}\"",
"format:check": "prettier --check \"ui/**/*.{js,html,css}\"",
"check": "npm run lint && npm run format:check"
},
"devDependencies": {
"eslint": "^8.57.1",
"eslint-plugin-html": "^8.1.1",
"prettier": "^3.3.3"
},
"private": true
}
+27 -14
View File
@@ -23,15 +23,27 @@
}
body {
font-family: 'HarmonyOS Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
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% {
0%,
100% {
opacity: 1;
transform: scale(1);
}
@@ -168,23 +180,24 @@ button:active {
}
/* 输入框聚焦效果 */
input:focus, select:focus {
input:focus,
select:focus {
outline: none;
}
/* 自定义复选框 */
input[type="checkbox"] {
input[type='checkbox'] {
cursor: pointer;
outline: none;
}
input[type="checkbox"]:focus {
input[type='checkbox']:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* 滑动条样式 */
input[type="range"].slider {
input[type='range'].slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
@@ -196,12 +209,12 @@ input[type="range"].slider {
font-family: inherit;
}
input[type="range"].slider:hover {
input[type='range'].slider:hover {
opacity: 0.9;
}
/* 滑块样式 - Webkit浏览器 */
input[type="range"].slider::-webkit-slider-thumb {
input[type='range'].slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
@@ -214,18 +227,18 @@ input[type="range"].slider::-webkit-slider-thumb {
transition: all 0.2s;
}
input[type="range"].slider::-webkit-slider-thumb:hover {
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 {
input[type='range'].slider::-webkit-slider-thumb:active {
transform: scale(1.05);
background: #eff6ff;
}
/* 滑块样式 - Firefox */
input[type="range"].slider::-moz-range-thumb {
input[type='range'].slider::-moz-range-thumb {
width: 24px;
height: 24px;
background: white;
@@ -236,12 +249,12 @@ input[type="range"].slider::-moz-range-thumb {
transition: all 0.2s;
}
input[type="range"].slider::-moz-range-thumb:hover {
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 {
input[type='range'].slider::-moz-range-thumb:active {
transform: scale(1.05);
background: #eff6ff;
}
+472 -161
View File
@@ -2,7 +2,9 @@
/* 仅包含项目中实际使用的类,优化加载性能 */
/* Reset & Base */
*, ::before, ::after {
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
@@ -10,236 +12,545 @@
}
/* Layout */
.h-screen { height: 100vh; }
.h-10 { height: 2.5rem; }
.h-9 { height: 2.25rem; }
.h-4 { height: 1rem; }
.h-2 { height: 0.5rem; }
.h-full { height: 100%; }
.h-\[calc\(100vh-2\.5rem\)\] { height: calc(100vh - 2.5rem); }
.h-screen {
height: 100vh;
}
.h-10 {
height: 2.5rem;
}
.h-9 {
height: 2.25rem;
}
.h-4 {
height: 1rem;
}
.h-2 {
height: 0.5rem;
}
.h-full {
height: 100%;
}
.h-\[calc\(100vh-2\.5rem\)\] {
height: calc(100vh - 2.5rem);
}
.w-full { width: 100%; }
.w-16 { width: 4rem; }
.w-9 { width: 2.25rem; }
.w-4 { width: 1rem; }
.w-2 { width: 0.5rem; }
.w-full {
width: 100%;
}
.w-16 {
width: 4rem;
}
.w-9 {
width: 2.25rem;
}
.w-4 {
width: 1rem;
}
.w-2 {
width: 0.5rem;
}
.min-h-0 { min-height: 0; }
.min-w-0 { min-width: 0; }
.max-w-\[700px\] { max-width: 700px; }
.min-h-0 {
min-height: 0;
}
.min-w-0 {
min-width: 0;
}
.max-w-\[700px\] {
max-width: 700px;
}
/* Flexbox */
.flex { display: flex; }
.flex-1 { flex: 1 1 0%; }
.flex-col { flex-direction: column; }
.flex-shrink-0 { flex-shrink: 0; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.flex {
display: flex;
}
.flex-1 {
flex: 1 1 0%;
}
.flex-col {
flex-direction: column;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.items-center {
align-items: center;
}
.items-start {
align-items: flex-start;
}
.justify-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
/* Grid */
.grid { display: grid; }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.col-span-2 { grid-column: span 2 / span 2; }
.grid {
display: grid;
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.col-span-2 {
grid-column: span 2 / span 2;
}
/* Spacing */
.gap-3 { gap: 0.75rem; }
.gap-6 { gap: 1.5rem; }
.space-x-1 > :not([hidden]) ~ :not([hidden]) { margin-left: 0.25rem; }
.space-x-2\.5 > :not([hidden]) ~ :not([hidden]) { margin-left: 0.625rem; }
.space-x-3 > :not([hidden]) ~ :not([hidden]) { margin-left: 0.75rem; }
.space-y-5 > :not([hidden]) ~ :not([hidden]) { margin-top: 1.25rem; }
.gap-3 {
gap: 0.75rem;
}
.gap-6 {
gap: 1.5rem;
}
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
margin-left: 0.25rem;
}
.space-x-2\.5 > :not([hidden]) ~ :not([hidden]) {
margin-left: 0.625rem;
}
.space-x-3 > :not([hidden]) ~ :not([hidden]) {
margin-left: 0.75rem;
}
.space-y-5 > :not([hidden]) ~ :not([hidden]) {
margin-top: 1.25rem;
}
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
.px-1 { padding-left: 0.25rem; padding-right: 0.25rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-2\.5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
.p-3 {
padding: 0.75rem;
}
.p-4 {
padding: 1rem;
}
.p-6 {
padding: 1.5rem;
}
.px-1 {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.py-2\.5 {
padding-top: 0.625rem;
padding-bottom: 0.625rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.m-0 { margin: 0; }
.mx-4 { margin-left: 1rem; margin-right: 1rem; }
.mx-auto { margin-left: auto; margin-right: auto; }
.mt-0\.5 { margin-top: 0.125rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-5 { margin-bottom: 1.25rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mt-6 { margin-top: 1.5rem; }
.pt-5 { padding-top: 1.25rem; }
.m-0 {
margin: 0;
}
.mx-4 {
margin-left: 1rem;
margin-right: 1rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.mt-0\.5 {
margin-top: 0.125rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mb-5 {
margin-bottom: 1.25rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mt-6 {
margin-top: 1.5rem;
}
.pt-5 {
padding-top: 1.25rem;
}
/* Position */
.relative { position: relative; }
.fixed { position: fixed; }
.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
.z-50 { z-index: 50; }
.relative {
position: relative;
}
.fixed {
position: fixed;
}
.inset-0 {
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.z-50 {
z-index: 50;
}
/* Display */
.hidden { display: none; }
.block { display: block; }
.hidden {
display: none;
}
.block {
display: block;
}
/* Overflow */
.overflow-hidden { overflow: hidden; }
.overflow-y-auto { overflow-y: auto; }
.overflow-x-hidden { overflow-x: hidden; }
.overflow-hidden {
overflow: hidden;
}
.overflow-y-auto {
overflow-y: auto;
}
.overflow-x-hidden {
overflow-x: hidden;
}
/* Colors - Background */
.bg-transparent { background-color: transparent; }
.bg-white { background-color: rgb(255 255 255); }
.bg-gray-50 { background-color: rgb(249 250 251); }
.bg-gray-100 { background-color: rgb(243 244 246); }
.bg-gray-200 { background-color: rgb(229 231 235); }
.bg-gray-900 { background-color: rgb(17 24 39); }
.bg-blue-50 { background-color: rgb(239 246 255); }
.bg-blue-500 { background-color: rgb(59 130 246); }
.bg-red-50 { background-color: rgb(254 242 242); }
.bg-black { background-color: rgb(0 0 0); }
.bg-transparent {
background-color: transparent;
}
.bg-white {
background-color: rgb(255 255 255);
}
.bg-gray-50 {
background-color: rgb(249 250 251);
}
.bg-gray-100 {
background-color: rgb(243 244 246);
}
.bg-gray-200 {
background-color: rgb(229 231 235);
}
.bg-gray-900 {
background-color: rgb(17 24 39);
}
.bg-blue-50 {
background-color: rgb(239 246 255);
}
.bg-blue-500 {
background-color: rgb(59 130 246);
}
.bg-red-50 {
background-color: rgb(254 242 242);
}
.bg-black {
background-color: rgb(0 0 0);
}
.bg-opacity-50 { --tw-bg-opacity: 0.5; }
.bg-opacity-50 { background-color: rgb(0 0 0 / var(--tw-bg-opacity)); }
.bg-opacity-50 {
--tw-bg-opacity: 0.5;
}
.bg-opacity-50 {
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
/* Colors - Text */
.text-gray-400 { color: rgb(156 163 175); }
.text-gray-500 { color: rgb(107 114 128); }
.text-gray-600 { color: rgb(75 85 99); }
.text-gray-700 { color: rgb(55 65 81); }
.text-gray-800 { color: rgb(31 41 55); }
.text-blue-600 { color: rgb(37 99 235); }
.text-blue-700 { color: rgb(29 78 216); }
.text-green-400 { color: rgb(74 222 128); }
.text-green-600 { color: rgb(22 163 74); }
.text-green-700 { color: rgb(21 128 61); }
.text-white { color: rgb(255 255 255); }
.text-gray-400 {
color: rgb(156 163 175);
}
.text-gray-500 {
color: rgb(107 114 128);
}
.text-gray-600 {
color: rgb(75 85 99);
}
.text-gray-700 {
color: rgb(55 65 81);
}
.text-gray-800 {
color: rgb(31 41 55);
}
.text-blue-600 {
color: rgb(37 99 235);
}
.text-blue-700 {
color: rgb(29 78 216);
}
.text-green-400 {
color: rgb(74 222 128);
}
.text-green-600 {
color: rgb(22 163 74);
}
.text-green-700 {
color: rgb(21 128 61);
}
.text-white {
color: rgb(255 255 255);
}
/* Colors - Border */
.border { border-width: 1px; }
.border-2 { border-width: 2px; }
.border-b { border-bottom-width: 1px; }
.border-t { border-top-width: 1px; }
.border-gray-200 { border-color: rgb(229 231 235); }
.border-gray-300 { border-color: rgb(209 213 219); }
.border-blue-300 { border-color: rgb(147 197 253); }
.border-blue-500 { border-color: rgb(59 130 246); }
.border-green-500 { border-color: rgb(34 197 94); }
.border {
border-width: 1px;
}
.border-2 {
border-width: 2px;
}
.border-b {
border-bottom-width: 1px;
}
.border-t {
border-top-width: 1px;
}
.border-gray-200 {
border-color: rgb(229 231 235);
}
.border-gray-300 {
border-color: rgb(209 213 219);
}
.border-blue-300 {
border-color: rgb(147 197 253);
}
.border-blue-500 {
border-color: rgb(59 130 246);
}
.border-green-500 {
border-color: rgb(34 197 94);
}
/* Border Radius */
.rounded { border-radius: 0.25rem; }
.rounded-lg { border-radius: 0.5rem; }
.rounded-full { border-radius: 9999px; }
.rounded {
border-radius: 0.25rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.rounded-full {
border-radius: 9999px;
}
/* Shadow */
.shadow-md { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); }
.shadow-lg { box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); }
.shadow-xl { box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); }
.shadow-2xl { box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); }
.shadow-md {
box-shadow:
0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
}
.shadow-lg {
box-shadow:
0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.shadow-xl {
box-shadow:
0 20px 25px -5px rgb(0 0 0 / 0.1),
0 8px 10px -6px rgb(0 0 0 / 0.1);
}
.shadow-2xl {
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
}
/* Typography */
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-base { font-size: 1rem; line-height: 1.5rem; }
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
.font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
.font-medium {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
.font-mono {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
}
/* Appearance */
.appearance-none { appearance: none; }
.appearance-none {
appearance: none;
}
/* Cursor */
.cursor-pointer { cursor: pointer; }
.cursor-pointer {
cursor: pointer;
}
/* Select */
.select-none { user-select: none; }
.select-none {
user-select: none;
}
/* Transform */
.transform { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); }
.hover\:-translate-y-1:hover { --tw-translate-y: -0.25rem; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); }
.transform {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate))
skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
scaleY(var(--tw-scale-y));
}
.hover\:-translate-y-1:hover {
--tw-translate-y: -0.25rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate))
skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x))
scaleY(var(--tw-scale-y));
}
/* Transition */
.transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.transition-colors { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.duration-200 { transition-duration: 200ms; }
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.duration-200 {
transition-duration: 200ms;
}
/* Gradient */
.bg-gradient-to-br { background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)); }
.from-blue-50 { --tw-gradient-from: #eff6ff; --tw-gradient-to: rgb(239 246 255 / 0); --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); }
.from-blue-100 { --tw-gradient-from: #dbeafe; --tw-gradient-to: rgb(219 234 254 / 0); --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); }
.from-green-50 { --tw-gradient-from: #f0fdf4; --tw-gradient-to: rgb(240 253 244 / 0); --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); }
.from-green-100 { --tw-gradient-from: #dcfce7; --tw-gradient-to: rgb(220 252 231 / 0); --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); }
.to-blue-100 { --tw-gradient-to: #dbeafe; }
.to-blue-200 { --tw-gradient-to: #bfdbfe; }
.to-green-100 { --tw-gradient-to: #dcfce7; }
.to-green-200 { --tw-gradient-to: #bbf7d0; }
.bg-gradient-to-br {
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
}
.from-blue-50 {
--tw-gradient-from: #eff6ff;
--tw-gradient-to: rgb(239 246 255 / 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.from-blue-100 {
--tw-gradient-from: #dbeafe;
--tw-gradient-to: rgb(219 234 254 / 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.from-green-50 {
--tw-gradient-from: #f0fdf4;
--tw-gradient-to: rgb(240 253 244 / 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.from-green-100 {
--tw-gradient-from: #dcfce7;
--tw-gradient-to: rgb(220 252 231 / 0);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-blue-100 {
--tw-gradient-to: #dbeafe;
}
.to-blue-200 {
--tw-gradient-to: #bfdbfe;
}
.to-green-100 {
--tw-gradient-to: #dcfce7;
}
.to-green-200 {
--tw-gradient-to: #bbf7d0;
}
/* Hover States - Background */
.hover\:bg-gray-100:hover {
background-color: rgb(243 244 246) !important;
.hover\:bg-gray-100:hover {
background-color: rgb(243 244 246) !important;
}
.hover\:bg-red-50:hover {
background-color: rgb(254 242 242) !important;
.hover\:bg-red-50:hover {
background-color: rgb(254 242 242) !important;
}
.hover\:bg-blue-50:hover {
background-color: rgb(239 246 255) !important;
.hover\:bg-blue-50:hover {
background-color: rgb(239 246 255) !important;
}
.hover\:bg-blue-600:hover {
background-color: rgb(37 99 235) !important;
.hover\:bg-blue-600:hover {
background-color: rgb(37 99 235) !important;
}
/* Hover States - Border */
.hover\:border-blue-300:hover {
border-color: rgb(147 197 253) !important;
.hover\:border-blue-300:hover {
border-color: rgb(147 197 253) !important;
}
/* Hover States - Text */
.hover\:text-blue-600:hover {
color: rgb(37 99 235) !important;
.hover\:text-blue-600:hover {
color: rgb(37 99 235) !important;
}
.hover\:text-red-600:hover {
color: rgb(220 38 38) !important;
.hover\:text-red-600:hover {
color: rgb(220 38 38) !important;
}
/* Hover States - Shadow */
.hover\:shadow-xl:hover {
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1) !important;
.hover\:shadow-xl:hover {
box-shadow:
0 20px 25px -5px rgb(0 0 0 / 0.1),
0 8px 10px -6px rgb(0 0 0 / 0.1) !important;
}
.hover\:shadow-lg:hover {
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1) !important;
.hover\:shadow-lg:hover {
box-shadow:
0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(0 0 0 / 0.1) !important;
}
/* Hover States - Gradient */
.hover\:from-blue-100:hover {
--tw-gradient-from: #dbeafe !important;
--tw-gradient-to: rgb(219 234 254 / 0) !important;
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
.hover\:from-blue-100:hover {
--tw-gradient-from: #dbeafe !important;
--tw-gradient-to: rgb(219 234 254 / 0) !important;
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
}
.hover\:from-green-100:hover {
--tw-gradient-from: #dcfce7 !important;
--tw-gradient-to: rgb(220 252 231 / 0) !important;
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
.hover\:from-green-100:hover {
--tw-gradient-from: #dcfce7 !important;
--tw-gradient-to: rgb(220 252 231 / 0) !important;
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
}
.hover\:to-blue-200:hover {
--tw-gradient-to: #bfdbfe !important;
.hover\:to-blue-200:hover {
--tw-gradient-to: #bfdbfe !important;
}
.hover\:to-green-200:hover {
--tw-gradient-to: #bbf7d0 !important;
.hover\:to-green-200:hover {
--tw-gradient-to: #bbf7d0 !important;
}
/* Active States */
.active\:bg-blue-700:active { background-color: rgb(29 78 216); }
.active\:bg-blue-700:active {
background-color: rgb(29 78 216);
}
/* Focus States */
.focus\:ring-2:focus { --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); }
.focus\:ring-blue-500:focus { --tw-ring-color: rgb(59 130 246); }
.focus\:ring-2:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width)
var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width))
var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus\:ring-blue-500:focus {
--tw-ring-color: rgb(59 130 246);
}
/* Group Hover */
.group:hover .group-hover\:text-red-600 { color: rgb(220 38 38); }
.group:hover .group-hover\:text-red-600 {
color: rgb(220 38 38);
}
/* Drop Zone */
.drop-zone { transition: all 0.3s ease; position: relative; }
.drop-zone {
transition: all 0.3s ease;
position: relative;
}
+349 -157
View File
@@ -1,172 +1,364 @@
<!DOCTYPE html>
<!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>
<!-- 本地化 Tailwind CSS - 仅包含使用的类,优化性能 -->
<link rel="stylesheet" href="css/tailwind-custom.css">
<link rel="stylesheet" href="css/style.css">
<!-- 预加载关键资源 -->
<link rel="preload" href="js/main.js" as="script">
</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 id="app-title" class="text-gray-700 text-xs font-medium select-none cursor-pointer hover:text-blue-600 transition-colors" title="访问 GitHub 仓库">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="refresh-icon-btn" class="w-9 h-9 bg-transparent 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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
<button id="settings-btn" class="w-9 h-9 bg-transparent 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 bg-transparent 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 bg-transparent 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 bg-transparent 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"/>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UPX GUI</title>
<!-- 本地化 Tailwind CSS - 仅包含使用的类,优化性能 -->
<link rel="stylesheet" href="css/tailwind-custom.css" />
<link rel="stylesheet" href="css/style.css" />
<!-- 预加载关键资源 -->
<link rel="preload" href="js/main.js" as="script" />
</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
id="app-title"
class="text-gray-700 text-xs font-medium select-none cursor-pointer hover:text-blue-600 transition-colors"
title="访问 GitHub 仓库"
>
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="refresh-icon-btn"
class="w-9 h-9 bg-transparent 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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</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"/>
<button
id="settings-btn"
class="w-9 h-9 bg-transparent 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 bg-transparent 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 bg-transparent 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 bg-transparent 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>
<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 w-full mx-4 transform transition-all" style="border-radius: 0.5rem; max-width: 700px;">
<h2 class="text-xl font-semibold text-gray-800 mb-5">全局设置</h2>
<div class="space-y-5">
<!-- 压缩级别 -->
<div class="bg-gray-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-3">
<label class="text-sm font-semibold text-gray-700">压缩级别</label>
<span id="level-display" class="text-base font-bold text-blue-600">级别 9</span>
</div>
<div class="relative mb-2">
<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>
<!-- 级别标记 -->
<div class="flex justify-between px-1 mb-2">
<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-600 font-semibold">best</span>
</div>
<p id="level-description" class="text-xs text-gray-500 text-center">推荐级别,平衡速度和压缩率</p>
</div>
<!-- 选项列表 - 双列布局 -->
<div class="grid grid-cols-2 gap-3">
<label class="flex items-start space-x-2.5 cursor-pointer p-3 border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-all rounded-lg">
<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 flex-shrink-0">
<div class="flex-1 min-w-0">
<span class="text-sm font-semibold text-gray-800 block">覆盖原文件</span>
<p class="text-xs text-gray-500 mt-0.5">直接替换原文件,不创建新文件</p>
</div>
</label>
<label class="flex items-start space-x-2.5 cursor-pointer p-3 border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-all rounded-lg">
<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 flex-shrink-0">
<div class="flex-1 min-w-0">
<span class="text-sm font-semibold text-gray-800 block">备份原文件</span>
<p class="text-xs text-gray-500 mt-0.5">保留 .bak 备份文件</p>
</div>
</label>
<label class="flex items-start space-x-2.5 cursor-pointer p-3 border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-all rounded-lg">
<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 flex-shrink-0">
<div class="flex-1 min-w-0">
<span class="text-sm font-semibold text-gray-800 block">极限压缩</span>
<p class="text-xs text-gray-500 mt-0.5">尝试所有压缩方法,速度较慢但压缩率最高</p>
</div>
</label>
<label class="flex items-start space-x-2.5 cursor-pointer p-3 border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-all rounded-lg">
<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 flex-shrink-0">
<div class="flex-1 min-w-0">
<span class="text-sm font-semibold text-gray-800 block">包含子文件夹</span>
<p class="text-xs text-gray-500 mt-0.5">处理文件夹时,递归处理所有子目录</p>
</div>
</label>
<label class="flex items-start space-x-2.5 cursor-pointer p-3 border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-all rounded-lg col-span-2">
<input type="checkbox" id="force-compress" class="w-4 h-4 mt-0.5 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500 cursor-pointer flex-shrink-0">
<div class="flex-1 min-w-0">
<span class="text-sm font-semibold text-gray-800 block">强制压缩</span>
<p class="text-xs text-gray-500 mt-0.5">强制处理受保护的文件(如 GUARD_CF),可能移除安全特性</p>
</div>
</label>
<!-- 主内容区 -->
<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="flex mt-6 pt-5 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-semibold hover:bg-blue-600 active:bg-blue-700 transition-colors shadow-md hover:shadow-lg rounded-lg">
保存设置
</button>
<!-- 日志输出 -->
<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>
<script type="module" src="js/main.js"></script>
</body>
<!-- 全局设置弹窗 -->
<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 w-full mx-4 transform transition-all"
style="border-radius: 0.5rem; max-width: 700px"
>
<h2 class="text-xl font-semibold text-gray-800 mb-5">全局设置</h2>
<div class="space-y-5">
<!-- 压缩级别 -->
<div class="bg-gray-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-3">
<label class="text-sm font-semibold text-gray-700">压缩级别</label>
<span id="level-display" class="text-base font-bold text-blue-600">
级别 9
</span>
</div>
<div class="relative mb-2">
<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>
<!-- 级别标记 -->
<div class="flex justify-between px-1 mb-2">
<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-600 font-semibold">best</span>
</div>
<p id="level-description" class="text-xs text-gray-500 text-center">
推荐级别,平衡速度和压缩率
</p>
</div>
<!-- 选项列表 - 双列布局 -->
<div class="grid grid-cols-2 gap-3">
<label
class="flex items-start space-x-2.5 cursor-pointer p-3 border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-all rounded-lg"
>
<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 flex-shrink-0"
/>
<div class="flex-1 min-w-0">
<span class="text-sm font-semibold text-gray-800 block">
覆盖原文件
</span>
<p class="text-xs text-gray-500 mt-0.5">
直接替换原文件,不创建新文件
</p>
</div>
</label>
<label
class="flex items-start space-x-2.5 cursor-pointer p-3 border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-all rounded-lg"
>
<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 flex-shrink-0"
/>
<div class="flex-1 min-w-0">
<span class="text-sm font-semibold text-gray-800 block">
备份原文件
</span>
<p class="text-xs text-gray-500 mt-0.5">保留 .bak 备份文件</p>
</div>
</label>
<label
class="flex items-start space-x-2.5 cursor-pointer p-3 border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-all rounded-lg"
>
<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 flex-shrink-0"
/>
<div class="flex-1 min-w-0">
<span class="text-sm font-semibold text-gray-800 block">
极限压缩
</span>
<p class="text-xs text-gray-500 mt-0.5">
尝试所有压缩方法,速度较慢但压缩率最高
</p>
</div>
</label>
<label
class="flex items-start space-x-2.5 cursor-pointer p-3 border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-all rounded-lg"
>
<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 flex-shrink-0"
/>
<div class="flex-1 min-w-0">
<span class="text-sm font-semibold text-gray-800 block">
包含子文件夹
</span>
<p class="text-xs text-gray-500 mt-0.5">
处理文件夹时,递归处理所有子目录
</p>
</div>
</label>
<label
class="flex items-start space-x-2.5 cursor-pointer p-3 border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-all rounded-lg col-span-2"
>
<input
type="checkbox"
id="force-compress"
class="w-4 h-4 mt-0.5 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500 cursor-pointer flex-shrink-0"
/>
<div class="flex-1 min-w-0">
<span class="text-sm font-semibold text-gray-800 block">
强制压缩
</span>
<p class="text-xs text-gray-500 mt-0.5">
强制处理受保护的文件(如 GUARD_CF),可能移除安全特性
</p>
</div>
</label>
</div>
</div>
<div class="flex mt-6 pt-5 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-semibold hover:bg-blue-600 active:bg-blue-700 transition-colors shadow-md hover:shadow-lg rounded-lg"
>
保存设置
</button>
</div>
</div>
</div>
<script type="module" src="js/main.js"></script>
</body>
</html>
+292 -280
View File
@@ -1,182 +1,190 @@
const { invoke } = window.__TAURI__.core;
const { open, save } = window.__TAURI__.dialog;
const { getCurrentWindow } = window.__TAURI__.window;
const { listen } = window.__TAURI__.event;
const { invoke } = window.__TAURI__.core
const { open, save } = window.__TAURI__.dialog
const { getCurrentWindow } = window.__TAURI__.window
const { listen } = window.__TAURI__.event
const appWindow = getCurrentWindow();
const appWindow = getCurrentWindow()
const PERFORMANCE_CONFIG = {
cpuCores: navigator.hardwareConcurrency || 4,
batchSize: null
};
batchSize: null,
}
function calculateOptimalBatchSize() {
const optimal = Math.max(2, Math.min(PERFORMANCE_CONFIG.cpuCores * 2, 16));
PERFORMANCE_CONFIG.batchSize = optimal;
return optimal;
const optimal = Math.max(2, Math.min(PERFORMANCE_CONFIG.cpuCores * 2, 16))
PERFORMANCE_CONFIG.batchSize = optimal
return optimal
}
// 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 forceCompressCheckbox = document.getElementById('force-compress');
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 refreshIconBtn = document.getElementById('refresh-icon-btn');
const appTitle = document.getElementById('app-title');
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 forceCompressCheckbox = document.getElementById('force-compress')
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 refreshIconBtn = document.getElementById('refresh-icon-btn')
const appTitle = document.getElementById('app-title')
// 窗口控制按钮
const minimizeBtn = document.getElementById('titlebar-minimize');
const maximizeBtn = document.getElementById('titlebar-maximize');
const closeBtn = document.getElementById('titlebar-close');
const minimizeBtn = document.getElementById('titlebar-minimize')
const maximizeBtn = document.getElementById('titlebar-maximize')
const closeBtn = document.getElementById('titlebar-close')
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
// 初始化性能配置
calculateOptimalBatchSize();
initWindowControls();
initOperationButtons();
initCompressionLevelSlider();
initTitleClick();
preventRefresh();
await setupDragAndDrop();
calculateOptimalBatchSize()
initWindowControls()
initOperationButtons()
initCompressionLevelSlider()
initTitleClick()
preventRefresh()
await setupDragAndDrop()
// 加载保存的配置
await loadSavedConfig();
await loadSavedConfig()
// 获取并显示UPX版本
try {
const version = await invoke('get_upx_version');
const version = await invoke('get_upx_version')
// 在标题栏显示版本
const versionElement = document.getElementById('upx-version');
const versionElement = document.getElementById('upx-version')
if (versionElement) {
versionElement.textContent = `- ${version}`;
versionElement.textContent = `- ${version}`
}
addLog(`UPX GUI 已就绪 - ${version}`, 'info');
addLog(`UPX GUI 已就绪 - ${version}`, 'info')
} catch (error) {
addLog('UPX GUI 已就绪 - 请选择操作', 'info');
addLog('UPX GUI 已就绪 - 请选择操作', 'info')
}
// 页面加载完成后显示窗口,避免白屏
setTimeout(async () => {
await appWindow.show();
}, 100);
await appWindow.show()
}, 100)
// 监听窗口大小变化,更新按钮位置缓存(使用防抖优化)
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
cachedButtonRects = null; // 清除缓存,下次使用时重新计算
}, 150);
}, { passive: true });
});
let resizeTimer
window.addEventListener(
'resize',
() => {
clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
cachedButtonRects = null // 清除缓存,下次使用时重新计算
}, 150)
},
{ passive: true }
)
})
function preventRefresh() {
const BLOCKED_COMBINATIONS = [
{ key: 'F5', message: '刷新功能已禁用' },
{ ctrlKey: true, key: 'r', message: '刷新功能已禁用' },
{ ctrlKey: true, key: 'w', message: null }
];
{ ctrlKey: true, key: 'w', message: null },
]
document.addEventListener('keydown', (e) => {
for (const combo of BLOCKED_COMBINATIONS) {
const isMatch = combo.ctrlKey
? e.ctrlKey && e.key.toLowerCase() === combo.key
: e.key === combo.key;
document.addEventListener(
'keydown',
(e) => {
for (const combo of BLOCKED_COMBINATIONS) {
const isMatch = combo.ctrlKey
? e.ctrlKey && e.key.toLowerCase() === combo.key
: e.key === combo.key
if (isMatch) {
e.preventDefault();
if (combo.message) addLog(combo.message, 'warning');
return false;
if (isMatch) {
e.preventDefault()
if (combo.message) addLog(combo.message, 'warning')
return false
}
}
}
}, { passive: false });
},
{ passive: false }
)
}
// 窗口控制
function initWindowControls() {
minimizeBtn.addEventListener('click', () => {
appWindow.minimize();
});
appWindow.minimize()
})
maximizeBtn.addEventListener('click', () => {
appWindow.toggleMaximize();
});
appWindow.toggleMaximize()
})
closeBtn.addEventListener('click', () => {
appWindow.close();
});
appWindow.close()
})
}
// 标题点击事件
function initTitleClick() {
appTitle.addEventListener('click', async (e) => {
// 阻止事件冒泡,避免触发拖动
e.stopPropagation();
e.stopPropagation()
try {
// 使用 Tauri 的 shell 插件打开 URL
const { open } = window.__TAURI__.shell;
await open('https://github.com/Y-ASLant/UPX-GUI');
addLog('已在浏览器中打开 GitHub 仓库', 'info');
const { open } = window.__TAURI__.shell
await open('https://github.com/Y-ASLant/UPX-GUI')
addLog('已在浏览器中打开 GitHub 仓库', 'info')
} catch (error) {
addLog(`打开链接失败: ${error}`, 'error');
addLog(`打开链接失败: ${error}`, 'error')
}
});
})
}
function initOperationButtons() {
refreshIconBtn.addEventListener('click', handleRefreshIcon);
settingsBtn.addEventListener('click', showSettingsModal);
closeSettingsBtn.addEventListener('click', handleCloseSettings);
settingsModal.addEventListener('click', handleModalBackdropClick);
refreshIconBtn.addEventListener('click', handleRefreshIcon)
settingsBtn.addEventListener('click', showSettingsModal)
closeSettingsBtn.addEventListener('click', handleCloseSettings)
settingsModal.addEventListener('click', handleModalBackdropClick)
compressBtn.addEventListener('click', () => handleOperationClick('compress'));
decompressBtn.addEventListener('click', () => handleOperationClick('decompress'));
compressBtn.addEventListener('click', () => handleOperationClick('compress'))
decompressBtn.addEventListener('click', () => handleOperationClick('decompress'))
}
async function handleCloseSettings() {
await saveCurrentConfig();
hideSettingsModal();
addLog('设置已保存', 'success');
await saveCurrentConfig()
hideSettingsModal()
addLog('设置已保存', 'success')
}
async function handleModalBackdropClick(e) {
if (e.target === settingsModal) {
await saveCurrentConfig();
hideSettingsModal();
await saveCurrentConfig()
hideSettingsModal()
}
}
async function handleOperationClick(mode) {
const modeName = mode === 'compress' ? '加壳压缩' : '脱壳解压';
const processFile = mode === 'compress' ? handleCompressWithFile : handleDecompressWithFile;
const selectFile = mode === 'compress' ? handleCompress : handleDecompress;
const modeName = mode === 'compress' ? '加壳压缩' : '脱壳解压'
const processFile = mode === 'compress' ? handleCompressWithFile : handleDecompressWithFile
const selectFile = mode === 'compress' ? handleCompress : handleDecompress
if (window.droppedFiles?.length > 0) {
const files = window.droppedFiles;
window.droppedFiles = null;
addLog(`开始批量${modeName}...`, 'info');
await processBatchFiles(files, mode);
const files = window.droppedFiles
window.droppedFiles = null
addLog(`开始批量${modeName}...`, 'info')
await processBatchFiles(files, mode)
} else if (window.droppedFile) {
const filePath = window.droppedFile;
window.droppedFile = null;
addLog(`开始${modeName}...`, 'info');
await processFile(filePath);
const filePath = window.droppedFile
window.droppedFile = null
addLog(`开始${modeName}...`, 'info')
await processFile(filePath)
} else {
addLog(`选择文件进行${modeName === '加壳压缩' ? '加壳' : '脱壳'}...`, 'info');
await selectFile();
addLog(`选择文件进行${modeName === '加壳压缩' ? '加壳' : '脱壳'}...`, 'info')
await selectFile()
}
}
@@ -191,36 +199,36 @@ const LEVEL_DESCRIPTIONS = {
7: '较高压缩率',
8: '高压缩率',
9: '推荐级别,平衡速度和压缩率',
10: '极致压缩,速度最慢'
};
10: '极致压缩,速度最慢',
}
// 更新压缩级别显示
function updateLevelDisplay(value) {
const level = parseInt(value);
levelDisplay.textContent = level === 10 ? '级别 best' : `级别 ${level}`;
levelDescription.textContent = LEVEL_DESCRIPTIONS[level] || '';
const level = parseInt(value)
levelDisplay.textContent = level === 10 ? '级别 best' : `级别 ${level}`
levelDescription.textContent = LEVEL_DESCRIPTIONS[level] || ''
}
// 初始化压缩级别滑动条
function initCompressionLevelSlider() {
updateLevelDisplay(compressionLevel.value);
compressionLevel.addEventListener('input', (e) => updateLevelDisplay(e.target.value));
updateLevelDisplay(compressionLevel.value)
compressionLevel.addEventListener('input', (e) => updateLevelDisplay(e.target.value))
}
// 获取当前压缩级别值
function getCompressionLevel() {
const value = parseInt(compressionLevel.value);
return value === 10 ? 'best' : value.toString();
const value = parseInt(compressionLevel.value)
return value === 10 ? 'best' : value.toString()
}
// 显示设置弹窗
function showSettingsModal() {
settingsModal.classList.remove('hidden');
settingsModal.classList.remove('hidden')
}
// 隐藏设置弹窗
function hideSettingsModal() {
settingsModal.classList.add('hidden');
settingsModal.classList.add('hidden')
}
// 扫描文件夹获取所有exe和dll文件
@@ -229,245 +237,253 @@ async function scanFolder(folderPath, includeSubfolders) {
const files = await invoke('scan_folder', {
options: {
folder_path: folderPath,
include_subfolders: includeSubfolders
}
});
return files;
include_subfolders: includeSubfolders,
},
})
return files
} catch (error) {
addLog(`扫描文件夹失败: ${error}`, 'error');
return [];
addLog(`扫描文件夹失败: ${error}`, 'error')
return []
}
}
async function processBatchFiles(files, mode) {
if (files.length === 0) {
addLog('没有找到可处理的文件', 'warning');
return;
addLog('没有找到可处理的文件', 'warning')
return
}
addLog(`批量处理模式 - 找到 ${files.length} 个文件`, 'info');
addLog(`使用 ${PERFORMANCE_CONFIG.batchSize} 并发处理(CPU核心: ${PERFORMANCE_CONFIG.cpuCores})`, 'info');
addLog(`批量处理模式 - 找到 ${files.length} 个文件`, 'info')
addLog(
`使用 ${PERFORMANCE_CONFIG.batchSize} 并发处理(CPU核心: ${PERFORMANCE_CONFIG.cpuCores})`,
'info'
)
const handler = mode === 'compress' ? handleCompressWithFile : handleDecompressWithFile;
let successCount = 0;
let failCount = 0;
const batchSize = PERFORMANCE_CONFIG.batchSize;
const handler = mode === 'compress' ? handleCompressWithFile : handleDecompressWithFile
let successCount = 0
let failCount = 0
const batchSize = PERFORMANCE_CONFIG.batchSize
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, Math.min(i + batchSize, files.length));
addLog(`处理进度: ${i + 1}-${Math.min(i + batchSize, files.length)}/${files.length}`, 'info');
const batch = files.slice(i, Math.min(i + batchSize, files.length))
addLog(
`处理进度: ${i + 1}-${Math.min(i + batchSize, files.length)}/${files.length}`,
'info'
)
await Promise.all(batch.map(async (file) => {
try {
await handler(file);
successCount++;
} catch {
addLog(`处理失败: ${file}`, 'error');
failCount++;
}
}));
await Promise.all(
batch.map(async (file) => {
try {
await handler(file)
successCount++
} catch {
addLog(`处理失败: ${file}`, 'error')
failCount++
}
})
)
await new Promise(resolve => setTimeout(resolve, 0));
await new Promise((resolve) => setTimeout(resolve, 0))
}
addLog(`批量处理完成! 成功: ${successCount} 个,失败: ${failCount} 个`, 'success', true);
addLog(`批量处理完成! 成功: ${successCount} 个,失败: ${failCount} 个`, 'success', true)
}
async function checkAndScanPath(path) {
const files = await scanFolder(path, includeSubfoldersCheckbox.checked);
const files = await scanFolder(path, includeSubfoldersCheckbox.checked)
if (files.length > 0) {
addLog(`扫描文件夹: ${path} (找到 ${files.length} 个文件)`, 'info');
return files;
addLog(`扫描文件夹: ${path} (找到 ${files.length} 个文件)`, 'info')
return files
}
const extension = path.toLowerCase();
const extension = path.toLowerCase()
if (extension.endsWith('.exe') || extension.endsWith('.dll')) {
return [path];
return [path]
}
return [];
return []
}
async function setupDragAndDrop() {
await listen('tauri://drag-drop', handleDragDrop);
setupVisualFeedback(compressBtn);
setupVisualFeedback(decompressBtn);
await listen('tauri://drag-drop', handleDragDrop)
setupVisualFeedback(compressBtn)
setupVisualFeedback(decompressBtn)
}
async function handleDragDrop(event) {
const { paths, position } = event.payload;
const { paths, position } = event.payload
if (!paths?.length) return;
if (!paths?.length) return
const allFiles = await collectFiles(paths);
const allFiles = await collectFiles(paths)
if (allFiles.length === 0) {
addLog('未找到 .exe 或 .dll 文件', 'warning');
return;
addLog('未找到 .exe 或 .dll 文件', 'warning')
return
}
await processDropByTarget(allFiles, position);
await processDropByTarget(allFiles, position)
}
async function collectFiles(paths) {
const allFiles = [];
const allFiles = []
for (const path of paths) {
const files = await checkAndScanPath(path);
allFiles.push(...files);
const files = await checkAndScanPath(path)
allFiles.push(...files)
}
return allFiles;
return allFiles
}
async function processDropByTarget(files, position) {
const dropTarget = getDropTarget(position);
const dropTarget = getDropTarget(position)
if (dropTarget === 'compress') {
addLog('检测到拖放至加壳区域', 'info');
await processBatchFiles(files, 'compress');
addLog('检测到拖放至加壳区域', 'info')
await processBatchFiles(files, 'compress')
} else if (dropTarget === 'decompress') {
addLog('检测到拖放至脱壳区域', 'info');
await processBatchFiles(files, 'decompress');
addLog('检测到拖放至脱壳区域', 'info')
await processBatchFiles(files, 'decompress')
} else {
storeFilesForLater(files);
storeFilesForLater(files)
}
}
function storeFilesForLater(files) {
if (files.length === 1) {
addLog('请点击"加壳压缩"或"脱壳解压"按钮', 'info');
window.droppedFile = files[0];
addLog('请点击"加壳压缩"或"脱壳解压"按钮', 'info')
window.droppedFile = files[0]
} else {
addLog(`已选择 ${files.length} 个文件,请点击操作按钮`, 'info');
window.droppedFiles = files;
addLog(`已选择 ${files.length} 个文件,请点击操作按钮`, 'info')
window.droppedFiles = files
}
}
// 缓存按钮位置信息
let cachedButtonRects = null;
let cachedButtonRects = null
// 更新按钮位置缓存
function updateButtonRectsCache() {
cachedButtonRects = {
compress: compressBtn.getBoundingClientRect(),
decompress: decompressBtn.getBoundingClientRect()
};
decompress: decompressBtn.getBoundingClientRect(),
}
}
function isPointInRect(x, y, rect) {
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom
}
function getDropTarget(position) {
if (!position) return null;
if (!position) return null
if (!cachedButtonRects) {
updateButtonRectsCache();
updateButtonRectsCache()
}
const { x, y } = position;
const { x, y } = position
if (isPointInRect(x, y, cachedButtonRects.compress)) {
return 'compress';
return 'compress'
}
if (isPointInRect(x, y, cachedButtonRects.decompress)) {
return 'decompress';
return 'decompress'
}
return null;
return null
}
// 为元素设置视觉反馈
function setupVisualFeedback(element) {
const handleDragEnter = (e) => {
e.preventDefault();
element.classList.add('drag-over');
};
e.preventDefault()
element.classList.add('drag-over')
}
const handleDragLeave = () => {
element.classList.remove('drag-over');
};
element.classList.remove('drag-over')
}
// 使用被动事件监听器优化性能
element.addEventListener('dragenter', handleDragEnter, { passive: false });
element.addEventListener('dragover', handleDragEnter, { passive: false });
element.addEventListener('dragleave', handleDragLeave, { passive: true });
element.addEventListener('drop', handleDragLeave, { passive: true });
element.addEventListener('dragenter', handleDragEnter, { passive: false })
element.addEventListener('dragover', handleDragEnter, { passive: false })
element.addEventListener('dragleave', handleDragLeave, { passive: true })
element.addEventListener('drop', handleDragLeave, { passive: true })
}
async function handleCompress() {
await handleFileSelect('compress');
await handleFileSelect('compress')
}
async function handleDecompress() {
await handleFileSelect('decompress');
await handleFileSelect('decompress')
}
async function handleFileSelect(mode) {
try {
const selected = await open({
multiple: true,
filters: [{ name: '可执行文件', extensions: ['exe', 'dll'] }]
});
filters: [{ name: '可执行文件', extensions: ['exe', 'dll'] }],
})
if (!selected || (Array.isArray(selected) && selected.length === 0)) {
addLog('未选择文件', 'warning');
return;
addLog('未选择文件', 'warning')
return
}
const files = Array.isArray(selected) ? selected : [selected];
const files = Array.isArray(selected) ? selected : [selected]
if (files.length === 1) {
addLog(`选择文件: ${files[0]}`, 'info');
const handler = mode === 'compress' ? handleCompressWithFile : handleDecompressWithFile;
await handler(files[0]);
addLog(`选择文件: ${files[0]}`, 'info')
const handler = mode === 'compress' ? handleCompressWithFile : handleDecompressWithFile
await handler(files[0])
} else {
addLog(`选择了 ${files.length} 个文件`, 'info');
await processBatchFiles(files, mode);
addLog(`选择了 ${files.length} 个文件`, 'info')
await processBatchFiles(files, mode)
}
} catch (error) {
addLog(`操作失败: ${error}`, 'error');
addLog(`操作失败: ${error}`, 'error')
}
}
async function handleCompressWithFile(inputFile) {
try {
let outputFile;
let outputFile
if (overwriteCheckbox.checked) {
outputFile = inputFile;
addLog('将覆盖原文件', 'info');
outputFile = inputFile
addLog('将覆盖原文件', 'info')
} else {
const ext = inputFile.substring(inputFile.lastIndexOf('.'));
const baseName = inputFile.substring(0, inputFile.lastIndexOf('.'));
const defaultOutput = `${baseName}_packed${ext}`;
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
});
defaultPath: defaultOutput,
})
if (!outputFile) {
addLog('未选择输出位置', 'warning');
return;
addLog('未选择输出位置', 'warning')
return
}
addLog(`输出文件: ${outputFile}`, 'info');
addLog(`输出文件: ${outputFile}`, 'info')
}
await processUpx('compress', inputFile, outputFile);
await processUpx('compress', inputFile, outputFile)
} catch (error) {
addLog(`操作失败: ${error}`, 'error');
addLog(`操作失败: ${error}`, 'error')
}
}
async function handleDecompressWithFile(inputFile) {
try {
addLog('将覆盖原文件', 'info');
await processUpx('decompress', inputFile, inputFile);
addLog('将覆盖原文件', 'info')
await processUpx('decompress', inputFile, inputFile)
} catch (error) {
addLog(`操作失败: ${error}`, 'error');
addLog(`操作失败: ${error}`, 'error')
}
}
@@ -480,25 +496,24 @@ async function processUpx(mode, inputFile, outputFile) {
compression_level: getCompressionLevel(),
backup: backupCheckbox.checked,
ultra_brute: ultraBruteCheckbox.checked,
force: forceCompressCheckbox.checked
};
force: forceCompressCheckbox.checked,
}
if (ultraBruteCheckbox.checked) {
addLog('已启用极限压缩模式', 'info');
addLog('已启用极限压缩模式', 'info')
}
if (forceCompressCheckbox.checked) {
addLog('已启用强制压缩模式', 'warning');
addLog('已启用强制压缩模式', 'warning')
}
const actionName = mode === 'compress' ? '加壳压缩' : '脱壳解压';
addLog(`开始${actionName}...`, 'info');
const result = await invoke('process_upx', { options });
parseProcessResult(result);
const actionName = mode === 'compress' ? '加壳压缩' : '脱壳解压'
addLog(`开始${actionName}...`, 'info')
const result = await invoke('process_upx', { options })
parseProcessResult(result)
} catch (error) {
parseProcessError(String(error));
parseProcessError(String(error))
}
}
@@ -507,101 +522,99 @@ function parseProcessResult(result) {
{ patterns: ['操作成功', '操作完成'], type: 'success', highlight: true },
{ patterns: ['输出:', '大小:', '压缩率:'], type: 'success', highlight: false },
{ patterns: ['UPX 输出:'], type: 'info', highlight: false },
{ patterns: ['扫描', '检测'], type: 'warning', highlight: false }
];
{ patterns: ['扫描', '检测'], type: 'warning', highlight: false },
]
result.split('\n').forEach((line) => {
if (!line.trim()) return;
if (!line.trim()) return
const match = LOG_PATTERNS.find(({ patterns }) =>
patterns.some(p => line.includes(p))
);
const match = LOG_PATTERNS.find(({ patterns }) => patterns.some((p) => line.includes(p)))
if (match) {
addLog(line, match.type, match.highlight);
addLog(line, match.type, match.highlight)
} else {
addLog(line, 'info');
addLog(line, 'info')
}
});
})
}
function parseProcessError(errorMsg) {
const ERROR_PATTERNS = [
{ test: (s) => s.includes('[错误]'), type: 'error' },
{ test: (s) => s.includes('解决方案:') || s.includes('可能原因:'), type: 'warning' },
{ test: (s) => s.trim().startsWith('-'), type: 'info' }
];
{ test: (s) => s.trim().startsWith('-'), type: 'info' },
]
errorMsg.split('\n').forEach((line, index) => {
if (!line.trim()) return;
if (!line.trim()) return
const match = ERROR_PATTERNS.find(({ test }) => test(line));
const match = ERROR_PATTERNS.find(({ test }) => test(line))
if (match) {
addLog(line, match.type);
addLog(line, match.type)
} else {
addLog(line, index === 0 ? 'error' : 'warning');
addLog(line, index === 0 ? 'error' : 'warning')
}
});
})
}
// 刷新图标缓存
async function handleRefreshIcon() {
try {
addLog('正在刷新图标缓存...', 'info');
await invoke('refresh_icon_cache');
addLog('图标缓存刷新完成', 'success');
addLog('正在刷新图标缓存...', 'info')
await invoke('refresh_icon_cache')
addLog('图标缓存刷新完成', 'success')
} catch (error) {
addLog(`刷新失败: ${error}`, 'error');
addLog(`刷新失败: ${error}`, 'error')
}
}
// 日志初始化标志
let logInitialized = false;
let logInitialized = false
// 日志管理配置
const LOG_CONFIG = {
MAX_LOGS: 1000, // 最大日志条数
TRIM_COUNT: 200 // 超出时一次删除的条数
};
MAX_LOGS: 1000, // 最大日志条数
TRIM_COUNT: 200, // 超出时一次删除的条数
}
// 添加日志
function addLog(message, type = 'info', highlight = false) {
const logLine = document.createElement('div');
logLine.className = `log-line log-${type} fade-in${highlight ? ' log-highlight' : ''}`;
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'
});
second: '2-digit',
})
logLine.textContent = `[${timestamp}] ${message}`;
logLine.textContent = `[${timestamp}] ${message}`
// 清除初始提示(只执行一次)
if (!logInitialized) {
logOutput.innerHTML = '';
logInitialized = true;
logOutput.innerHTML = ''
logInitialized = true
}
logOutput.appendChild(logLine);
logOutput.appendChild(logLine)
// 日志数量管理:超过最大值时删除旧日志
const logCount = logOutput.children.length;
const logCount = logOutput.children.length
if (logCount > LOG_CONFIG.MAX_LOGS) {
// 批量删除旧日志以提升性能
for (let i = 0; i < LOG_CONFIG.TRIM_COUNT; i++) {
if (logOutput.firstChild) {
logOutput.removeChild(logOutput.firstChild);
logOutput.removeChild(logOutput.firstChild)
}
}
}
// 使用 requestAnimationFrame 优化滚动性能
requestAnimationFrame(() => {
logOutput.scrollTop = logOutput.scrollHeight;
});
logOutput.scrollTop = logOutput.scrollHeight
})
}
async function saveCurrentConfig() {
@@ -612,33 +625,32 @@ async function saveCurrentConfig() {
backup: backupCheckbox.checked,
ultra_brute: ultraBruteCheckbox.checked,
include_subfolders: includeSubfoldersCheckbox.checked,
force_compress: forceCompressCheckbox.checked
};
force_compress: forceCompressCheckbox.checked,
}
await invoke('save_config', { config });
await invoke('save_config', { config })
} catch (error) {
console.error('保存配置失败:', error);
console.error('保存配置失败:', error)
}
}
function applyConfigToUI(config) {
compressionLevel.value = config.compression_level;
overwriteCheckbox.checked = config.overwrite;
backupCheckbox.checked = config.backup;
ultraBruteCheckbox.checked = config.ultra_brute;
includeSubfoldersCheckbox.checked = config.include_subfolders;
forceCompressCheckbox.checked = config.force_compress;
updateLevelDisplay(config.compression_level);
compressionLevel.value = config.compression_level
overwriteCheckbox.checked = config.overwrite
backupCheckbox.checked = config.backup
ultraBruteCheckbox.checked = config.ultra_brute
includeSubfoldersCheckbox.checked = config.include_subfolders
forceCompressCheckbox.checked = config.force_compress
updateLevelDisplay(config.compression_level)
}
async function loadSavedConfig() {
try {
const config = await invoke('load_config');
applyConfigToUI(config);
addLog('已加载上次保存的配置', 'info');
const config = await invoke('load_config')
applyConfigToUI(config)
addLog('已加载上次保存的配置', 'info')
} catch (error) {
console.error('加载配置失败:', error);
addLog('使用默认配置', 'info');
console.error('加载配置失败:', error)
addLog('使用默认配置', 'info')
}
}