Norcoo commited on
Commit
637fff5
·
verified ·
1 Parent(s): e4e7802

Upload 2 files

Browse files
Files changed (1) hide show
  1. app.py +326 -142
app.py CHANGED
@@ -2,7 +2,6 @@ import gradio as gr
2
  import os
3
  import sqlite3
4
  import hashlib
5
- import shutil
6
  from datetime import datetime
7
  from pathlib import Path
8
  from PIL import Image
@@ -10,6 +9,8 @@ import json
10
 
11
  # 环境变量
12
  ACCESS_PASSWORD = os.environ.get("ACCESS_PASSWORD", "changeme")
 
 
13
 
14
  # 文件存储路径
15
  IMAGE_DIR = Path("uploaded_images")
@@ -58,90 +59,125 @@ def check_password(password):
58
 
59
  def generate_filename(original_filename):
60
  """生成唯一的文件名"""
61
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
62
  hash_suffix = hashlib.md5(str(datetime.now().timestamp()).encode()).hexdigest()[:8]
63
- ext = Path(original_filename).suffix
 
 
64
  return f"{timestamp}_{hash_suffix}{ext}"
65
 
66
- def upload_image(image, description, password):
67
- """上传图片"""
 
 
 
 
 
 
 
 
68
  if not check_password(password):
69
- return "❌ 密码错误!", None, None
 
 
 
70
 
71
- if image is None:
72
- return "❌ 请选择要上传的图片!", None, None
 
73
 
74
  try:
75
- # 获取原始文件名
76
- if isinstance(image, str):
77
- # 如果是文件路径
78
- original_filename = Path(image).name
79
- img = Image.open(image)
80
- else:
81
- # 如果是 PIL Image
82
- original_filename = "uploaded_image.png"
83
- img = image
84
-
85
- # 生成新文件名
86
- new_filename = generate_filename(original_filename)
87
- file_path = IMAGE_DIR / new_filename
88
-
89
- # 保存图片
90
- img.save(file_path, quality=95, optimize=True)
91
-
92
- # 获取文件信息
93
- file_size = file_path.stat().st_size
94
- mime_type = f"image/{file_path.suffix[1:]}"
95
- upload_time = datetime.now().isoformat()
96
-
97
- # 插入数据库
98
- conn = get_db_connection()
99
- cursor = conn.cursor()
100
- cursor.execute(
101
- """
102
- INSERT INTO images (filename, original_filename, file_path, file_size, mime_type, upload_time, description)
103
- VALUES (?, ?, ?, ?, ?, ?, ?)
104
- """,
105
- (new_filename, original_filename, str(file_path), file_size, mime_type, upload_time, description or "")
106
- )
107
- image_id = cursor.lastrowid
108
- conn.commit()
109
- conn.close()
110
-
111
- # 生成访问 URL
112
- url = f"/file={file_path}"
113
-
114
- result_text = f"""✅ 上传成功!
115
-
116
- **图片ID**: {image_id}
117
- **文件名**: {original_filename}
118
- **大小**: {file_size / 1024:.2f} KB
119
- **上传时间**: {upload_time}
120
-
121
- **访问URL**:
122
- ```
123
- {url}
124
- ```
125
-
126
- 💡 提示:可以在"查看图片"标签页中输入 ID: {image_id} 来查看图片
127
- """
128
-
129
- return result_text, get_image_list(password), url
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  except Exception as e:
132
- return f"❌ 上传失败: {str(e)}", None, None
133
 
134
- def get_image_list(password):
135
- """获取图片列表"""
136
  if not check_password(password):
137
- return " 密码错误!"
138
 
139
  try:
140
  conn = get_db_connection()
141
  cursor = conn.cursor()
