app.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. import tkinter as tk
  2. from tkinter import filedialog, messagebox, ttk
  3. import pandas as pd
  4. from openpyxl import load_workbook
  5. import os
  6. import sys
  7. import shutil
  8. from collections import Counter
  9. from datetime import datetime
  10. class ExcelMapperApp:
  11. def __init__(self, root):
  12. self.root = root
  13. self.root.title("Excel 自动填表工具")
  14. self.root.geometry("800x600")
  15. self.root.resizable(True, True)
  16. if getattr(sys, 'frozen', False):
  17. self.base_dir = os.path.dirname(sys.executable)
  18. else:
  19. self.base_dir = os.path.dirname(os.path.abspath(__file__))
  20. self.source_file = tk.StringVar()
  21. self.header_row = tk.StringVar(value="4")
  22. self.source_header_row = tk.StringVar(value="2")
  23. self.df = None
  24. self.valid_values = {} # 存储 Valid Values 工作表的数据
  25. self.status_text = tk.StringVar(value="请选择来源Excel文件")
  26. self.setup_ui()
  27. def setup_ui(self):
  28. title_label = tk.Label(self.root, text="Excel 自动填表工具", font=("Arial", 16, "bold"))
  29. title_label.pack(pady=10)
  30. file_frame = tk.LabelFrame(self.root, text="文件选择", font=("Arial", 10, "bold"))
  31. file_frame.pack(pady=10, padx=20, fill="x")
  32. tk.Label(file_frame, text="来源数据Excel:").grid(row=0, column=0, sticky="w", pady=5)
  33. tk.Entry(file_frame, textvariable=self.source_file, width=50).grid(row=0, column=1, padx=5, pady=5)
  34. tk.Button(file_frame, text="浏览", command=self.select_source_file).grid(row=0, column=2, pady=5)
  35. tk.Label(file_frame, text="来源表头行号:").grid(row=1, column=0, sticky="w", pady=5)
  36. tk.Entry(file_frame, textvariable=self.source_header_row, width=10).grid(row=1, column=1, sticky="w", padx=5, pady=5)
  37. tk.Label(file_frame, text="(第2行是字段名)", fg="gray").grid(row=1, column=1, sticky="e", padx=5, pady=5)
  38. tk.Label(file_frame, text="模板表头行号:").grid(row=2, column=0, sticky="w", pady=5)
  39. tk.Entry(file_frame, textvariable=self.header_row, width=10).grid(row=2, column=1, sticky="w", padx=5, pady=5)
  40. tk.Label(file_frame, text="(第4行是字段名)", fg="gray").grid(row=2, column=1, sticky="e", padx=5, pady=5)
  41. template_path = os.path.join(self.base_dir, "模板.xlsx")
  42. tk.Label(file_frame, text=f"目标模板: {template_path}", fg="blue").grid(row=3, column=0, columnspan=3, sticky="w", pady=3)
  43. tk.Label(file_frame, text="(数据将直接追加到模板文件末尾)", fg="green").grid(row=4, column=0, columnspan=3, sticky="w", pady=3)
  44. btn_frame = tk.Frame(self.root)
  45. btn_frame.pack(pady=10)
  46. tk.Button(btn_frame, text="🔍 查看来源结构", command=self.inspect_source,
  47. bg="#9C27B0", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
  48. tk.Button(btn_frame, text="🔍 检查模板表头", command=self.check_template_header,
  49. bg="#FF9800", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
  50. tk.Button(btn_frame, text="🚀 追加到模板", command=self.generate_output,
  51. bg="#4CAF50", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
  52. tk.Button(btn_frame, text="清空日志", command=self.clear_log,
  53. bg="#f44336", fg="white", font=("Arial", 10), width=14).pack(side="left", padx=5)
  54. log_frame = tk.LabelFrame(self.root, text="运行日志", font=("Arial", 10, "bold"))
  55. log_frame.pack(pady=10, padx=20, fill="both", expand=True)
  56. self.log_text = tk.Text(log_frame, height=16, font=("Courier", 9))
  57. self.log_text.pack(side="left", fill="both", expand=True)
  58. scrollbar = tk.Scrollbar(log_frame, command=self.log_text.yview)
  59. scrollbar.pack(side="right", fill="y")
  60. self.log_text.config(yscrollcommand=scrollbar.set)
  61. status_bar = tk.Label(self.root, textvariable=self.status_text, relief="sunken", anchor="w",
  62. font=("Arial", 9), bg="#f0f0f0")
  63. status_bar.pack(side="bottom", fill="x")
  64. def log(self, msg):
  65. self.log_text.insert(tk.END, msg + "\n")
  66. self.log_text.see(tk.END)
  67. self.root.update()
  68. print(msg)
  69. def clear_log(self):
  70. self.log_text.delete(1.0, tk.END)
  71. def select_source_file(self):
  72. file_path = filedialog.askopenfilename(
  73. title="选择来源Excel文件",
  74. filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")]
  75. )
  76. if file_path:
  77. self.source_file.set(file_path)
  78. self.status_text.set(f"已选择: {os.path.basename(file_path)}")
  79. self.log(f"📁 已选择来源文件: {file_path}")
  80. def inspect_source(self):
  81. source_path = self.source_file.get()
  82. if not source_path:
  83. messagebox.showwarning("警告", "请先选择来源Excel文件!")
  84. return
  85. self.log("\n" + "="*60)
  86. self.log("🔍 分析来源Excel结构...")
  87. try:
  88. wb = load_workbook(source_path)
  89. ws = wb.active
  90. self.log(f"📊 共 {ws.max_row} 行, {ws.max_column} 列")
  91. self.log("\n📌 前3行 × 前20列:")
  92. for row in range(1, min(4, ws.max_row + 1)):
  93. values = []
  94. for col in range(1, min(21, ws.max_column + 1)):
  95. val = ws.cell(row=row, column=col).value
  96. if val:
  97. values.append(f"列{col}:{str(val)[:30]}")
  98. if values:
  99. self.log(f" 第{row}行: {' | '.join(values)}")
  100. self.log(f"\n📌 第2行(列名):")
  101. row2_vals = []
  102. for col in range(1, min(ws.max_column + 1, 30)):
  103. val = ws.cell(row=2, column=col).value
  104. if val:
  105. row2_vals.append(f"列{col}:{str(val)[:25]}")
  106. if row2_vals:
  107. self.log(f" {' | '.join(row2_vals)}")
  108. except Exception as e:
  109. self.log(f"❌ 分析失败: {str(e)}")
  110. def check_template_header(self):
  111. template_path = os.path.join(self.base_dir, "模板.xlsx")
  112. if not os.path.exists(template_path):
  113. self.log(f"❌ 找不到模板文件: {template_path}")
  114. return
  115. self.log("\n" + "="*60)
  116. self.log("🔍 检查模板表头...")
  117. try:
  118. wb = load_workbook(template_path)
  119. ws = wb.active
  120. self.log(f"📊 共 {ws.max_row} 行, {ws.max_column} 列")
  121. # 显示前5行
  122. for row in range(1, min(6, ws.max_row + 1)):
  123. values = []
  124. for col in range(1, min(31, ws.max_column + 1)):
  125. val = ws.cell(row=row, column=col).value
  126. if val:
  127. values.append(f"列{col}:{str(val)[:30]}")
  128. if values:
  129. self.log(f"\n📌 第{row}行: {' | '.join(values[:10])}")
  130. if len(values) > 10:
  131. self.log(f" ... 还有 {len(values)-10} 个有值单元格")
  132. else:
  133. self.log(f"\n📌 第{row}行: (全空)")
  134. # 检查 Valid Values 工作表
  135. if "Valid Values" in wb.sheetnames:
  136. self.log(f"\n✅ 找到 'Valid Values' 工作表")
  137. ws_valid = wb["Valid Values"]
  138. self.log(f" 共 {ws_valid.max_row} 行, {ws_valid.max_column} 列")
  139. # 读取前几行
  140. for row in range(1, min(6, ws_valid.max_row + 1)):
  141. values = []
  142. for col in range(1, min(10, ws_valid.max_column + 1)):
  143. val = ws_valid.cell(row=row, column=col).value
  144. if val:
  145. values.append(str(val)[:20])
  146. if values:
  147. self.log(f" Valid Values 第{row}行: {' | '.join(values)}")
  148. else:
  149. self.log(f"\n⚠️ 未找到 'Valid Values' 工作表")
  150. except Exception as e:
  151. self.log(f"❌ 检查失败: {str(e)}")
  152. def load_valid_values(self, wb):
  153. """从 Valid Values 工作表读取有效值"""
  154. valid_data = {}
  155. if "Valid Values" not in wb.sheetnames:
  156. self.log("⚠️ 未找到 'Valid Values' 工作表,将使用硬编码默认值")
  157. return valid_data
  158. ws_valid = wb["Valid Values"]
  159. # 读取第1行作为列名
  160. headers = []
  161. for col in range(1, ws_valid.max_column + 1):
  162. val = ws_valid.cell(row=1, column=col).value
  163. if val:
  164. headers.append(str(val).strip())
  165. else:
  166. headers.append(f"Column_{col}")
  167. # 读取数据行
  168. for row in range(2, ws_valid.max_row + 1):
  169. for col in range(1, len(headers) + 1):
  170. val = ws_valid.cell(row=row, column=col).value
  171. if val:
  172. header = headers[col - 1]
  173. if header not in valid_data:
  174. valid_data[header] = []
  175. if str(val).strip() not in valid_data[header]:
  176. valid_data[header].append(str(val).strip())
  177. return valid_data
  178. def generate_output(self):
  179. self.log("\n" + "="*60)
  180. self.log("🚀 开始处理...")
  181. source_path = self.source_file.get()
  182. template_path = os.path.join(self.base_dir, "模板.xlsx")
  183. if not source_path:
  184. messagebox.showwarning("警告", "请选择来源Excel文件!")
  185. return
  186. if not os.path.exists(template_path):
  187. self.log(f"❌ 找不到模板文件: {template_path}")
  188. messagebox.showerror("错误", f"找不到模板文件:\n{template_path}")
  189. return
  190. try:
  191. header_row = int(self.header_row.get())
  192. source_header = int(self.source_header_row.get())
  193. # ========== 1. 备份模板 ==========
  194. backup_path = template_path.replace(".xlsx", f"_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx")
  195. shutil.copy2(template_path, backup_path)
  196. self.log(f"📦 已备份模板到: {backup_path}")
  197. # ========== 2. 读取来源数据 ==========
  198. self.log(f"📖 读取来源数据: {source_path}")
  199. self.log(f"📌 使用第 {source_header} 行作为列名")
  200. self.df = pd.read_excel(source_path, header=source_header - 1)
  201. self.log(f"✅ 成功读取 {len(self.df)} 行数据")
  202. # ========== 3. 加载模板并读取 Valid Values ==========
  203. self.log(f"\n📖 加载模板: {template_path}")
  204. wb = load_workbook(template_path)
  205. ws = wb.active
  206. # 加载 Valid Values
  207. self.valid_values = self.load_valid_values(wb)
  208. self.log(f"📋 从 Valid Values 读取到 {len(self.valid_values)} 个字段的有效值")
  209. # ========== 4. 验证模板表头 ==========
  210. col_indices = {}
  211. for col in range(1, ws.max_column + 1):
  212. cell_value = ws.cell(row=header_row, column=col).value
  213. if cell_value:
  214. col_name = str(cell_value).strip()
  215. if col_name and not col_name.startswith("Unnamed"):
  216. col_indices[col_name] = col
  217. self.log(f"📊 模板中共有 {len(col_indices)} 个有名称的列")
  218. # ========== 5. 找到已有数据的最后一行 ==========
  219. last_row = 5
  220. for row in range(5, ws.max_row + 2):
  221. is_empty = True
  222. for col in range(1, min(20, ws.max_column + 1)):
  223. if ws.cell(row=row, column=col).value is not None:
  224. is_empty = False
  225. break
  226. if is_empty:
  227. last_row = row
  228. break
  229. else:
  230. last_row = ws.max_row + 1
  231. self.log(f"📌 已有数据到第 {last_row - 1} 行,从第 {last_row} 行开始追加")
  232. # ========== 6. 定义默认值映射 ==========
  233. # 格式: 目标列名 -> (值来源, 处理方式)
  234. # 值来源: 'fixed' 固定值, 'valid' 从Valid Values取第一个, 'source' 从来源取, 'computed' 计算
  235. default_mapping = {
  236. # 固定值
  237. "Variant Type": ("fixed", "Not Variant"),
  238. "Minimum Order Quantity": ("fixed", 1),
  239. "Force Quantity Multiplier": ("fixed", 1),
  240. "Display Set Quantity": ("fixed", 1),
  241. "Ship Type": ("valid", "Ship Type"), # 从Valid Values取第一个
  242. "Lead Time": ("fixed", 48),
  243. "Replacement Lead Time": ("fixed", 120),
  244. "Flat Pack": ("fixed", "Yes"),
  245. "Assembly Required": ("fixed", "Yes"),
  246. "Canada Product Restriction": ("fixed", "Yes"),
  247. "CARB Phase II Compliant (formaldehyde emissions)": ("fixed", "Yes"),
  248. "Commercial Warranty": ("fixed", "Yes"),
  249. "CANFER Compliant": ("fixed", "Yes"),
  250. "Commercial Warranty Length": ("fixed", "1 Years"),
  251. "Composite Wood Product (CWP)": ("fixed", "Yes"),
  252. "Country Of Manufacturer": ("fixed", "China"),
  253. "General Certificate of Conformity (GCC)": ("fixed", "Yes"),
  254. "Hazard Class(es)": ("fixed", "Does Not Apply"),
  255. "Hazardous Material / Dangerous Goods": ("fixed", "No"),
  256. "Hazardous Material Weight": ("fixed", "Does Not Apply"),
  257. "ISTA Certified": ("popup", ""), # 弹出输入框
  258. "Level of Assembly": ("fixed", "Full Assembly Needed"),
  259. "Packing Group": ("fixed", "Does Not Apply"),
  260. "Reason for Restriction": ("fixed", "Does Not Apply"),
  261. "Supplier Intended and Approved Use": ("fixed", "Residential Use"),
  262. "TSCA Title VI Compliant (formaldehyde emissions)": ("fixed", "Yes"),
  263. "UN or ID number": ("fixed", "Does Not Apply"),
  264. "Uniform Packaging and Labeling Regulations (UPLR) Compliant": ("fixed", "Yes"),
  265. "Warning Required": ("fixed", "No"),
  266. "Warranty Length": ("fixed", "1 Years"),
  267. "Battery or Batteries Included": ("fixed", "No"),
  268. # 动态值(从来源取)
  269. "Product Type": ("source", "产品类目"),
  270. "Brand": ("valid", "Brand"), # 从Valid Values取第一个
  271. }
  272. # ========== 7. 匹配来源列 ==========
  273. source_mapping = {
  274. "Item Code": "Item Code",
  275. "产品名称": "产品名称",
  276. "优惠单价": "优惠单价",
  277. "产品重量": "产品重量(磅)",
  278. "包装尺寸-重量": "包装尺寸-重量(磅)",
  279. "包装尺寸-高度": "包装尺寸-高度(英寸)",
  280. "包装尺寸-宽度": "包装尺寸-宽度(英寸)",
  281. "包装尺寸-长度": "包装尺寸-长度(英寸)",
  282. "颜色": "颜色",
  283. "材质": "材质",
  284. "组装长度": "组装长度(英寸)",
  285. "组装高度": "组装高度(英寸)",
  286. "组装宽度": "组装宽度(英寸)",
  287. "产品类目": "产品类目",
  288. }
  289. self.log(f"\n🔍 匹配来源列:")
  290. source_map = {}
  291. for key, expected in source_mapping.items():
  292. found = False
  293. for col in self.df.columns:
  294. if col == expected:
  295. source_map[key] = col
  296. found = True
  297. break
  298. if found:
  299. self.log(f" ✅ '{key}' -> '{source_map[key]}'")
  300. else:
  301. self.log(f" ❌ '{key}' -> 未找到")
  302. source_map[key] = None
  303. # ========== 8. 填充数据 ==========
  304. def get_key(row):
  305. item = row.get("Item Code", "")
  306. name = row.get("产品名称", "")
  307. return f"{item}_{name}"
  308. keys = [get_key(row) for _, row in self.df.iterrows()]
  309. key_count = Counter(keys)
  310. key_index = Counter()
  311. row_num = last_row
  312. filled_count = 0
  313. self.log(f"\n✍️ 开始追加数据...")
  314. for idx, (_, src_row) in enumerate(self.df.iterrows()):
  315. item_code = src_row.get("Item Code", "")
  316. product_name = src_row.get("产品名称", "")
  317. if pd.isna(item_code):
  318. item_code = ""
  319. if pd.isna(product_name):
  320. product_name = ""
  321. key = get_key(src_row)
  322. if key_count[key] > 1:
  323. key_index[key] += 1
  324. suffix = f".{key_index[key] - 1}"
  325. mfr_suffix = f".{key_index[key]}"
  326. else:
  327. suffix = ""
  328. mfr_suffix = ""
  329. self.log(f"\n--- 第 {idx+1}/{len(self.df)} 行 ---")
  330. self.log(f" Item Code: '{item_code}'")
  331. self.log(f" 产品名称: '{str(product_name)[:50]}...'")
  332. fill_count = 0
  333. # ===== Supplier Part Number =====
  334. if "Supplier Part Number" in col_indices:
  335. val = f"{item_code}{product_name}{suffix}"
  336. ws.cell(row=row_num, column=col_indices["Supplier Part Number"], value=val)
  337. fill_count += 1
  338. # ===== Manufacturer Part Number =====
  339. if "Manufacturer Part Number" in col_indices:
  340. val = f"{item_code}{product_name}{mfr_suffix}"
  341. ws.cell(row=row_num, column=col_indices["Manufacturer Part Number"], value=val)
  342. fill_count += 1
  343. # ===== Base Cost =====
  344. if "Base Cost" in col_indices and source_map.get("优惠单价"):
  345. col = source_map["优惠单价"]
  346. val = src_row.get(col, "")
  347. if pd.notna(val):
  348. ws.cell(row=row_num, column=col_indices["Base Cost"], value=val)
  349. fill_count += 1
  350. # ===== MSRP =====
  351. if "Manufacturer Suggested Retail Price" in col_indices and source_map.get("优惠单价"):
  352. col = source_map["优惠单价"]
  353. val = src_row.get(col, 0)
  354. try:
  355. num = float(val) if pd.notna(val) else 0
  356. ws.cell(row=row_num, column=col_indices["Manufacturer Suggested Retail Price"], value=num + 70)
  357. fill_count += 1
  358. except:
  359. pass
  360. # ===== Product Weight =====
  361. if "Product Weight" in col_indices and source_map.get("产品重量"):
  362. col = source_map["产品重量"]
  363. val = src_row.get(col, "")
  364. if pd.notna(val):
  365. ws.cell(row=row_num, column=col_indices["Product Weight"], value=val)
  366. fill_count += 1
  367. # ===== Carton 尺寸 =====
  368. carton_map = [
  369. ("Carton Weight 1", "包装尺寸-重量"),
  370. ("Carton Height 1", "包装尺寸-高度"),
  371. ("Carton Width 1", "包装尺寸-宽度"),
  372. ("Carton Depth 1", "包装尺寸-长度"),
  373. ]
  374. for target, src_name in carton_map:
  375. if target in col_indices and source_map.get(src_name):
  376. col = source_map[src_name]
  377. val = src_row.get(col, "")
  378. if pd.notna(val):
  379. ws.cell(row=row_num, column=col_indices[target], value=val)
  380. fill_count += 1
  381. # ===== Color/Material =====
  382. cm_map = [
  383. ("Base Color", "颜色"),
  384. ("Base Material", "材质"),
  385. ("Color", "颜色"),
  386. ("Material", "材质"),
  387. ("Top Color", "颜色"),
  388. ("Top Material", "材质"),
  389. ]
  390. for target, src_name in cm_map:
  391. if target in col_indices and source_map.get(src_name):
  392. col = source_map[src_name]
  393. val = src_row.get(col, "")
  394. if pd.notna(val):
  395. ws.cell(row=row_num, column=col_indices[target], value=val)
  396. fill_count += 1
  397. # ===== Overall 尺寸 =====
  398. overall_map = [
  399. ("Overall Depth - Front to Back", "组装长度"),
  400. ("Overall Height - Top to Bottom", "组装高度"),
  401. ("Overall Product Weight", "产品重量"),
  402. ("Overall Width - Side to Side", "组装宽度"),
  403. ]
  404. for target, src_name in overall_map:
  405. if target in col_indices and source_map.get(src_name):
  406. col = source_map[src_name]
  407. val = src_row.get(col, "")
  408. if pd.notna(val):
  409. ws.cell(row=row_num, column=col_indices[target], value=val)
  410. fill_count += 1
  411. # ===== 默认值字段 =====
  412. variant_type = "Not Variant" # 默认值
  413. for target_col, (source_type, source_val) in default_mapping.items():
  414. if target_col not in col_indices:
  415. continue
  416. col_idx = col_indices[target_col]
  417. if source_type == "fixed":
  418. # 固定值
  419. ws.cell(row=row_num, column=col_idx, value=source_val)
  420. fill_count += 1
  421. self.log(f" ✅ {target_col} = '{source_val}' (固定值)")
  422. elif source_type == "valid":
  423. # 从 Valid Values 取第一个值
  424. if source_val in self.valid_values and self.valid_values[source_val]:
  425. val = self.valid_values[source_val][0]
  426. ws.cell(row=row_num, column=col_idx, value=val)
  427. fill_count += 1
  428. self.log(f" ✅ {target_col} = '{val}' (从Valid Values)")
  429. else:
  430. self.log(f" ⚠️ {target_col}: Valid Values中无数据,使用默认值'Not Variant'")
  431. ws.cell(row=row_num, column=col_idx, value="Not Variant")
  432. fill_count += 1
  433. elif source_type == "source":
  434. # 从来源取
  435. if source_map.get(source_val):
  436. col = source_map[source_val]
  437. val = src_row.get(col, "")
  438. if pd.notna(val):
  439. ws.cell(row=row_num, column=col_idx, value=val)
  440. fill_count += 1
  441. self.log(f" ✅ {target_col} = '{val}' (从来源)")
  442. else:
  443. self.log(f" ⚠️ {target_col}: 来源为空")
  444. else:
  445. self.log(f" ⚠️ {target_col}: 找不到来源列'{source_val}'")
  446. elif source_type == "popup":
  447. # 弹出输入框(ISTA Certified)
  448. self.log(f" ⏳ 请为 {target_col} 输入值...")
  449. # 这里用简单方式:如果10秒没输入就跳过
  450. result = self.show_input_dialog(f"请输入 {target_col}", "包装认证 (ISTA Certified):")
  451. if result:
  452. ws.cell(row=row_num, column=col_idx, value=result)
  453. fill_count += 1
  454. self.log(f" ✅ {target_col} = '{result}' (用户输入)")
  455. else:
  456. self.log(f" ⏭️ {target_col}: 用户跳过")
  457. # ===== 特殊逻辑:Variant Grouping 1/2 =====
  458. # Variant Grouping 1: 如果 Variant Type 是 Not Variant 则不填,否则填 "Color"
  459. if "Variant Grouping 1" in col_indices:
  460. if variant_type != "Not Variant":
  461. ws.cell(row=row_num, column=col_indices["Variant Grouping 1"], value="Color")
  462. fill_count += 1
  463. if "Variant Attribute Name On Site 1" in col_indices:
  464. if variant_type != "Not Variant":
  465. ws.cell(row=row_num, column=col_indices["Variant Attribute Name On Site 1"], value="Finish")
  466. fill_count += 1
  467. if "Variant Grouping 2" in col_indices:
  468. if variant_type != "Not Variant":
  469. ws.cell(row=row_num, column=col_indices["Variant Grouping 2"], value="Size")
  470. fill_count += 1
  471. if "Variant Attribute Name On Site 2" in col_indices:
  472. if variant_type != "Not Variant":
  473. ws.cell(row=row_num, column=col_indices["Variant Attribute Name On Site 2"], value="Dimensions")
  474. fill_count += 1
  475. self.log(f" 📊 本行填充 {fill_count} 个字段")
  476. row_num += 1
  477. filled_count += 1
  478. # ========== 9. 保存 ==========
  479. wb.save(template_path)
  480. self.log(f"\n✅ 成功追加到模板: {template_path}")
  481. self.log(f"📊 共追加 {filled_count} 行数据")
  482. self.log(f"📦 备份文件: {backup_path}")
  483. self.status_text.set(f"✅ 已追加 {filled_count} 行到模板")
  484. messagebox.showinfo("成功",
  485. f"已追加 {filled_count} 行数据到模板文件:\n{template_path}\n\n"
  486. f"备份文件保存在: {backup_path}"
  487. )
  488. except Exception as e:
  489. self.log(f"\n❌ 错误: {str(e)}")
  490. import traceback
  491. self.log(traceback.format_exc())
  492. messagebox.showerror("错误", f"生成失败:\n{str(e)}")
  493. def show_input_dialog(self, title, prompt):
  494. """显示输入对话框,10秒超时自动跳过"""
  495. result = tk.StringVar()
  496. dialog = tk.Toplevel(self.root)
  497. dialog.title(title)
  498. dialog.geometry("400x120")
  499. dialog.transient(self.root)
  500. dialog.grab_set()
  501. tk.Label(dialog, text=prompt, font=("Arial", 11)).pack(pady=10)
  502. entry = tk.Entry(dialog, width=40)
  503. entry.pack(pady=5)
  504. entry.focus()
  505. # 超时计数器
  506. timeout_seconds = 10
  507. time_label = tk.Label(dialog, text=f"剩余 {timeout_seconds} 秒...", fg="gray")
  508. time_label.pack(pady=5)
  509. def on_ok():
  510. result.set(entry.get())
  511. dialog.destroy()
  512. def on_cancel():
  513. dialog.destroy()
  514. def countdown(count):
  515. if count <= 0:
  516. dialog.destroy()
  517. return
  518. time_label.config(text=f"剩余 {count} 秒...")
  519. dialog.after(1000, countdown, count - 1)
  520. tk.Button(dialog, text="确定", command=on_ok, width=10).pack(side="left", padx=20, pady=10)
  521. tk.Button(dialog, text="跳过", command=on_cancel, width=10).pack(side="right", padx=20, pady=10)
  522. dialog.after(1000, countdown, timeout_seconds - 1)
  523. self.root.wait_window(dialog)
  524. return result.get()
  525. if __name__ == "__main__":
  526. root = tk.Tk()
  527. app = ExcelMapperApp(root)
  528. root.mainloop()