Claude commited on
Commit
7b4204f
·
unverified ·
1 Parent(s): 49bdcf2

重构前端架构并实现4套主题切换功能

Browse files

主要改进:
- 实现前后端分离架构,将HTML/CSS/JS从Python中独立出来
- 创建标准的静态资源目录结构(static/css, static/js, templates)
- 实现4套可切换主题:Apple、Glassmorphism、Minimal、Tech
- 添加主题持久化(localStorage)和键盘快捷键支持
- 增强交互功能:拖拽上传、实时参数验证、文件大小预检查

技术细节:
- main.py: 简化代码,移除内联HTML模板(-307行,-39%)
- 新增 templates/index.html: 纯HTML结构模板
- 新增 static/css/themes.css: 使用CSS变量实现主题切换
- 新增 static/js/app.js: 所有前端交互逻辑
- requirements.txt: 新增jinja2依赖

改进效果:
- 代码可维护性显著提升
- 支持静态资源CDN缓存
- 完整的IDE语法支持
- 模块化架构便于扩展

Files changed (5) hide show
  1. main.py +15 -312
  2. requirements.txt +2 -1
  3. static/css/themes.css +556 -0
  4. static/js/app.js +252 -0
  5. templates/index.html +103 -0
main.py CHANGED
@@ -21,9 +21,12 @@ from fastapi import (
21
  HTTPException,
22
  BackgroundTasks,
23
  Path,
24
- Form
 
25
  )
26
  from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
 
 
27
  import subprocess
28
  import asyncio
29
  import tempfile
@@ -63,6 +66,12 @@ app = FastAPI(
63
  version="4.0.0"
64
  )
65
 
 
 
 
 
 
 
66
  # 启动时确保临时目录存在
67
  os.makedirs(TEMP_DIR, exist_ok=True)
68
 
@@ -103,321 +112,15 @@ def cleanup_temp_dir(temp_dir: str):
103
  except Exception as cleanup_error:
104
  logger.error(f"后台清理:删除 {temp_dir} 失败: {cleanup_error}", exc_info=True)
105
 