142
  cursor.execute(
143
  """
144
- SELECT id, filename, original_filename, file_size, upload_time, description
145
  FROM images
146
  ORDER BY upload_time DESC
147
  """
@@ -150,31 +186,69 @@ def get_image_list(password):
150
  conn.close()
151
 
152
  if not rows:
153
- return "暂无图片"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- # 格式化输出
156
- output = "## 📸 图片列表\n\n"
157
  for row in rows:
158
- output += f"**ID: {row['id']}** | {row['original_filename']}\n"
159
- output += f"- 存储文件名: {row['filename']}\n"
160
- output += f"- 大小: {row['file_size'] / 1024:.2f} KB\n"
161
- output += f"- 上传时间: {row['upload_time']}\n"
162
- if row['description']:
163
- output += f"- 描述: {row['description']}\n"
164
- output += "\n---\n\n"
165
-
166
- return output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
  except Exception as e:
169
- return f" 获取列表失败: {str(e)}"
170
 
171
- def view_image(image_id, password):
172
- """查看图片"""
173
  if not check_password(password):
174
- return "❌ 密码错误!", None, None
175
 
176
  if not image_id:
177
- return "❌ 请输入图片ID!", None, None
178
 
179
  try:
180
  conn = get_db_connection()
@@ -187,17 +261,17 @@ def view_image(image_id, password):
187
  conn.close()
188
 
189
  if not row:
190
- return "❌ 图片不存在!", None, None
191
 
192
  file_path = Path(row['file_path'])
193
 
194
  if not file_path.exists():
195
- return "❌ 图片文件已损坏或丢失!", None, None
196
 
197
  # 生成访问 URL
198
- url = f"/file={file_path}"
199
 
200
- info_text = f""" 图片信息
201
 
202
  **ID**: {row['id']}
203
  **原始文件名**: {row['original_filename']}
@@ -208,14 +282,14 @@ def view_image(image_id, password):
208
 
209
  **访问URL**:
210
  ```
211
- {url}
212
  ```
213
  """
214
 
215
- return info_text, str(file_path), url
216
 
217
  except Exception as e:
218
- return f"❌ 查看失败: {str(e)}", None, None
219
 
220
  def delete_image(image_id, password):
221
  """删除图片"""
@@ -249,7 +323,7 @@ def delete_image(image_id, password):
249
  if file_path.exists():
250
  file_path.unlink()
251
 
252
- return f"✅ 已删除图片: {original_filename} (ID: {image_id})", get_image_list(password)
253
 
254
  except Exception as e:
255
  return f"❌ 删除失败: {str(e)}", None
@@ -277,12 +351,22 @@ def get_stats(password):
277
 
278
  conn.close()
279
 
 
 
 
280
  stats = f"""## 📊 统计信息
281
 
282
  **总图片数**: {total_count} 张
283
  **总存储大小**: {total_size / 1024 / 1024:.2f} MB
284
  **最近上传**: {last_upload['upload_time'] if last_upload else '暂无'}
285
- **存储路径**: {IMAGE_DIR.absolute()}
 
 
 
 
 
 
 
286
  **数据库**: {Path(DB_PATH).absolute()}
287
  """
288
 
@@ -306,13 +390,15 @@ def export_data(password):
306
  # 转换为 JSON
307
  data = []
308
  for row in rows:
 
309
  data.append({
310
  'id': row['id'],
311
  'filename': row['filename'],
312
  'original_filename': row['original_filename'],
313
  'file_size': row['file_size'],
314
  'upload_time': row['upload_time'],
315
- 'description': row['description']
 
316
  })
317
 
318
  # 保存为文件
@@ -328,10 +414,21 @@ def export_data(password):
328
  # 初始化数据库
329
  init_success = init_db()
330
 
 
 
 
 
 
 
 
 
 
 
 
331
  # 创建 Gradio 界面
332
- with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft()) as app:
333
  gr.Markdown("# 🖼️ 私人图床服务")
334
- gr.Markdown("⚠️ 请输入密码���使用此服务 | 图片存储在服务器本地文件系统")
335
 
336
  if not init_success:
337
  gr.Markdown("## ❌ 数据库初始化失败!")
@@ -339,66 +436,137 @@ with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft()) as app:
339
  with gr.Tabs():
340
  # 上传图片标签页
341
  with gr.Tab("📤 上传图片"):
 
 
342
  with gr.Row():
