ziconghe commited on
Commit
de1b4c8
·
verified ·
1 Parent(s): c9ef830

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +91 -200
  2. requirements.txt +2 -2
app.py CHANGED
@@ -1,9 +1,4 @@
1
  # -*- coding: utf-8 -*-
2
- """
3
- 款式选择器 - Hugging Face Spaces 版本
4
- 支持多Sheet + 嵌入图片提取
5
- """
6
-
7
  import gradio as gr
8
  import pandas as pd
9
  import openpyxl
@@ -16,7 +11,6 @@ import tempfile
16
  from datetime import datetime
17
 
18
  def extract_images_from_excel(file_path):
19
- """提取Excel中的嵌入图片"""
20
  images = {}
21
  try:
22
  wb = openpyxl.load_workbook(file_path)
@@ -26,55 +20,46 @@ def extract_images_from_excel(file_path):
26
  anchor = img.anchor
27
  if hasattr(anchor, '_from'):
28
  row = anchor._from.row + 1
29
- col = anchor._from.col + 1
30
- else:
31
- continue
32
- try:
33
- img_data = img._data()
34
- if img_data:
35
- images[(sheet_name, row)] = img_data
36
- except:
37
- pass
38
  wb.close()
39
  except Exception as e:
40
- print(f"提取图片出错: {e}")
41
  return images
42
 
43
  def find_header_row(df):
44
- """查找表头行"""
45
  keywords = ['款号', '货号', 'SKU', '序号', '编号']
46
  for idx, row in df.iterrows():
47
  for val in row.values:
48
  if pd.notna(val):
49
- val_str = str(val).lower()
50
  for kw in keywords:
51
- if kw.lower() in val_str:
52
  return idx
53
  return 0
54
 
55
  def find_column(headers, possible_names):
56
- """查找列索引"""
57
  for i, h in enumerate(headers):
58
  if pd.notna(h):
59
- h_str = str(h).lower()
60
  for name in possible_names:
61
- if name.lower() in h_str:
62
  return i
63
  return -1
64
 
65
  def load_excel(file):
66
- """加载Excel文件"""
67
  if file is None:
68
- return "请先上传Excel文件", [], {}
69
 
70
  all_data = []
71
  all_images = {}
72
 
73
  try:
74
- all_images = extract_images_from_excel(file.name)
75
- print(f"提取到 {len(all_images)} 张图片")
76
-
77
- xl = pd.ExcelFile(file.name)
78
 
79
  for sheet_name in xl.sheet_names:
80
  df = pd.read_excel(xl, sheet_name=sheet_name, header=None)
@@ -96,7 +81,7 @@ def load_excel(file):
96
 
97
  for idx in range(header_row + 1, len(df)):
98
  row = df.iloc[idx]
99
- sku = row.iloc[sku_col] if sku_col >= 0 and sku_col < len(row) else None
100
 
101
  if pd.isna(sku) or str(sku).strip() == '':
102
  continue
@@ -104,13 +89,18 @@ def load_excel(file):
104
  excel_row = idx + 1
105
  has_image = (sheet_name, excel_row) in all_images