106
- # --- 5. HTML 模板 ---
107
-
108
- HTML_UPLOAD_PAGE = """
109
- <!DOCTYPE html>
110
- <html lang="zh-CN">
111
- <head>
112
- <meta charset="UTF-8">
113
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
114
- <title>Magick 图像转换器</title>
115
- <style>
116
- * { box-sizing: border-box; margin: 0; padding: 0; }
117
- body {
118
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
119
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
120
- min-height: 100vh;
121
- padding: 20px;
122
- display: flex;
123
- align-items: center;
124
- justify-content: center;
125
- }
126
- .container {
127
- background: white;
128
- border-radius: 20px;
129
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
130
- max-width: 600px;
131
- width: 100%;
132
- padding: 40px;
133
- }
134
- h1 {
135
- color: #333;
136
- margin-bottom: 10px;
137
- font-size: 28px;
138
- text-align: center;
139
- }
140
- .subtitle {
141
- color: #666;
142
- text-align: center;
143
- margin-bottom: 30px;
144
- font-size: 14px;
145
- }
146
- .form-group {
147
- margin-bottom: 25px;
148
- }
149
- label {
150
- display: block;
151
- color: #333;
152
- font-weight: 600;
153
- margin-bottom: 8px;
154
- font-size: 14px;
155
- }
156
- .file-input-wrapper {
157
- position: relative;
158
- border: 2px dashed #667eea;
159
- border-radius: 10px;
160
- padding: 30px;
161
- text-align: center;
162
- background: #f8f9ff;
163
- cursor: pointer;
164
- transition: all 0.3s;
165
- }
166
- .file-input-wrapper:hover {
167
- border-color: #764ba2;
168
- background: #f0f2ff;
169
- }
170
- .file-input-wrapper input[type="file"] {
171
- position: absolute;
172
- width: 100%;
173
- height: 100%;
174
- top: 0;
175
- left: 0;
176
- opacity: 0;
177
- cursor: pointer;
178
- }
179
- .file-label {
180
- color: #667eea;
181
- font-weight: 600;
182
- }
183
- select, input[type="range"] {
184
- width: 100%;
185
- padding: 12px;
186
- border: 2px solid #e0e0e0;
187
- border-radius: 8px;
188
- font-size: 14px;
189
- transition: border-color 0.3s;
190
- }
191
- select:focus {
192
- outline: none;
193
- border-color: #667eea;
194
- }
195
- .radio-group {
196
- display: flex;
197
- gap: 20px;
198
- }
199
- .radio-label {
200
- display: flex;
201
- align-items: center;
202
- cursor: pointer;
203
- font-weight: normal;
204
- }
205
- .radio-label input[type="radio"] {
206
- margin-right: 8px;
207
- cursor: pointer;
208
- }
209
- .slider-container {
210
- display: flex;
211
- align-items: center;
212
- gap: 15px;
213
- }
214
- input[type="range"] {
215
- flex: 1;
216
- }
217
- .slider-value {
218
- min-width: 45px;
219
- text-align: center;
220
- font-weight: 600;
221
- color: #667eea;
222
- font-size: 18px;
223
- }
224
- .param-hint {
225
- background: #f0f2ff;
226
- padding: 12px;
227
- border-radius: 8px;
228
- font-size: 13px;
229
- color: #555;
230
- margin-top: 10px;
231
- border-left: 4px solid #667eea;
232
- }
233
- .submit-btn {
234
- width: 100%;
235
- padding: 15px;
236
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
237
- color: white;
238
- border: none;
239
- border-radius: 10px;
240
- font-size: 16px;
241
- font-weight: 600;
242
- cursor: pointer;
243
- transition: transform 0.2s, box-shadow 0.2s;
244
- }
245
- .submit-btn:hover {
246
- transform: translateY(-2px);
247
- box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
248
- }
249
- .submit-btn:active {
250
- transform: translateY(0);
251
- }
252
- .links {
253
- margin-top: 25px;
254
- text-align: center;
255
- padding-top: 25px;
256
- border-top: 1px solid #e0e0e0;
257
- }
258
- .links a {
259
- color: #667eea;
260
- text-decoration: none;
261
- margin: 0 15px;
262
- font-size: 14px;
263
- font-weight: 500;
264
- }
265
- .links a:hover {
266
- text-decoration: underline;
267
- }
268
- .selected-file {
269
- margin-top: 10px;
270
- color: #28a745;
271
- font-size: 13px;
272
- font-weight: 500;
273
- }
274
- </style>
275
- </head>
276
- <body>
277
- <div class="container">
278
- <h1>🧙‍♂️ Magick 图像转换器</h1>
279
- <p class="subtitle">支持多格式转换 | 有损/无损模式 | 支持动画图像</p>
280
-
281
- <form id="uploadForm" action="/" method="POST" enctype="multipart/form-data">
282
- <div class="form-group">
283
- <label>选择图像文件</label>
284
- <div class="file-input-wrapper">
285
- <input type="file" name="file" id="fileInput" accept="image/*" required>
286
- <div class="file-label">
287
- 📁 点击选择或拖拽文件到此处
288
- <div style="font-size: 12px; color: #999; margin-top: 8px;">
289
- 支持 JPG, PNG, GIF, WebP, AVIF, HEIF 等格式
290
- </div>
291
- </div>
292
- </div>
293
- <div id="selectedFile" class="selected-file"></div>
294
- </div>
295
-
296
- <div class="form-group">
297
- <label for="target_format">目标格式</label>
298
- <select name="target_format" id="target_format" required>
299
- <option value="webp">WebP - 现代高效格式</option>
300
- <option value="avif">AVIF - 最新一代格式</option>
301
- <option value="jpeg">JPEG - 经典有损格式</option>
302
- <option value="png">PNG - 无损格式</option>
303
- <option value="gif">GIF - 动画格式</option>
304
- <option value="heif" selected>HEIF - 高效图像格式</option>
305
- </select>
306
- </div>
307
-
308
- <div class="form-group">
309
- <label>转换模式</label>
310
- <div class="radio-group">
311
- <label class="radio-label">
312
- <input type="radio" name="mode" value="lossy">
313
- 有损压缩 (更小体积)
314
- </label>
315
- <label class="radio-label">
316
- <input type="radio" name="mode" value="lossless" checked>
317
- 无损压缩 (保持质量)
318
- </label>
319
- </div>
320
- </div>
321
-
322
- <div class="form-group">
323
- <label for="setting">质量参数</label>
324
- <div class="slider-container">
325
- <input type="range" name="setting" id="setting" min="0" max="100" value="0">
326
- <span class="slider-value" id="settingValue">0</span>
327
- </div>
328
- <div class="param-hint" id="paramHint">
329
- 压缩速度: 0 - 最慢/最佳压缩 (0=最慢/最佳,100=最快/最差)
330
- </div>
331
- </div>
332
-
333
- <button type="submit" class="submit-btn">🚀 开始转换</button>
334
- </form>
335
-
336
- <div class="links">
337
- <a href="/docs" target="_blank">📖 API 文档</a>
338
- <a href="/health" target="_blank">🏥 健康检查</a>
339
- </div>
340
- </div>
341
-
342
- <script>
343
- // 文件选择提示
344
- const fileInput = document.getElementById('fileInput');
345
- const selectedFile = document.getElementById('selectedFile');
346
-
347
- fileInput.addEventListener('change', function() {
348
- if (this.files.length > 0) {
349
- selectedFile.textContent = '✓ 已选择: ' + this.files[0].name;
350
- }
351
- });
352
-
353
- // 滑块实时更新
354
- const slider = document.getElementById('setting');
355
- const sliderValue = document.getElementById('settingValue');
356
- const paramHint = document.getElementById('paramHint');
357
- const modeRadios = document.querySelectorAll('input[name="mode"]');
358
-
359
- function updateHint() {
360
- const mode = document.querySelector('input[name="mode"]:checked').value;
361
- const value = slider.value;
362
- sliderValue.textContent = value;
363
-
364
- if (mode === 'lossy') {
365
- let quality = '中等';
366
- if (value >= 90) quality = '极高';
367
- else if (value >= 80) quality = '高';
368
- else if (value >= 60) quality = '中等';
369
- else if (value >= 40) quality = '中低';
370
- else quality = '低';
371
- paramHint.textContent = `质量: ${value} - ${quality}质量 (0=最低质量,100=最高质量)`;
372
- } else {
373
- let speed = '平衡';
374
- if (value <= 20) speed = '最慢/最佳压缩';
375
- else if (value <= 40) speed = '较慢/较好压缩';
376
- else if (value <= 60) speed = '平衡';
377
- else if (value <= 80) speed = '较快/较差压缩';
378
- else speed = '最快/最差压缩';
379
- paramHint.textContent = `压缩速度: ${value} - ${speed} (0=最慢/最佳,100=最快/最差)`;
380
- }
381
- }
382
-
383
- slider.addEventListener('input', updateHint);
384
-
385
- // 当模式切换时,自动调整质量值
386
- modeRadios.forEach(radio => radio.addEventListener('change', function() {
387
- const mode = document.querySelector('input[name="mode"]:checked').value;
388
- if (mode === 'lossless') {
389
- // 无损模式:默认最佳质量(0=最慢/最佳压缩)
390
- slider.value = 0;
391
- } else {
392
- // 有损模式:默认中等质量(50=中等质量)
393
- slider.value = 50;
394
- }
395
- updateHint();
396
- }));
397
-
398
- // 表单提交处理
399
- const form = document.getElementById('uploadForm');
400
- const submitBtn = form.querySelector('.submit-btn');
401
- const originalBtnText = submitBtn.textContent;
402
-
403
- form.addEventListener('submit', function() {
404
- submitBtn.textContent = '⏳ 转换中...';
405
- submitBtn.disabled = true;
406
- });
407
- </script>
408
- </body>
409
- </html>
410
- """
411
-
412
- # --- 6. API 端点 ---
413
 