343
- with gr.Column():
344
- upload_image_input = gr.Image(label="选择图片", type="pil")
345
- upload_desc_input = gr.Textbox(label="描述(可选)", placeholder="给图片添加描述...")
346
- upload_password_input = gr.Textbox(label="密码", type="password", placeholder="输入访问密码")
347
- upload_btn = gr.Button("上传", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
 
349
- with gr.Column():
350
- upload_output = gr.Textbox(label="上传结果", lines=10)
351
- upload_url_output = gr.Textbox(label="图片URL", lines=2)
352
- upload_list_output = gr.Markdown(label="图片列表")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
  upload_btn.click(
355
- upload_image,
356
- inputs=[upload_image_input, upload_desc_input, upload_password_input],
357
- outputs=[upload_output, upload_list_output, upload_url_output]
358
  )
359
 
360
- # 查看图片标签页
361
- with gr.Tab("👀 查看图片"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  with gr.Row():
363
  with gr.Column():
364
- view_id_input = gr.Textbox(label="图片ID", placeholder="输入图片ID")
365
- view_password_input = gr.Textbox(label="密码", type="password", placeholder="输入访问密码")
366
- view_btn = gr.Button("查看", variant="primary")
367
- view_output = gr.Textbox(label="图片信息", lines=10)
368
- view_url_output = gr.Textbox(label="图片URL", lines=2)
 
 
 
 
 
 
369
 
370
  with gr.Column():
371
- view_image_output = gr.Image(label="图片预览")
372
 
373
- view_btn.click(
374
- view_image,
375
- inputs=[view_id_input, view_password_input],
376
- outputs=[view_output, view_image_output, view_url_output]
377
- )
378
-
379
- # 图片列表标签页
380
- with gr.Tab("📋 图片列表"):
381
- list_password_input = gr.Textbox(label="密码", type="password", placeholder="输入访问密码")
382
- list_btn = gr.Button("刷新列表", variant="primary")
383
- list_output = gr.Markdown(label="图片列表")
384
-
385
- list_btn.click(
386
- get_image_list,
387
  inputs=[list_password_input],
388
  outputs=[list_output]
389
  )
 
 
 
 
 
 
390
 
391
  # 删除图片标签页
392
  with gr.Tab("🗑️ 删除图片"):
 
 
393
  with gr.Row():
394
  with gr.Column():
395
- delete_id_input = gr.Textbox(label="图片ID", placeholder="输入要删除的图片ID")
396
- delete_password_input = gr.Textbox(label="密码", type="password", placeholder="输入访问密码")
397
- delete_btn = gr.Button("删除", variant="stop")
 
 
 
 
 
 
 
 
398
 
399
  with gr.Column():
400
- delete_output = gr.Textbox(label="删除结果", lines=3)
401
- delete_list_output = gr.Markdown(label="剩余图片列表")
 
 
 
 
 
 
402
 
403
  delete_btn.click(
404
  delete_image,
@@ -408,13 +576,19 @@ with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft()) as app:
408
 
409
  # 统计信息标签页
410
  with gr.Tab("📊 统计信息"):
411
- stats_password_input = gr.Textbox(label="密码", type="password", placeholder="输入访问密码")
412
- stats_btn = gr.Button("查看统计", variant="primary")
 
 
 
 
413
  stats_output = gr.Markdown(label="统计信息")
414
 
415
  gr.Markdown("---")
416
- gr.Markdown("### 导出数据")
417
- export_btn = gr.Button("导出元数据(JSON", variant="secondary")
 
 
418
  export_output = gr.Textbox(label="导出结果", lines=2)
419
  export_file_output = gr.File(label="下载文件")
420
 
@@ -431,8 +605,18 @@ with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft()) as app:
431
  )
432
 
433
  gr.Markdown("---")
434
- gr.Markdown("💡 提示:图片存储在服务器本地,URL可以直接在外部访问(需要 Space 为 Public)")
435
- gr.Markdown("🔒 安全提示:建议将 Space 设为 Private 以保护隐私")
 
 
 
 
 
 
 
 
 
 
436
 
437
  if __name__ == "__main__":
438
  app.launch()
 
2
  import os
3
  import sqlite3
4
  import hashlib
 
5
  from datetime import datetime
6
  from pathlib import Path
7
  from PIL import Image
 
9
 
10
  # 环境变量
11
  ACCESS_PASSWORD = os.environ.get("ACCESS_PASSWORD", "changeme")
12
+ HF_USERNAME = os.environ.get("HF_USERNAME", "")
13
+ HF_SPACE_NAME = os.environ.get("HF_SPACE_NAME", "")
14
 
15
  # 文件存储路径
16
  IMAGE_DIR = Path("uploaded_images")
 
59
 
60
  def generate_filename(original_filename):
61
  """生成唯一的文件名"""
62
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
63
  hash_suffix = hashlib.md5(str(datetime.now().timestamp()).encode()).hexdigest()[:8]