106
 
 
 
 
 
 
107
  item = {
108
  'sku': str(sku).strip(),
109
- 'name': str(row.iloc[name_col]).strip() if name_col >= 0 and name_col < len(row) and pd.notna(row.iloc[name_col]) else '',
110
- 'material': str(row.iloc[material_col]).strip() if material_col >= 0 and material_col < len(row) and pd.notna(row.iloc[material_col]) else '',
111
- 'color': str(row.iloc[color_col]).strip() if color_col >= 0 and color_col < len(row) and pd.notna(row.iloc[color_col]) else '',
112
- 'size': str(row.iloc[size_col]).strip() if size_col >= 0 and size_col < len(row) and pd.notna(row.iloc[size_col]) else '',
113
- 'price': str(row.iloc[price_col]).strip() if price_col >= 0 and price_col < len(row) and pd.notna(row.iloc[price_col]) else '',
114
  'sheet': sheet_name,
115
  'excel_row': excel_row,
116
  'has_image': has_image,
@@ -119,43 +109,35 @@ def load_excel(file):
119
  all_data.append(item)
120
 
121
  xl.close()
122
-
123
- sheets_with_data = len(set(item['sheet'] for item in all_data))
124
  images_count = sum(1 for item in all_data if item['has_image'])
125
-
126
- status = f"✅ 加载成功!共 {len(all_data)} 条款式,来自 {sheets_with_data} 个Sheet,{images_count} 张图片"
127
  return status, all_data, all_images
128
 
129
  except Exception as e:
130
- return f"加载失败: {str(e)}", [], {}
131
 
132
  def search_sku(keyword, all_data):
133
- """搜索款号"""
134
  if not keyword or not all_data:
135
  return []
136
-
137
  keyword = keyword.lower().strip()
138
  matches = []
139
-
140
  for item in all_data:
141
  if keyword in item['sku'].lower():
142
  label = f"{item['sku']} [{item['sheet']}]"
143
  if item['has_image']:
144
- label += " 📷"
145
  matches.append((label, item['unique_id']))
146
-
147
  return matches[:50]
148
 
149
- def get_image_base64(all_images, sheet_name, excel_row, max_size=80):
150
- """获取图片的base64"""
151
  key = (sheet_name, excel_row)
152
  if key not in all_images:
153
  return None
154
-
155
  try:
156
  img_data = all_images[key]
157
  img = Image.open(io.BytesIO(img_data))
158
- img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
159
  buffer = io.BytesIO()
160
  img.save(buffer, format='PNG')
161
  return base64.b64encode(buffer.getvalue()).decode()
@@ -163,75 +145,57 @@ def get_image_base64(all_images, sheet_name, excel_row, max_size=80):
163
  return None
164
 
165
  def update_selected_table(selected_ids, all_data, all_images):
166
- """更新已选表格"""
167
  if not selected_ids or not all_data:
168
- return "<p style='text-align:center;color:#999;padding:40px;'>还没有选择任何款式<br>在左边搜索添加</p>", 0
169
-
170
- selected_items = []
171
- for uid in selected_ids:
172
- for item in all_data:
173
- if item['unique_id'] == uid:
174
- selected_items.append(item)
175
- break
176
 
 
177
  if not selected_items:
178
- return "<p style='text-align:center;color:#999;padding:40px;'>还没有选择任何款式</p>", 0
179
-
180
- html = '''
181
- <style>
182
- .sel-table { width:100%; border-collapse:collapse; font-size:13px; }
183
- .sel-table th { background:#f0f4ff; padding:12px 8px; text-align:left; border-bottom:2px solid #ddd; font-weight:600; }
184
- .sel-table td { padding:12px 8px; border-bottom:1px solid #eee; vertical-align:middle; }
185
- .sel-table tr:hover { background:#f9f9ff; }
186
- .sku-cell { font-weight:600; color:#6366f1; }
187
- .price-cell { color:#f59e0b; font-weight:600; }
188
- .sheet-cell { background:#ede9fe; color:#7c3aed; padding:2px 8px; border-radius:4px; font-size:11px; display:inline-block; }
189
- .img-thumb { width:60px; height:60px; object-fit:cover; border-radius:6px; border:1px solid #e0e0e0; }
190
- .no-img { width:60px; height:60px; background:#f5f5f5; border-radius:6px; display:flex; align-items:center; justify-content:center; color:#bbb; font-size:11px; }
191
- </style>
192
- <table class="sel-table">
193
- <thead><tr>
194
- <th>序号</th><th>图片</th><th>款号</th><th>材质</th><th>颜色</th><th>尺码</th><th>价格</th><th>来源</th>
195
- </tr></thead>
196
- <tbody>
197
- '''
198
 
 
199
  for i, item in enumerate(selected_items, 1):
200
  b64 = get_image_base64(all_images, item['sheet'], item['excel_row'])
201
- img_html = f'<img class="img-thumb" src="data:image/png;base64,{b64}">' if b64 else '<div class="no-img">无图</div>'
 
 
 
202
 
203
- def truncate(s, n):
204
  s = str(s) if s else '-'
205
- return s[:n] + '...' if len(s) > n else s
206
 
207
- html += f'''
208
- <tr>
209
- <td>{i}</td>
210
- <td>{img_html}</td>
211
- <td class="sku-cell">{item['sku']}</td>
212
- <td>{truncate(item['material'], 12)}</td>
213
- <td>{truncate(item['color'], 12)}</td>
214
- <td>{truncate(item['size'], 8)}</td>
215
- <td class="price-cell">{item['price'] or '-'}</td>
216
- <td><span class="sheet-cell">{item['sheet']}</span></td>
217
- </tr>
218
- '''
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
- html += '</tbody></table>'
221
  return html, len(selected_items)
222
 
223
  def export_excel_with_images(selected_ids, customer_name, all_data, all_images):
224
- """导出带图片的Excel"""
225
  if not selected_ids or not all_data:
226
  return None
227
 
228
- selected_items = []
229
- for uid in selected_ids:
230
- for item in all_data:
231
- if item['unique_id'] == uid:
232
- selected_items.append(item)
233
- break
234
-
235
  if not selected_items:
236
  return None
237
 
@@ -244,9 +208,7 @@ def export_excel_with_images(selected_ids, customer_name, all_data, all_images):
244
  cell = ws.cell(row=1, column=col, value=header)
245
  cell.font = openpyxl.styles.Font(bold=True)
246
  cell.fill = openpyxl.styles.PatternFill(start_color="E0E7FF", end_color="E0E7FF", fill_type="solid")
247
- cell.alignment = openpyxl.styles.Alignment(horizontal='center', vertical='center')
248
 
249
- ws.row_dimensions[1].height = 25
250
  ws.column_dimensions['A'].width = 6
251
  ws.column_dimensions['B'].width = 15
252
  ws.column_dimensions['C'].width = 14
@@ -258,7 +220,6 @@ def export_excel_with_images(selected_ids, customer_name, all_data, all_images):
258
 
259
  for row_num, item in enumerate(selected_items, 2):
260
  ws.row_dimensions[row_num].height = 80
261
-
262
  ws.cell(row=row_num, column=1, value=row_num - 1)
263
  ws.cell(row=row_num, column=3, value=item['sku'])
264
  ws.cell(row=row_num, column=4, value=item['material'])
@@ -267,170 +228,100 @@ def export_excel_with_images(selected_ids, customer_name, all_data, all_images):
267
  ws.cell(row=row_num, column=7, value=item['price'])
268
  ws.cell(row=row_num, column=8, value=item['sheet'])
269
 
270
- for col in range(1, 9):
271
- ws.cell(row=row_num, column=col).alignment = openpyxl.styles.Alignment(vertical='center', wrap_text=True)
272
-
273
  img_key = (item['sheet'], item['excel_row'])
274
  if img_key in all_images:
275
  try:
276
  img_data = all_images[img_key]
277
  img = Image.open(io.BytesIO(img_data))
278
  img.thumbnail((100, 100), Image.Resampling.LANCZOS)
279
-
280
  with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
281
  img.save(tmp.name, 'PNG')
282
  tmp_path = tmp.name
283
-
284
  xl_img = XLImage(tmp_path)
285
  xl_img.width = 75
286
  xl_img.height = 75
287
  ws.add_image(xl_img, f'B{row_num}')
288
-
289
  os.unlink(tmp_path)
290
- except Exception as e:
291
- print(f"插入图片失败: {e}")
292
 
293
  today = datetime.now().strftime('%Y-%m-%d')
294
  customer = customer_name.strip() if customer_name else '客户'
295
  filename = f"{customer}的选款_{today}.xlsx"
296
-
297
  output_path = os.path.join(tempfile.gettempdir(), filename)
298
  wb.save(output_path)
299
  wb.close()
300
-
301
  return output_path
302
 
303
- # ============ Gradio 界面 ============
304
 
305
- with gr.Blocks(
306
- title="款式选择器",
307
- theme=gr.themes.Soft(primary_hue="indigo"),
308
- css="""
309
- .main-title { text-align: center; margin-bottom: 20px; }
310
- .main-title h1 { font-size: 2em; margin-bottom: 5px; }
311
- .main-title p { color: #666; }
312
- """
313
- ) as app:
314
-
315
- # 状态存储
316
  all_data_state = gr.State([])
317
  all_images_state = gr.State({})
318
  selected_ids_state = gr.State([])
319
 
320
- gr.HTML("""
321
- <div class="main-title">
322
- <h1>📦 款式选择器</h1>
323
- <p>支持多Sheet + 自动提取嵌入图片 | 上传 → 搜索 → 选款 → 导出</p>
324
- </div>
325
- """)
326
 
327
  with gr.Row():
328
- # 左侧面板
329
  with gr.Column(scale=1):
330
- gr.Markdown("### 📤 第一步:上传款式表")
331
- file_input = gr.File(
332
- label="选择Excel文件(支持.xlsx)",
333
- file_types=[".xlsx", ".xls"],
334
- type="filepath"
335
- )
336
- load_status = gr.Textbox(label="状态", interactive=False, value="等待上传...")
337
 
338
- gr.Markdown("### 🔍 第二步:搜索款号")
339
- search_input = gr.Textbox(
340
- label="输入款号关键字",
341
- placeholder="如:PZ、PM200、HSZ...",
342
- interactive=True
343
- )
344
- search_results = gr.CheckboxGroup(
345
- label="搜索结果(勾选后点击添加)",
346
- choices=[],
347
- interactive=True
348
- )
349
- add_btn = gr.Button("➕ 添加选中项到右侧", variant="primary", size="lg")
350
 
351
- # 右侧面板
352
  with gr.Column(scale=2):
353
- gr.Markdown("### ✅ 第三步:确认已选款式")
354
  selected_count = gr.Number(label="已选数量", value=0, interactive=False)
355
- selected_table = gr.HTML(
356
- "<p style='text-align:center;color:#999;padding:40px;'>还没有选择任何款式<br>在左边搜索并添加</p>"
357
- )
358
 
359
- gr.Markdown("### 📥 第四步:导出")
360
  with gr.Row():
361
- customer_input = gr.Textbox(
362
- label="客户名",
363
- placeholder="输入客户名,如:张三",
364
- scale=2
365
- )
366
- clear_btn = gr.Button("🗑️ 清空已选", variant="secondary", scale=1)
367
-
368
- export_btn = gr.Button("📥 生成Excel(带图片)", variant="primary", size="lg")
369
- export_file = gr.File(label="⬇️ 点击下载", visible=True)
370
-
371
- # ============ 事件绑定 ============
372
 
373
  def on_file_upload(file):
374
  status, data, images = load_excel(file)
375
  return status, data, images, [], gr.update(choices=[])
376
 
377
- file_input.change(
378
- on_file_upload,
379
- inputs=[file_input],
380
- outputs=[load_status, all_data_state, all_images_state, selected_ids_state, search_results]
381
- )
382
 
383
  def on_search(keyword, all_data):
384
  if not keyword or not all_data:
385
  return gr.update(choices=[], value=[])
386
- matches = search_sku(keyword, all_data)
387
- return gr.update(choices=matches, value=[])
388
 
389
- search_input.change(
390
- on_search,
391
- inputs=[search_input, all_data_state],
392
- outputs=[search_results]
393
- )
394
 
395
  def on_add(search_result, current_selected, all_data, all_images):
396
  if not search_result:
397
  html, count = update_selected_table(current_selected, all_data, all_images)
398
  return current_selected, html, count, gr.update(value=[])
399
-
400
  new_selected = list(current_selected) if current_selected else []
401
  for uid in search_result:
402
  if uid not in new_selected:
403
  new_selected.append(uid)
404
-
405
  html, count = update_selected_table(new_selected, all_data, all_images)
406
  return new_selected, html, count, gr.update(value=[])
407
 
408
- add_btn.click(
409
- on_add,
410
- inputs=[search_results, selected_ids_state, all_data_state, all_images_state],
411
- outputs=[selected_ids_state, selected_table, selected_count, search_results]
412
- )
413
 
414
  def on_clear(all_data, all_images):
415
  html, count = update_selected_table([], all_data, all_images)
416
  return [], html, count
417
 
418
- clear_btn.click(
419
- on_clear,
420
- inputs=[all_data_state, all_images_state],
421
- outputs=[selected_ids_state, selected_table, selected_count]
422
- )
423
 
424
  def on_export(selected_ids, customer_name, all_data, all_images):
425
  if not selected_ids:
426
  return None
427
  return export_excel_with_images(selected_ids, customer_name, all_data, all_images)
428
 
429
- export_btn.click(
430
- on_export,
431
- inputs=[selected_ids_state, customer_input, all_data_state, all_images_state],
432
- outputs=[export_file]
433
- )
434
 
435
- # 启动
436
- app.launch()
 
1
  # -*- coding: utf-8 -*-
 
 
 
 
 
2
  import gradio as gr
3
  import pandas as pd
4
  import openpyxl
 
11
  from datetime import datetime
12
 
13
  def extract_images_from_excel(file_path):
 
14
  images = {}
15
  try:
16
  wb = openpyxl.load_workbook(file_path)
 
20
  anchor = img.anchor
21
  if hasattr(anchor, '_from'):
22
  row = anchor._from.row + 1
23
+ try:
24
+ img_data = img._data()
25
+ if img_data:
26
+ images[(sheet_name, row)] = img_data
27
+ except:
28
+ pass
 
 
 
29
  wb.close()
30
  except Exception as e:
31
+ print(f"提取图片出错: {e}")
32
  return images
33
 
34
  def find_header_row(df):
 
35
  keywords = ['款号', '货号', 'SKU', '序号', '编号']
36
  for idx, row in df.iterrows():
37
  for val in row.values:
38
  if pd.notna(val):
 
39
  for kw in keywords:
40
+ if kw.lower() in str(val).lower():
41
  return idx
42
  return 0
43
 
44
  def find_column(headers, possible_names):
 
45
  for i, h in enumerate(headers):
46
  if pd.notna(h):
 
47
  for name in possible_names:
48
+ if name.lower() in str(h).lower():
49
  return i
50
  return -1
51
 
52
  def load_excel(file):
 
53
  if file is None:
54
+ return "请先上传Excel文件", [], {}
55
 
56
  all_data = []
57
  all_images = {}
58
 
59
  try:
60
+ file_path = file if isinstance(file, str) else file.name
61
+ all_images = extract_images_from_excel(file_path)
62
+ xl = pd.ExcelFile(file_path)
 
63
 
64
  for sheet_name in xl.sheet_names:
65
  df = pd.read_excel(xl, sheet_name=sheet_name, header=None)
 
81
 
82
  for idx in range(header_row + 1, len(df)):
83
  row = df.iloc[idx]
84
+ sku = row.iloc[sku_col] if sku_col < len(row) else None
85
 
86
  if pd.isna(sku) or str(sku).strip() == '':
87
  continue
 
89
  excel_row = idx + 1
90
  has_image = (sheet_name, excel_row) in all_images
91
 
92
+ def safe_get(col_idx):
93
+ if col_idx >= 0 and col_idx < len(row) and pd.notna(row.iloc[col_idx]):
94
+ return str(row.iloc[col_idx]).strip()
95
+ return ''
96
+
97
  item = {
98
  'sku': str(sku).strip(),
99
+ 'name': safe_get(name_col),
100
+ 'material': safe_get(material_col),
101
+ 'color': safe_get(color_col),
102
+ 'size': safe_get(size_col),
103
+ 'price': safe_get(price_col),
104
  'sheet': sheet_name,
105
  'excel_row': excel_row,
106
  'has_image': has_image,
 
109
  all_data.append(item)
110
 
111
  xl.close()
112
+ sheets_count = len(set(item['sheet'] for item in all_data))
 
113
  images_count = sum(1 for item in all_data if item['has_image'])
114
+ status = f"加载成功! {len(all_data)}条款式, {sheets_count}个Sheet, {images_count}张图片"
 
115
  return status, all_data, all_images
116
 
117
  except Exception as e:
118
+ return f"加载失败: {str(e)}", [], {}
119
 
120
  def search_sku(keyword, all_data):
 
121
  if not keyword or not all_data:
122
  return []
 
123
  keyword = keyword.lower().strip()
124
  matches = []
 
125
  for item in all_data:
126
  if keyword in item['sku'].lower():
127
  label = f"{item['sku']} [{item['sheet']}]"
128
  if item['has_image']:
129
+ label += " (有图)"
130
  matches.append((label, item['unique_id']))
 
131
  return matches[:50]
132
 
133
+ def get_image_base64(all_images, sheet_name, excel_row):
 
134
  key = (sheet_name, excel_row)
135
  if key not in all_images:
136
  return None
 
137
  try:
138
  img_data = all_images[key]
139
  img = Image.open(io.BytesIO(img_data))
140
+ img.thumbnail((80, 80), Image.Resampling.LANCZOS)
141
  buffer = io.BytesIO()
142
  img.save(buffer, format='PNG')
143
  return base64.b64encode(buffer.getvalue()).decode()
 
145
  return None
146
 
147
  def update_selected_table(selected_ids, all_data, all_images):
 
148
  if not selected_ids or not all_data:
149
+ return "<p style='text-align:center;color:#888;padding:30px;'>还没有选择款式在左边搜索添加</p>", 0
 
 
 
 
 
 
 
150
 
151
+ selected_items = [item for item in all_data if item['unique_id'] in selected_ids]
152
  if not selected_items:
153
+ return "<p style='text-align:center;color:#888;padding:30px;'>还没有选择款式</p>", 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
+ rows_html = ""
156
  for i, item in enumerate(selected_items, 1):
157
  b64 = get_image_base64(all_images, item['sheet'], item['excel_row'])
158
+ if b64:
159
+ img_html = f'<img src="data:image/png;base64,{b64}" style="width:60px;height:60px;object-fit:cover;border-radius:4px;">'
160
+ else:
161
+ img_html = '<div style="width:60px;height:60px;background:#f0f0f0;border-radius:4px;display:flex;align-items:center;justify-content:center;color:#aaa;font-size:11px;">无图</div>'
162
 
163
+ def trunc(s, n=12):
164
  s = str(s) if s else '-'
165
+ return s[:n] + '..' if len(s) > n else s
166
 
167
+ rows_html += f'''<tr>
168
+ <td style="padding:8px;border-bottom:1px solid #eee;">{i}</td>
169
+ <td style="padding:8px;border-bottom:1px solid #eee;">{img_html}</td>
170
+ <td style="padding:8px;border-bottom:1px solid #eee;font-weight:600;color:#4f46e5;">{item['sku']}</td>
171
+ <td style="padding:8px;border-bottom:1px solid #eee;">{trunc(item['material'])}</td>
172
+ <td style="padding:8px;border-bottom:1px solid #eee;">{trunc(item['color'])}</td>
173
+ <td style="padding:8px;border-bottom:1px solid #eee;">{trunc(item['size'], 8)}</td>
174
+ <td style="padding:8px;border-bottom:1px solid #eee;color:#f59e0b;font-weight:600;">{item['price'] or '-'}</td>
175
+ <td style="padding:8px;border-bottom:1px solid #eee;"><span style="background:#e0e7ff;color:#4338ca;padding:2px 6px;border-radius:3px;font-size:11px;">{item['sheet']}</span></td>
176
+ </tr>'''
177
+
178
+ html = f'''<table style="width:100%;border-collapse:collapse;font-size:13px;">
179
+ <thead><tr style="background:#f5f3ff;">
180
+ <th style="padding:10px 8px;text-align:left;border-bottom:2px solid #c7d2fe;">序号</th>
181
+ <th style="padding:10px 8px;text-align:left;border-bottom:2px solid #c7d2fe;">图片</th>
182
+ <th style="padding:10px 8px;text-align:left;border-bottom:2px solid #c7d2fe;">款号</th>
183
+ <th style="padding:10px 8px;text-align:left;border-bottom:2px solid #c7d2fe;">材质</th>
184
+ <th style="padding:10px 8px;text-align:left;border-bottom:2px solid #c7d2fe;">颜色</th>
185
+ <th style="padding:10px 8px;text-align:left;border-bottom:2px solid #c7d2fe;">尺码</th>
186
+ <th style="padding:10px 8px;text-align:left;border-bottom:2px solid #c7d2fe;">价格</th>
187
+ <th style="padding:10px 8px;text-align:left;border-bottom:2px solid #c7d2fe;">来源</th>
188
+ </tr></thead>
189
+ <tbody>{rows_html}</tbody>
190
+ </table>'''
191
 
 
192
  return html, len(selected_items)
193
 
194
  def export_excel_with_images(selected_ids, customer_name, all_data, all_images):
 
195
  if not selected_ids or not all_data:
196
  return None
197
 
198
+ selected_items = [item for item in all_data if item['unique_id'] in selected_ids]
 
 
 
 
 
 
199
  if not selected_items:
200
  return None
201
 
 
208
  cell = ws.cell(row=1, column=col, value=header)
209
  cell.font = openpyxl.styles.Font(bold=True)
210
  cell.fill = openpyxl.styles.PatternFill(start_color="E0E7FF", end_color="E0E7FF", fill_type="solid")
 
211
 
 
212
  ws.column_dimensions['A'].width = 6
213
  ws.column_dimensions['B'].width = 15
214
  ws.column_dimensions['C'].width = 14
 
220
 
221
  for row_num, item in enumerate(selected_items, 2):
222
  ws.row_dimensions[row_num].height = 80
 
223
  ws.cell(row=row_num, column=1, value=row_num - 1)
224
  ws.cell(row=row_num, column=3, value=item['sku'])
225
  ws.cell(row=row_num, column=4, value=item['material'])
 
228
  ws.cell(row=row_num, column=7, value=item['price'])
229
  ws.cell(row=row_num, column=8, value=item['sheet'])
230
 
 
 
 
231
  img_key = (item['sheet'], item['excel_row'])
232
  if img_key in all_images:
233
  try:
234
  img_data = all_images[img_key]
235
  img = Image.open(io.BytesIO(img_data))
236
  img.thumbnail((100, 100), Image.Resampling.LANCZOS)
 
237
  with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
238
  img.save(tmp.name, 'PNG')
239
  tmp_path = tmp.name
 
240
  xl_img = XLImage(tmp_path)
241
  xl_img.width = 75
242
  xl_img.height = 75
243
  ws.add_image(xl_img, f'B{row_num}')
 
244
  os.unlink(tmp_path)
245
+ except:
246
+ pass
247
 
248
  today = datetime.now().strftime('%Y-%m-%d')
249
  customer = customer_name.strip() if customer_name else '客户'
250
  filename = f"{customer}的选款_{today}.xlsx"
 
251
  output_path = os.path.join(tempfile.gettempdir(), filename)
252
  wb.save(output_path)
253
  wb.close()
 
254
  return output_path
255
 
256
+ # ========== Gradio 界面 ==========
257
 
258
+ with gr.Blocks(title="款式选择器") as app:
 
 
 
 
 
 
 
 
 
 
259
  all_data_state = gr.State([])
260
  all_images_state = gr.State({})
261
  selected_ids_state = gr.State([])
262
 
263
+ gr.Markdown("# 📦 款式选择器\n支持多Sheet + 自动提取嵌入图片")
 
 
 
 
 
264
 
265
  with gr.Row():
 
266
  with gr.Column(scale=1):
267
+ gr.Markdown("### 📤 上传款式表")
268
+ file_input = gr.File(label="选择Excel文件", file_types=[".xlsx", ".xls"])
269
+ load_status = gr.Textbox(label="状态", value="等待上传...", interactive=False)
 
 
 
 
270
 
271
+ gr.Markdown("### 🔍 搜索款号")
272
+ search_input = gr.Textbox(label="输入款号关键字", placeholder="如:PZ、PM200...")
273
+ search_results = gr.CheckboxGroup(label="搜索结果(勾选后点添加)", choices=[])
274
+ add_btn = gr.Button("➕ 添加选中项", variant="primary")
 
 
 
 
 
 
 
 
275
 
 
276
  with gr.Column(scale=2):
277
+ gr.Markdown("### ✅ 已选款式")
278
  selected_count = gr.Number(label="已选数量", value=0, interactive=False)
279
+ selected_table = gr.HTML("<p style='text-align:center;color:#888;padding:30px;'>还没有选择款式</p>")
 
 
280
 
281
+ gr.Markdown("### 📥 导出")
282
  with gr.Row():
283
+ customer_input = gr.Textbox(label="客户名", placeholder="如:张三", scale=2)
284
+ clear_btn = gr.Button("🗑️ 清空", scale=1)
285
+ export_btn = gr.Button("📥 生成Excel(带图片)", variant="primary")
286
+ export_file = gr.File(label="点击下载")
 
 
 
 
 
 
 
287
 
288
  def on_file_upload(file):
289
  status, data, images = load_excel(file)
290
  return status, data, images, [], gr.update(choices=[])
291
 
292
+ file_input.change(on_file_upload, [file_input], [load_status, all_data_state, all_images_state, selected_ids_state, search_results])
 
 
 
 
293
 
294
  def on_search(keyword, all_data):
295
  if not keyword or not all_data:
296
  return gr.update(choices=[], value=[])
297
+ return gr.update(choices=search_sku(keyword, all_data), value=[])
 
298
 
299
+ search_input.change(on_search, [search_input, all_data_state], [search_results])
 
 
 
 
300
 
301
  def on_add(search_result, current_selected, all_data, all_images):
302
  if not search_result:
303
  html, count = update_selected_table(current_selected, all_data, all_images)
304
  return current_selected, html, count, gr.update(value=[])
 
305
  new_selected = list(current_selected) if current_selected else []
306
  for uid in search_result:
307
  if uid not in new_selected:
308
  new_selected.append(uid)
 
309
  html, count = update_selected_table(new_selected, all_data, all_images)
310
  return new_selected, html, count, gr.update(value=[])
311
 
312
+ add_btn.click(on_add, [search_results, selected_ids_state, all_data_state, all_images_state], [selected_ids_state, selected_table, selected_count, search_results])
 
 
 
 
313
 
314
  def on_clear(all_data, all_images):
315
  html, count = update_selected_table([], all_data, all_images)
316
  return [], html, count
317
 
318
+ clear_btn.click(on_clear, [all_data_state, all_images_state], [selected_ids_state, selected_table, selected_count])
 
 
 
 
319
 
320
  def on_export(selected_ids, customer_name, all_data, all_images):
321
  if not selected_ids:
322
  return None
323
  return export_excel_with_images(selected_ids, customer_name, all_data, all_images)
324
 
325
+ export_btn.click(on_export, [selected_ids_state, customer_input, all_data_state, all_images_state], [export_file])
 
 
 
 
326
 
327
+ app.launch()
 
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- gradio>=4.0.0
2
  pandas
3
  openpyxl
4
- pillow
 
1
+ gradio
2
  pandas
3
  openpyxl
4
+ pillow