414
- @app.get("/", response_class=HTMLResponse, summary="上传界面")
415
- async def root():
416
  """
417
  返回用户友好的HTML上传表单页面。
418
- 提供图形化界面进行图像转换,无需编程知识。
419
  """
420
- return HTML_UPLOAD_PAGE
421
 
422
  @app.get("/health", summary="服务健康检查")
423
  async def health_check():
 
21
  HTTPException,
22
  BackgroundTasks,
23
  Path,
24
+ Form,
25
+ Request
26
  )
27
  from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
28
+ from fastapi.staticfiles import StaticFiles
29
+ from fastapi.templating import Jinja2Templates
30
  import subprocess
31
  import asyncio
32
  import tempfile
 
66
  version="4.0.0"
67
  )
68
 
69
+ # 挂载静态文件目录(CSS、JS等)
70
+ app.mount("/static", StaticFiles(directory="static"), name="static")
71
+
72
+ # 配置模板引擎
73
+ templates = Jinja2Templates(directory="templates")
74
+
75
  # 启动时确保临时目录存在
76
  os.makedirs(TEMP_DIR, exist_ok=True)
77
 
 
112
  except Exception as cleanup_error:
113
  logger.error(f"后台清理:删除 {temp_dir} 失败: {cleanup_error}", exc_info=True)
114
 
115
+ # --- 5. API 端点 ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ @app.get("/", summary="上传界面")
118
+ async def root(request: Request):
119
  """
120
  返回用户友好的HTML上传表单页面。
121
+ 提供图形化界面进行图像转换,支持4套主题切换。
122
  """
123
+ return templates.TemplateResponse("index.html", {"request": request})
124
 
125
  @app.get("/health", summary="服务健康检查")
126
  async def health_check():
requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
  fastapi
2
  uvicorn[standard]
3
- python-multipart
 
 
1
  fastapi
2
  uvicorn[standard]