64
+ ext = Path(original_filename).suffix.lower()
65
+ if not ext:
66
+ ext = ".png"
67
  return f"{timestamp}_{hash_suffix}{ext}"
68
 
69
+ def generate_full_url(file_path):
70
+ """生成完整的图片URL"""
71
+ relative_path = f"/file={file_path}"
72
+ if HF_USERNAME and HF_SPACE_NAME:
73
+ return f"https://{HF_USERNAME}-{HF_SPACE_NAME}.hf.space{relative_path}"
74
+ else:
75
+ return relative_path
76
+
77
+ def upload_images(images, description, password):
78
+ """批量上传图片"""
79
  if not check_password(password):
80
+ return "❌ 密码错误!", None, ""
81
+
82
+ if not images or len(images) == 0:
83
+ return "❌ 请选择要上传的图片!", None, ""
84
 
85
+ results = []
86
+ success_count = 0
87
+ fail_count = 0
88
 
89
  try:
90
+ for idx, image in enumerate(images):
91
+ try:
92
+ # 获取原始文件名
93
+ if isinstance(image, str):
94
+ # 如果是文件路径
95
+ original_filename = Path(image).name
96
+ img = Image.open(image)
97
+ else:
98
+ # 如果是 PIL Image
99
+ original_filename = f"uploaded_image_{idx+1}.png"
100
+ img = image
101
+
102
+ # 生成新文件名
103
+ new_filename = generate_filename(original_filename)
104
+ file_path = IMAGE_DIR / new_filename
105
+
106
+ # 保存图片
107
+ img.save(file_path, quality=95, optimize=True)
108
+
109
+ # 获取文件信息
110
+ file_size = file_path.stat().st_size
111
+ mime_type = f"image/{file_path.suffix[1:]}"
112
+ upload_time = datetime.now().isoformat()
113
+
114
+ # 插入数据库
115
+ conn = get_db_connection()
116
+ cursor = conn.cursor()
117
+ cursor.execute(
118
+ """
119
+ INSERT INTO images (filename, original_filename, file_path, file_size, mime_type, upload_time, description)
120
+ VALUES (?, ?, ?, ?, ?, ?, ?)
121
+ """,
122
+ (new_filename, original_filename, str(file_path), file_size, mime_type, upload_time, description or "")
123
+ )
124
+ image_id = cursor.lastrowid
125
+ conn.commit()
126
+ conn.close()
127
+
128
+ # 生成访问 URL
129
+ full_url = generate_full_url(file_path)
130
+
131
+ results.append({
132
+ 'id': image_id,
133
+ 'filename': original_filename,
134
+ 'size': file_size,
135
+ 'url': full_url,
136
+ 'status': 'success'
137
+ })
138
+ success_count += 1
139
+
140
+ except Exception as e:
141
+ results.append({
142
+ 'filename': f"图片 {idx+1}",
143
+ 'error': str(e),
144
+ 'status': 'failed'
145
+ })
146
+ fail_count += 1
147
+
148
+ # 生成结果文本
149
+ result_text = f"## 📤 上传结果\n\n"
150
+ result_text += f"✅ 成功: {success_count} 张 | ❌ 失败: {fail_count} 张\n\n"
151
+
152
+ for r in results:
153
+ if r['status'] == 'success':
154
+ result_text += f"### ✅ {r['filename']}\n"
155
+ result_text += f"- **ID**: {r['id']}\n"
156
+ result_text += f"- **大小**: {r['size'] / 1024:.2f} KB\n"
157
+ result_text += f"- **URL**: `{r['url']}`\n\n"
158
+ else:
159
+ result_text += f"### ❌ {r['filename']}\n"
160
+ result_text += f"- **错误**: {r['error']}\n\n"
161
+
162
+ # 生成URL列表(用于复制)
163
+ url_list = "\n".join([r['url'] for r in results if r['status'] == 'success'])
164
+
165
+ return result_text, get_image_list_html(password), url_list
166
 
167
  except Exception as e:
168
+ return f"❌ 上传失败: {str(e)}", None, ""
169
 
170
+ def get_image_list_html(password):
171
+ """获取图片列表(HTML格式)"""
172
  if not check_password(password):
173
+ return "<p style='color: red;'>❌ 密码错误!</p>"
174
 
175
  try:
176
  conn = get_db_connection()
177
  cursor = conn.cursor()
178
  cursor.execute(
179
  """
180
+ SELECT id, filename, original_filename, file_path, file_size, upload_time, description
181
  FROM images
182
  ORDER BY upload_time DESC
183
  """
 
186
  conn.close()
187
 
188
  if not rows:
189
+ return "<p>暂无图片</p>"
190
+
191
+ # 生成HTML表格
192
+ html = """
193
+ <style>
194
+ .image-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
195
+ .image-table th, .image-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
196
+ .image-table th { background-color: #f0f0f0; font-weight: bold; }
197
+ .image-table tr:hover { background-color: #f5f5f5; }
198
+ .btn-view { background: #4CAF50; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; }
199
+ .btn-copy { background: #2196F3; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; }
200
+ .btn-delete { background: #f44336; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; }
201
+ .image-id { font-weight: bold; color: #2196F3; }
202
+ </style>
203
+ <table class="image-table">
204
+ <thead>
205
+ <tr>
206
+ <th>ID</th>
207
+ <th>文件名</th>
208
+ <th>大小</th>
209
+ <th>上传时间</th>
210
+ <th>描述</th>
211
+ <th>操作</th>
212
+ </tr>
213
+ </thead>
214
+ <tbody>
215
+ """
216
 
 
 
217
  for row in rows:
218
+ upload_time_str = datetime.fromisoformat(row['upload_time']).strftime('%Y-%m-%d %H:%M:%S')
219
+ full_url = generate_full_url(row['file_path'])
220
+
221
+ html += f"""
222
+ <tr>
223
+ <td class="image-id">{row['id']}</td>
224
+ <td>{row['original_filename']}</td>
225
+ <td>{row['file_size'] / 1024:.2f} KB</td>
226
+ <td>{upload_time_str}</td>
227
+ <td>{row['description'] or '-'}</td>
228
+ <td>
229
+ <button class="btn-view" onclick="window.viewImage_{row['id']}()">查看</button>
230
+ <button class="btn-copy" onclick="navigator.clipboard.writeText('{full_url}').then(() => alert('URL已复制到剪贴板!'))">复制URL</button>
231
+ </td>
232
+ </tr>
233
+ """
234
+
235
+ html += """
236
+ </tbody>
237
+ </table>
238
+ """
239
+
240
+ return html
241
 
242
  except Exception as e:
243
+ return f"<p style='color: red;'>❌ 获取列表失败: {str(e)}</p>"
244
 
245
+ def view_image_by_id(image_id, password):
246
+ """通过ID查看图片"""
247
  if not check_password(password):
248
+ return "❌ 密码错误!", None, ""
249
 
250
  if not image_id:
251
+ return "❌ 请输入图片ID!", None, ""
252
 
253
  try:
254
  conn = get_db_connection()
 
261
  conn.close()
262
 
263
  if not row:
264
+ return "❌ 图片不存在!", None, ""
265
 
266
  file_path = Path(row['file_path'])
267
 
268
  if not file_path.exists():
269
+ return "❌ 图片文件已损坏或丢失!", None, ""
270
 
271
  # 生成访问 URL
272
+ full_url = generate_full_url(file_path)
273
 
274
+ info_text = f"""## 📷 图片信息
275
 
276
  **ID**: {row['id']}
277
  **原始文件名**: {row['original_filename']}
 
282
 
283
  **访问URL**:
284
  ```
285
+ {full_url}
286
  ```
287
  """
288
 
289
+ return info_text, str(file_path), full_url
290
 
291
  except Exception as e:
292
+ return f"❌ 查看失败: {str(e)}", None, ""
293
 
294
  def delete_image(image_id, password):
295
  """删除图片"""
 
323
  if file_path.exists():
324
  file_path.unlink()
325
 
326
+ return f"✅ 已删除图片: {original_filename} (ID: {image_id})", get_image_list_html(password)
327
 
328
  except Exception as e:
329
  return f"❌ 删除失败: {str(e)}", None
 
351
 
352
  conn.close()
353
 
354
+ # 检查配置状态
355
+ config_status = "✅ 已配置" if (HF_USERNAME and HF_SPACE_NAME) else "⚠️ 未配置(仅显示相对路径)"
356
+
357
  stats = f"""## 📊 统计信息
358
 
359
  **总图片数**: {total_count} 张
360
  **总存储大小**: {total_size / 1024 / 1024:.2f} MB
361
  **最近上传**: {last_upload['upload_time'] if last_upload else '暂无'}
362
+
363
+ ### 🔧 配置信息
364
+ **HF用户名**: {HF_USERNAME or '未设置'}
365
+ **Space名称**: {HF_SPACE_NAME or '未设置'}
366
+ **URL生成**: {config_status}
367
+
368
+ ### 📁 存储信息
369
+ **图片目录**: {IMAGE_DIR.absolute()}
370
  **数据库**: {Path(DB_PATH).absolute()}
371
  """
372
 
 
390
  # 转换为 JSON
391
  data = []
392
  for row in rows:
393
+ full_url = generate_full_url(row['file_path'])
394
  data.append({
395
  'id': row['id'],
396
  'filename': row['filename'],
397
  'original_filename': row['original_filename'],
398
  'file_size': row['file_size'],
399
  'upload_time': row['upload_time'],
400
+ 'description': row['description'],
401
+ 'url': full_url
402
  })
403
 
404
  # 保存为文件
 
414
  # 初始化数据库
415
  init_success = init_db()
416
 
417
+ # 自定义CSS
418
+ custom_css = """
419
+ .compact-gallery .gallery {
420
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)) !important;
421
+ }
422
+ .url-box textarea {
423
+ font-family: monospace;
424
+ font-size: 12px;
425
+ }
426
+ """
427
+
428
  # 创建 Gradio 界面
429
+ with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft(), css=custom_css) as app:
430
  gr.Markdown("# 🖼️ 私人图床服务")