3
+ python-multipart
4
+ jinja2
static/css/themes.css ADDED
@@ -0,0 +1,556 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ========================================
2
+ 基础样式重置
3
+ ======================================== */
4
+ * {
5
+ box-sizing: border-box;
6
+ margin: 0;
7
+ padding: 0;
8
+ }
9
+
10
+ /* ========================================
11
+ 主题 1: Apple Style (经典苹果风)
12
+ ======================================== */
13
+ :root,
14
+ [data-theme="apple"] {
15
+ --bg-primary: #f5f5f7;
16
+ --bg-container: #ffffff;
17
+ --bg-input: #ffffff;
18
+ --bg-input-hover: #f9f9f9;
19
+ --bg-hint: #f5f5f7;
20
+
21
+ --color-primary: #0071e3;
22
+ --color-primary-hover: #0077ed;
23
+ --color-accent: #06c;
24
+ --color-success: #30d158;
25
+
26
+ --text-primary: #1d1d1f;
27
+ --text-secondary: #86868b;
28
+ --text-hint: #6e6e73;
29
+
30
+ --border-color: #d2d2d7;
31
+ --border-focus: #0071e3;
32
+
33
+ --shadow-sm: 0 2px 16px rgba(0, 0, 0, 0.08);
34
+ --shadow-md: 0 4px 24px rgba(0, 0, 0, 0.12);
35
+ --shadow-hover: 0 8px 32px rgba(0, 0, 0, 0.16);
36
+
37
+ --radius-sm: 8px;
38
+ --radius-md: 12px;
39
+ --radius-lg: 18px;
40
+
41
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
42
+ }
43
+
44
+ /* ========================================
45
+ 主题 2: Glassmorphism (玻璃拟态)
46
+ ======================================== */
47
+ [data-theme="glass"] {
48
+ --bg-primary: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
49
+ --bg-container: rgba(255, 255, 255, 0.1);
50
+ --bg-input: rgba(255, 255, 255, 0.05);
51
+ --bg-input-hover: rgba(255, 255, 255, 0.15);
52
+ --bg-hint: rgba(255, 255, 255, 0.08);
53
+
54
+ --color-primary: #00d4ff;
55
+ --color-primary-hover: #00bfea;
56
+ --color-accent: #0099ff;
57
+ --color-success: #00ff88;
58
+
59
+ --text-primary: #ffffff;
60
+ --text-secondary: rgba(255, 255, 255, 0.7);
61
+ --text-hint: rgba(255, 255, 255, 0.6);
62
+
63
+ --border-color: rgba(255, 255, 255, 0.18);
64
+ --border-focus: rgba(255, 255, 255, 0.4);
65
+
66
+ --shadow-sm: 0 8px 32px rgba(0, 0, 0, 0.3);
67
+ --shadow-md: 0 12px 48px rgba(0, 0, 0, 0.4);
68
+ --shadow-hover: 0 16px 64px rgba(0, 0, 0, 0.5);
69
+
70
+ --radius-sm: 12px;
71
+ --radius-md: 16px;
72
+ --radius-lg: 20px;
73
+
74
+ --transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
75
+
76
+ --glass-blur: blur(10px);
77
+ }
78
+
79
+ /* ========================================
80
+ 主题 3: Minimal Monochrome (极简黑白)
81
+ ======================================== */
82
+ [data-theme="minimal"] {
83
+ --bg-primary: #fafafa;
84
+ --bg-container: #ffffff;
85
+ --bg-input: #ffffff;
86
+ --bg-input-hover: #f5f5f5;
87
+ --bg-hint: #f9f9f9;
88
+
89
+ --color-primary: #0066ff;
90
+ --color-primary-hover: #0052cc;
91
+ --color-accent: #000000;
92
+ --color-success: #000000;
93
+
94
+ --text-primary: #000000;
95
+ --text-secondary: #666666;
96
+ --text-hint: #999999;
97
+
98
+ --border-color: #e5e5e5;
99
+ --border-focus: #000000;
100
+
101
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04);
102
+ --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08);
103
+ --shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.12);
104
+
105
+ --radius-sm: 4px;
106
+ --radius-md: 6px;
107
+ --radius-lg: 8px;
108
+
109
+ --transition: all 0.2s ease;
110
+ }
111
+
112
+ /* ========================================
113
+ 主题 4: Tech Gradient (现代科技)
114
+ ======================================== */
115
+ [data-theme="tech"] {
116
+ --bg-primary: linear-gradient(135deg, #e0e7ff 0%, #f0f4f8 100%);
117
+ --bg-container: rgba(255, 255, 255, 0.9);
118
+ --bg-input: #ffffff;
119
+ --bg-input-hover: #f8fafc;
120
+ --bg-hint: #f1f5f9;
121
+
122
+ --color-primary: #3b82f6;
123
+ --color-primary-hover: #2563eb;
124
+ --color-accent: #6366f1;
125
+ --color-success: #10b981;
126
+
127
+ --text-primary: #1e293b;
128
+ --text-secondary: #64748b;
129
+ --text-hint: #94a3b8;
130
+
131
+ --border-color: #cbd5e1;
132
+ --border-focus: #3b82f6;
133
+
134
+ --shadow-sm: 0 4px 12px rgba(59, 130, 246, 0.1);
135
+ --shadow-md: 0 8px 24px rgba(59, 130, 246, 0.15);
136
+ --shadow-hover: 0 12px 32px rgba(59, 130, 246, 0.2);
137
+
138
+ --radius-sm: 10px;
139
+ --radius-md: 14px;
140
+ --radius-lg: 18px;
141
+
142
+ --transition: all 0.3s ease-out;
143
+ }
144
+
145
+ /* ========================================
146
+ 全局布局
147
+ ======================================== */
148
+ body {
149
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
150
+ background: var(--bg-primary);
151
+ min-height: 100vh;
152
+ padding: 20px;
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ transition: var(--transition);
157
+ }
158
+
159
+ /* 玻璃主题特殊处理 */
160
+ [data-theme="glass"] body,
161
+ [data-theme="tech"] body {
162
+ background-image: var(--bg-primary);
163
+ }
164
+
165
+ .container {
166
+ background: var(--bg-container);
167
+ border-radius: var(--radius-lg);
168
+ box-shadow: var(--shadow-md);
169
+ max-width: 600px;
170
+ width: 100%;
171
+ padding: 40px;
172
+ transition: var(--transition);
173
+ }
174
+
175
+ /* 玻璃效果 */
176
+ [data-theme="glass"] .container {
177
+ backdrop-filter: var(--glass-blur);
178
+ -webkit-backdrop-filter: var(--glass-blur);
179
+ border: 1px solid var(--border-color);
180
+ }
181
+
182
+ /* ========================================
183
+ 主题切换器
184
+ ======================================== */
185
+ .theme-switcher {
186
+ position: fixed;
187
+ top: 20px;
188
+ right: 20px;
189
+ display: flex;
190
+ gap: 8px;
191
+ background: var(--bg-container);
192
+ padding: 8px;
193
+ border-radius: var(--radius-md);
194
+ box-shadow: var(--shadow-sm);
195
+ z-index: 1000;
196
+ }
197
+
198
+ [data-theme="glass"] .theme-switcher {
199
+ backdrop-filter: var(--glass-blur);
200
+ -webkit-backdrop-filter: var(--glass-blur);
201
+ border: 1px solid var(--border-color);
202
+ }
203
+
204
+ .theme-btn {
205
+ display: flex;
206
+ flex-direction: column;
207
+ align-items: center;
208
+ gap: 4px;
209
+ padding: 10px 12px;
210
+ background: transparent;
211
+ border: 2px solid transparent;
212
+ border-radius: var(--radius-sm);
213
+ cursor: pointer;
214
+ transition: var(--transition);
215
+ color: var(--text-primary);
216
+ }
217
+
218
+ .theme-btn:hover {
219
+ background: var(--bg-input-hover);
220
+ border-color: var(--color-primary);
221
+ }
222
+
223
+ .theme-btn.active {
224
+ background: var(--color-primary);
225
+ color: white;
226
+ border-color: var(--color-primary);
227
+ }
228
+
229
+ .theme-icon {
230
+ font-size: 20px;
231
+ }
232
+
233
+ .theme-name {
234
+ font-size: 11px;
235
+ font-weight: 600;
236
+ }
237
+
238
+ /* ========================================
239
+ 标题和文字
240
+ ======================================== */
241
+ h1 {
242
+ color: var(--text-primary);
243
+ margin-bottom: 10px;
244
+ font-size: 28px;
245
+ text-align: center;
246
+ font-weight: 700;
247
+ transition: var(--transition);
248
+ }
249
+
250
+ .subtitle {
251
+ color: var(--text-secondary);
252
+ text-align: center;
253
+ margin-bottom: 30px;
254
+ font-size: 14px;
255
+ transition: var(--transition);
256
+ }
257
+
258
+ /* ========================================
259
+ 表单元素
260
+ ======================================== */
261
+ .form-group {
262
+ margin-bottom: 25px;
263
+ }
264
+
265
+ label {
266
+ display: block;
267
+ color: var(--text-primary);
268
+ font-weight: 600;
269
+ margin-bottom: 8px;
270
+ font-size: 14px;
271
+ transition: var(--transition);
272
+ }
273
+
274
+ /* 文件上传区域 */
275
+ .file-input-wrapper {
276
+ position: relative;
277
+ border: 2px dashed var(--border-color);
278
+ border-radius: var(--radius-md);
279
+ padding: 30px;
280
+ text-align: center;
281
+ background: var(--bg-input);
282
+ cursor: pointer;
283
+ transition: var(--transition);
284
+ }
285
+
286
+ .file-input-wrapper:hover {
287
+ border-color: var(--color-primary);
288
+ background: var(--bg-input-hover);
289
+ }
290
+
291
+ .file-input-wrapper input[type="file"] {
292
+ position: absolute;
293
+ width: 100%;
294
+ height: 100%;
295
+ top: 0;
296
+ left: 0;
297
+ opacity: 0;
298
+ cursor: pointer;
299
+ }
300
+
301
+ .file-label {
302
+ color: var(--color-primary);
303
+ font-weight: 600;
304
+ transition: var(--transition);
305
+ }
306
+
307
+ .file-hint {
308
+ font-size: 12px;
309
+ color: var(--text-hint);
310
+ margin-top: 8px;
311
+ }
312
+
313
+ .selected-file {
314
+ margin-top: 10px;
315
+ color: var(--color-success);
316
+ font-size: 13px;
317
+ font-weight: 500;
318
+ }
319
+
320
+ /* 下拉选择框 */
321
+ select {
322
+ width: 100%;
323
+ padding: 12px;
324
+ border: 2px solid var(--border-color);
325
+ border-radius: var(--radius-sm);
326
+ font-size: 14px;
327
+ background: var(--bg-input);
328
+ color: var(--text-primary);
329
+ transition: var(--transition);
330
+ cursor: pointer;
331
+ }
332
+
333
+ select:focus {
334
+ outline: none;
335
+ border-color: var(--border-focus);
336
+ }
337
+
338
+ /* 单选按钮组 */
339
+ .radio-group {
340
+ display: flex;
341
+ gap: 20px;
342
+ flex-wrap: wrap;
343
+ }
344
+
345
+ .radio-label {
346
+ display: flex;
347
+ align-items: center;
348
+ cursor: pointer;
349
+ font-weight: normal;
350
+ color: var(--text-primary);
351
+ transition: var(--transition);
352
+ }
353
+
354
+ .radio-label input[type="radio"] {
355
+ margin-right: 8px;
356
+ cursor: pointer;
357
+ accent-color: var(--color-primary);
358
+ }
359
+
360
+ /* 滑块 */
361
+ .slider-container {
362
+ display: flex;
363
+ align-items: center;
364
+ gap: 15px;
365
+ }
366
+
367
+ input[type="range"] {
368
+ flex: 1;
369
+ height: 6px;
370
+ border-radius: 3px;
371
+ background: var(--border-color);
372
+ outline: none;
373
+ -webkit-appearance: none;
374
+ }
375
+
376
+ input[type="range"]::-webkit-slider-thumb {
377
+ -webkit-appearance: none;
378
+ appearance: none;
379
+ width: 20px;
380
+ height: 20px;
381
+ border-radius: 50%;
382
+ background: var(--color-primary);
383
+ cursor: pointer;
384
+ transition: var(--transition);
385
+ }
386
+
387
+ input[type="range"]::-webkit-slider-thumb:hover {
388
+ transform: scale(1.2);
389
+ box-shadow: 0 0 0 8px rgba(0, 113, 227, 0.1);
390
+ }
391
+
392
+ input[type="range"]::-moz-range-thumb {
393
+ width: 20px;
394
+ height: 20px;
395
+ border-radius: 50%;
396
+ background: var(--color-primary);
397
+ cursor: pointer;
398
+ border: none;
399
+ }
400
+
401
+ .slider-value {
402
+ min-width: 45px;
403
+ text-align: center;
404
+ font-weight: 700;
405
+ color: var(--color-primary);
406
+ font-size: 18px;
407
+ transition: var(--transition);
408
+ }
409
+
410
+ /* 参数提示 */
411
+ .param-hint {
412
+ background: var(--bg-hint);
413
+ padding: 12px;
414
+ border-radius: var(--radius-sm);
415
+ font-size: 13px;
416
+ color: var(--text-hint);
417
+ margin-top: 10px;
418
+ border-left: 4px solid var(--color-primary);
419
+ transition: var(--transition);
420
+ }
421
+
422
+ /* ========================================
423
+ 按钮
424
+ ======================================== */
425
+ .submit-btn {
426
+ width: 100%;
427
+ padding: 15px;
428
+ background: var(--color-primary);
429
+ color: white;
430
+ border: none;
431
+ border-radius: var(--radius-md);
432
+ font-size: 16px;
433
+ font-weight: 600;
434
+ cursor: pointer;
435
+ transition: var(--transition);
436
+ box-shadow: var(--shadow-sm);
437
+ }
438
+
439
+ .submit-btn:hover:not(:disabled) {
440
+ background: var(--color-primary-hover);
441
+ transform: translateY(-2px);
442
+ box-shadow: var(--shadow-hover);
443
+ }
444
+
445
+ .submit-btn:active:not(:disabled) {
446
+ transform: translateY(0);
447
+ }
448
+
449
+ .submit-btn:disabled {
450
+ opacity: 0.6;
451
+ cursor: not-allowed;
452
+ }
453
+
454
+ /* Minimal 主题按钮特殊样式 */
455
+ [data-theme="minimal"] .submit-btn {
456
+ background: var(--text-primary);
457
+ }
458
+
459
+ [data-theme="minimal"] .submit-btn:hover:not(:disabled) {
460
+ background: var(--color-primary);
461
+ }
462
+
463
+ /* ========================================
464
+ 底部链接
465
+ ======================================== */
466
+ .links {
467
+ margin-top: 25px;
468
+ text-align: center;
469
+ padding-top: 25px;
470
+ border-top: 1px solid var(--border-color);
471
+ }
472
+
473
+ .links a {
474
+ color: var(--color-primary);
475
+ text-decoration: none;
476
+ margin: 0 15px;
477
+ font-size: 14px;
478
+ font-weight: 500;
479
+ transition: var(--transition);
480
+ }
481
+
482
+ .links a:hover {
483
+ text-decoration: underline;
484
+ color: var(--color-primary-hover);
485
+ }
486
+
487
+ /* ========================================
488
+ 响应式设计
489
+ ======================================== */
490
+ @media (max-width: 768px) {
491
+ .container {
492
+ padding: 30px 20px;
493
+ }
494
+
495
+ .theme-switcher {
496
+ top: 10px;
497
+ right: 10px;
498
+ padding: 6px;
499
+ gap: 6px;
500
+ }
501
+
502
+ .theme-btn {
503
+ padding: 8px 10px;
504
+ }
505
+
506
+ .theme-icon {
507
+ font-size: 18px;
508
+ }
509
+
510
+ .theme-name {
511
+ font-size: 10px;
512
+ }
513
+
514
+ h1 {
515
+ font-size: 24px;
516
+ }
517
+
518
+ .radio-group {
519
+ flex-direction: column;
520
+ gap: 12px;
521
+ }
522
+ }
523
+
524
+ /* ========================================
525
+ 动画效果
526
+ ======================================== */
527
+ @keyframes fadeIn {
528
+ from {
529
+ opacity: 0;
530
+ transform: translateY(20px);
531
+ }
532
+ to {
533
+ opacity: 1;
534
+ transform: translateY(0);
535
+ }
536
+ }
537
+
538
+ .container {
539
+ animation: fadeIn 0.5s ease-out;
540
+ }
541
+
542
+ /* 主题切换过渡动画 */
543
+ body,
544
+ .container,
545
+ h1,
546
+ .subtitle,
547
+ label,
548
+ .file-label,
549
+ .radio-label,
550
+ .slider-value,
551
+ .param-hint,
552
+ select,
553
+ .submit-btn,
554
+ .links a {
555
+ transition: var(--transition);
556
+ }
static/js/app.js ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Magick 图像转换器 - 前端交互逻辑
3
+ * 功能:主题切换、文件上传、参数调整、表单提交
4
+ */
5
+
6
+ (function() {
7
+ 'use strict';
8
+
9
+ // ==========================================
10
+ // 1. 主题切换功能
11
+ // ==========================================
12
+ const THEME_KEY = 'magick-theme-preference';
13
+ const themeBtns = document.querySelectorAll('.theme-btn');
14
+ const body = document.body;
15
+
16
+ /**
17
+ * 应用主题到页面
18
+ * @param {string} theme - 主题名称 (apple|glass|minimal|tech)
19
+ */
20
+ function applyTheme(theme) {
21
+ body.setAttribute('data-theme', theme);
22
+
23
+ // 更新按钮激活状态
24
+ themeBtns.forEach(btn => {
25
+ if (btn.dataset.theme === theme) {
26
+ btn.classList.add('active');
27
+ } else {
28
+ btn.classList.remove('active');
29
+ }
30
+ });
31
+
32
+ // 保存到 localStorage
33
+ try {
34
+ localStorage.setItem(THEME_KEY, theme);
35
+ } catch (e) {
36
+ console.warn('无法保存主题偏好:', e);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * 加载用户保存的主题偏好
42
+ */
43
+ function loadThemePreference() {
44
+ try {
45
+ const savedTheme = localStorage.getItem(THEME_KEY);
46
+ if (savedTheme && ['apple', 'glass', 'minimal', 'tech'].includes(savedTheme)) {
47
+ return savedTheme;
48
+ }
49
+ } catch (e) {
50
+ console.warn('无法读取主题偏好:', e);
51
+ }
52
+ return 'apple'; // 默认主题
53
+ }
54
+
55
+ // 初始化主题
56
+ const initialTheme = loadThemePreference();
57
+ applyTheme(initialTheme);
58
+
59
+ // 绑定主题切换按钮事件
60
+ themeBtns.forEach(btn => {
61
+ btn.addEventListener('click', () => {
62
+ const theme = btn.dataset.theme;
63
+ applyTheme(theme);
64
+ });
65
+ });
66
+
67
+ // ==========================================
68
+ // 2. 文件上传交互
69
+ // ==========================================
70
+ const fileInput = document.getElementById('fileInput');
71
+ const selectedFile = document.getElementById('selectedFile');
72
+ const fileInputWrapper = document.querySelector('.file-input-wrapper');
73
+
74
+ /**
75
+ * 显示已选择的文件名
76
+ */
77
+ fileInput.addEventListener('change', function() {
78
+ if (this.files.length > 0) {
79
+ const file = this.files[0];
80
+ const fileName = file.name;
81
+ const fileSize = (file.size / (1024 * 1024)).toFixed(2); // MB
82
+ selectedFile.textContent = `✓ 已选择: ${fileName} (${fileSize} MB)`;
83
+ } else {
84
+ selectedFile.textContent = '';
85
+ }
86
+ });
87
+
88
+ /**
89
+ * 拖拽上传功能
90
+ */
91
+ fileInputWrapper.addEventListener('dragover', (e) => {
92
+ e.preventDefault();
93
+ fileInputWrapper.style.borderColor = 'var(--color-primary)';
94
+ fileInputWrapper.style.background = 'var(--bg-input-hover)';
95
+ });
96
+
97
+ fileInputWrapper.addEventListener('dragleave', (e) => {
98
+ e.preventDefault();
99
+ fileInputWrapper.style.borderColor = 'var(--border-color)';
100
+ fileInputWrapper.style.background = 'var(--bg-input)';
101
+ });
102
+
103
+ fileInputWrapper.addEventListener('drop', (e) => {
104
+ e.preventDefault();
105
+ fileInputWrapper.style.borderColor = 'var(--border-color)';
106
+ fileInputWrapper.style.background = 'var(--bg-input)';
107
+
108
+ const files = e.dataTransfer.files;
109
+ if (files.length > 0) {
110
+ fileInput.files = files;
111
+ // 触发 change 事件以显示文件名
112
+ const event = new Event('change');
113
+ fileInput.dispatchEvent(event);
114
+ }
115
+ });
116
+
117
+ // ==========================================
118
+ // 3. 质量参数滑块交互
119
+ // ==========================================
120
+ const slider = document.getElementById('setting');
121
+ const sliderValue = document.getElementById('settingValue');
122
+ const paramHint = document.getElementById('paramHint');
123
+ const modeRadios = document.querySelectorAll('input[name="mode"]');
124
+
125
+ /**
126
+ * 更新参数提示文本
127
+ */
128
+ function updateHint() {
129
+ const mode = document.querySelector('input[name="mode"]:checked').value;
130
+ const value = parseInt(slider.value);
131
+ sliderValue.textContent = value;
132
+
133
+ if (mode === 'lossy') {
134
+ // 有损模式:质量提示
135
+ let quality = '中等';
136
+ if (value >= 90) quality = '极高';
137
+ else if (value >= 80) quality = '高';
138
+ else if (value >= 60) quality = '中等';
139
+ else if (value >= 40) quality = '中低';
140
+ else quality = '低';
141
+
142
+ paramHint.textContent = `质量: ${value} - ${quality}质量 (0=最低质量,100=最高质量)`;
143
+ } else {
144
+ // 无损模式:压缩速度提示
145
+ let speed = '平衡';
146
+ if (value <= 20) speed = '最慢/最佳压缩';
147
+ else if (value <= 40) speed = '较慢/较好压缩';
148
+ else if (value <= 60) speed = '平衡';
149
+ else if (value <= 80) speed = '较快/较差压缩';
150
+ else speed = '最快/最差压缩';
151
+
152
+ paramHint.textContent = `压缩速度: ${value} - ${speed} (0=最慢/最佳,100=最快/最差)`;
153
+ }
154
+ }
155
+
156
+ // 滑块移动时更新
157
+ slider.addEventListener('input', updateHint);
158
+
159
+ /**
160
+ * 模式切换时自动调整质量值
161
+ */
162
+ modeRadios.forEach(radio => {
163
+ radio.addEventListener('change', function() {
164
+ const mode = document.querySelector('input[name="mode"]:checked').value;
165
+
166
+ if (mode === 'lossless') {
167
+ // 无损模式:默认最佳质量(0=最慢/最佳压缩)
168
+ slider.value = 0;
169
+ } else {
170
+ // 有损模式:默认中等质量(80=高质量)
171
+ slider.value = 80;
172
+ }
173
+
174
+ updateHint();
175
+ });
176
+ });
177
+
178
+ // 初始化提示
179
+ updateHint();
180
+
181
+ // ==========================================
182
+ // 4. 表单提交处理
183
+ // ==========================================
184
+ const form = document.getElementById('uploadForm');
185
+ const submitBtn = form.querySelector('.submit-btn');
186
+ const originalBtnText = submitBtn.textContent;
187
+
188
+ form.addEventListener('submit', function(e) {
189
+ // 显示加载状态
190
+ submitBtn.textContent = '⏳ 转换中...';
191
+ submitBtn.disabled = true;
192
+
193
+ // 如果表单提交失败,需要恢复按钮状态
194
+ // 这里使用 setTimeout 作为后备方案
195
+ setTimeout(() => {
196
+ if (submitBtn.disabled) {
197
+ submitBtn.textContent = originalBtnText;
198
+ submitBtn.disabled = false;
199
+ }
200
+ }, 60000); // 60秒超时恢复
201
+ });
202
+
203
+ // ==========================================
204
+ // 5. 键盘快捷键
205
+ // ==========================================
206
+ document.addEventListener('keydown', function(e) {
207
+ // Ctrl/Cmd + K: 聚焦文件输入
208
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
209
+ e.preventDefault();
210
+ fileInput.click();
211
+ }
212
+
213
+ // Ctrl/Cmd + 1-4: 切换主题
214
+ if ((e.ctrlKey || e.metaKey) && e.key >= '1' && e.key <= '4') {
215
+ e.preventDefault();
216
+ const themes = ['apple', 'glass', 'minimal', 'tech'];
217
+ const themeIndex = parseInt(e.key) - 1;
218
+ applyTheme(themes[themeIndex]);
219
+ }
220
+ });
221
+
222
+ // ==========================================
223
+ // 6. 工具函数 - 参数验证
224
+ // ==========================================
225
+
226
+ /**
227
+ * 验证文件大小(客户端预检查)
228
+ */
229
+ fileInput.addEventListener('change', function() {
230
+ if (this.files.length > 0) {
231
+ const file = this.files[0];
232
+ const maxSize = 200 * 1024 * 1024; // 200MB
233
+
234
+ if (file.size > maxSize) {
235
+ alert(`文件过大!最大支持 200MB,当前文件: ${(file.size / (1024 * 1024)).toFixed(2)} MB`);
236
+ this.value = ''; // 清空选择
237
+ selectedFile.textContent = '';
238
+ return;
239
+ }
240
+ }
241
+ });
242
+
243
+ // ==========================================
244
+ // 7. 初始化完成提示
245
+ // ==========================================
246
+ console.log('%c🧙‍♂️ Magick 图像转换器', 'font-size: 20px; font-weight: bold; color: #0071e3;');
247
+ console.log('%c✨ 前端已加载完成', 'color: #30d158;');
248
+ console.log('%c快捷键提示:', 'font-weight: bold;');
249
+ console.log(' Ctrl/Cmd + K: 打开文件选择');
250
+ console.log(' Ctrl/Cmd + 1-4: 切换主题 (1:Apple, 2:Glass, 3:Minimal, 4:Tech)');
251
+
252
+ })();
templates/index.html ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Magick 图像转换器</title>
7
+ <link rel="stylesheet" href="/static/css/themes.css">
8
+ </head>
9
+ <body>
10
+ <!-- 主题切换器 -->
11
+ <div class="theme-switcher">
12
+ <button class="theme-btn" data-theme="apple">
13
+ <span class="theme-icon">🍎</span>
14
+ <span class="theme-name">Apple</span>
15
+ </button>
16
+ <button class="theme-btn" data-theme="glass">
17
+ <span class="theme-icon">💎</span>
18
+ <span class="theme-name">Glass</span>
19
+ </button>
20
+ <button class="theme-btn" data-theme="minimal">
21
+ <span class="theme-icon">🖤</span>
22
+ <span class="theme-name">Minimal</span>
23
+ </button>
24
+ <button class="theme-btn" data-theme="tech">
25
+ <span class="theme-icon">🚀</span>
26
+ <span class="theme-name">Tech</span>
27
+ </button>
28
+ </div>
29
+
30
+ <div class="container">
31
+ <h1>🧙‍♂️ Magick 图像转换器</h1>
32
+ <p class="subtitle">支持多格式转换 | 有损/无损模式 | 支持动画图像</p>
33
+
34
+ <form id="uploadForm" action="/" method="POST" enctype="multipart/form-data">
35
+ <!-- 文件上传区域 -->
36
+ <div class="form-group">
37
+ <label>选择图像文件</label>
38
+ <div class="file-input-wrapper">
39
+ <input type="file" name="file" id="fileInput" accept="image/*" required>
40
+ <div class="file-label">
41
+ 📁 点击选择或拖拽文件到此处
42
+ <div class="file-hint">
43
+ 支持 JPG, PNG, GIF, WebP, AVIF, HEIF 等格式
44
+ </div>
45
+ </div>
46
+ </div>
47
+ <div id="selectedFile" class="selected-file"></div>
48
+ </div>
49
+
50
+ <!-- 目标格式选择 -->
51
+ <div class="form-group">
52
+ <label for="target_format">目标格式</label>
53
+ <select name="target_format" id="target_format" required>
54
+ <option value="webp">WebP - 现代高效格式</option>
55
+ <option value="avif">AVIF - 最新一代格式</option>
56
+ <option value="jpeg">JPEG - 经典有损格式</option>
57
+ <option value="png">PNG - 无损格式</option>
58
+ <option value="gif">GIF - 动画格式</option>
59
+ <option value="heif" selected>HEIF - 高效图像格式</option>
60
+ </select>
61
+ </div>
62
+
63
+ <!-- 转换模式选择 -->
64
+ <div class="form-group">
65
+ <label>转换模式</label>
66
+ <div class="radio-group">
67
+ <label class="radio-label">
68
+ <input type="radio" name="mode" value="lossy">
69
+ 有损压缩 (更小体积)
70
+ </label>
71
+ <label class="radio-label">
72
+ <input type="radio" name="mode" value="lossless" checked>
73
+ 无损压缩 (保持质量)
74
+ </label>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- 质量参数滑块 -->
79
+ <div class="form-group">
80
+ <label for="setting">质量参数</label>
81
+ <div class="slider-container">
82
+ <input type="range" name="setting" id="setting" min="0" max="100" value="0">
83
+ <span class="slider-value" id="settingValue">0</span>
84
+ </div>
85
+ <div class="param-hint" id="paramHint">
86
+ 压缩速度: 0 - 最慢/最佳压缩 (0=最慢/最佳,100=最快/最差)
87
+ </div>
88
+ </div>
89
+
90
+ <!-- 提交按钮 -->
91
+ <button type="submit" class="submit-btn">🚀 开始转换</button>
92
+ </form>
93
+
94
+ <!-- 底部链接 -->
95
+ <div class="links">
96
+ <a href="/docs" target="_blank">📖 API 文档</a>
97
+ <a href="/health" target="_blank">🏥 健康检查</a>
98
+ </div>
99
+ </div>
100
+
101
+ <script src="/static/js/app.js"></script>
102
+ </body>
103
+ </html>