431
+ gr.Markdown("⚠️ 请输入密码以使用此服务 | 支持批量上传 | 图片存储在服务器本地")
432
 
433
  if not init_success:
434
  gr.Markdown("## ❌ 数据库初始化失败!")
 
436
  with gr.Tabs():
437
  # 上传图片标签页
438
  with gr.Tab("📤 上传图片"):
439
+ gr.Markdown("### 批量上传图片(支持多张)")
440
+
441
  with gr.Row():
442
+ with gr.Column(scale=1):
443
+ upload_password_input = gr.Textbox(
444
+ label="🔐 密码",
445
+ type="password",
446
+ placeholder="输入访问密码"
447
+ )
448
+ upload_images_input = gr.File(
449
+ label="选择图片(支持多选)",
450
+ file_count="multiple",
451
+ file_types=["image"]
452
+ )
453
+ upload_desc_input = gr.Textbox(
454
+ label="描述(可选)",
455
+ placeholder="为这批图片添加描述...",
456
+ lines=2
457
+ )
458
+
459
+ with gr.Row():
460
+ upload_btn = gr.Button("📤 上传", variant="primary", scale=2)
461
+ toggle_preview_btn = gr.Button("👁️ 显示/隐藏预览", scale=1)
462
 
463
+ with gr.Column(scale=1):
464
+ upload_output = gr.Markdown(label="上传结果")
465
+ upload_url_output = gr.Textbox(
466
+ label="📋 图片URL列表(可复制)",
467
+ lines=5,
468
+ elem_classes=["url-box"]
469
+ )
470
+
471
+ # 可折叠的预览区域
472
+ with gr.Accordion("🖼️ 图片预览", open=False) as preview_accordion:
473
+ upload_preview = gr.Gallery(
474
+ label="上传预览",
475
+ show_label=False,
476
+ columns=4,
477
+ rows=2,
478
+ height="auto",
479
+ elem_classes=["compact-gallery"]
480
+ )
481
+
482
+ # 绑定事件
483
+ upload_images_input.change(
484
+ lambda files: [f.name for f in files] if files else [],
485
+ inputs=[upload_images_input],
486
+ outputs=[upload_preview]
487
+ )
488
 
489
  upload_btn.click(
490
+ lambda files, desc, pwd: upload_images([f.name for f in files] if files else [], desc, pwd),
491
+ inputs=[upload_images_input, upload_desc_input, upload_password_input],
492
+ outputs=[upload_output, gr.HTML(visible=False), upload_url_output]
493
  )
494
 
495
+ # 图片列表标签页
496
+ with gr.Tab("📋 图片列表"):
497
+ gr.Markdown("### 查看和管理所有图片")
498
+
499
+ with gr.Row():
500
+ list_password_input = gr.Textbox(
501
+ label="🔐 密码",
502
+ type="password",
503
+ placeholder="输入访问密码",
504
+ scale=3
505
+ )
506
+ list_refresh_btn = gr.Button("🔄 刷新列表", variant="primary", scale=1)
507
+
508
+ list_output = gr.HTML(label="图片列表")
509
+
510
+ gr.Markdown("---")
511
+ gr.Markdown("### 📷 查看图片详情")
512
+
513
  with gr.Row():
514
  with gr.Column():
515
+ view_id_input = gr.Textbox(
516
+ label="图片ID",
517
+ placeholder="输入图片ID查看详情"
518
+ )
519
+ view_btn = gr.Button("👁️ 查看详情", variant="secondary")
520
+ view_info_output = gr.Markdown(label="图片信息")
521
+ view_url_output = gr.Textbox(
522
+ label="图片URL(点击复制)",
523
+ lines=2,
524
+ elem_classes=["url-box"]
525
+ )
526
 
527
  with gr.Column():
528
+ view_image_output = gr.Image(label="图片预览", height=400)
529
 
530
+ # 绑定事件
531
+ list_refresh_btn.click(
532
+ get_image_list_html,
 
 
 
 
 
 
 
 
 
 
 
533
  inputs=[list_password_input],
534
  outputs=[list_output]
535
  )
536
+
537
+ view_btn.click(
538
+ view_image_by_id,
539
+ inputs=[view_id_input, list_password_input],
540
+ outputs=[view_info_output, view_image_output, view_url_output]
541
+ )
542
 
543
  # 删除图片标签页
544
  with gr.Tab("🗑️ 删除图片"):
545
+ gr.Markdown("### ⚠️ 删除操作不可恢复,请谨慎操作")
546
+
547
  with gr.Row():
548
  with gr.Column():
549
+ delete_id_input = gr.Textbox(
550
+ label="图片ID",
551
+ placeholder="输入要删除的图片ID"
552
+ )
553
+ delete_password_input = gr.Textbox(
554
+ label="🔐 密码",
555
+ type="password",
556
+ placeholder="输入访问密码"
557
+ )
558
+ delete_btn = gr.Button("🗑️ 确认删除", variant="stop")
559
+ delete_output = gr.Textbox(label="删除结果", lines=3)
560
 
561
  with gr.Column():
562
+ gr.Markdown("### 💡 提示")
563
+ gr.Markdown("""
564
+ - 删除操作会同时删除数据库记录和文件
565
+ - 删除后无法恢复
566
+ - 可以在图片列表中查看要删除的图片ID
567
+ """)
568
+
569
+ delete_list_output = gr.HTML(label="剩余图片列表")
570
 
571
  delete_btn.click(
572
  delete_image,
 
576
 
577
  # 统计信息标签页
578
  with gr.Tab("📊 统计信息"):
579
+ stats_password_input = gr.Textbox(
580
+ label="🔐 密码",
581
+ type="password",
582
+ placeholder="输入访问密码"
583
+ )
584
+ stats_btn = gr.Button("📊 查看统计", variant="primary")
585
  stats_output = gr.Markdown(label="统计信息")
586
 
587
  gr.Markdown("---")
588
+ gr.Markdown("### 📦 导出数据")
589
+ gr.Markdown("导出所有图片的元数据(包含完整URL)为JSON格式")
590
+
591
+ export_btn = gr.Button("📦 导出元数据(JSON)", variant="secondary")
592
  export_output = gr.Textbox(label="导出结果", lines=2)
593
  export_file_output = gr.File(label="下载文件")
594
 
 
605
  )
606
 
607
  gr.Markdown("---")
608
+ gr.Markdown("""
609
+ ### 💡 使用提示
610
+ - **完整URL**: 在环境变量中配置 `HF_USERNAME` 和 `HF_SPACE_NAME` 即可生成完整的外部访问URL
611
+ - **批量上传**: 支持一次选择多张图片上传
612
+ - **快速复制**: 在图片列表中点击"复制URL"按钮即可复制到剪贴板
613
+ - **数据安全**: 定期导出元数据进行备份
614
+
615
+ ### 🔒 安全提示
616
+ - 建议将 Space 设为 Private 以保护隐私
617
+ - 定期更换访问密码
618
+ - 不要分享完整URL给不信任的人
619
+ """)
620
 
621
  if __name__ == "__main__":
622
  app.launch()