FD箱唛生成器2.0

代码:

import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import pandas as pd
from docx import Document
from docx.shared import Cm, Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
import os
import traceback
import threading
import queue
import sys
import re # 导入正则表达式库,用于文件名清理
import math # 导入数学库,用于向上取整

# 尝试导入pywin32库,如果失败则提示用户安装
try:
    import win32com.client
    import pythoncom # 导入pythoncom模块以支持多线程COM
except ImportError:
    # 在GUI完全加载前,无法直接使用messagebox
    # 这种启动时依赖检查的错误,最好在主程序入口处处理
    print("错误:缺少关键库 pywin32。请在命令行运行 'pip install pywin32' 来安装。")
    # 实际应用中可以创建一个简单的tk窗口来显示这个错误
    # 此处为简化,直接退出
    sys.exit(1)


# --- 配置区 ---
FONT_SIZES_CN = {
    '一号': 26, '小一': 24, '二号': 22, '小二': 18,
    '三号': 16, '小三': 15, '四号': 14, '五号': 10.5,
}
OUTPUT_FOLDER_NAME = "FD箱唛导出文件"

class LabelGeneratorApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Word & PDF 标签生成器 (双方案)")
        self.root.geometry("500x400") # 增加窗口高度以容纳新选项

        self.excel_path = ""
        self.task_queue = queue.Queue()

        # --- GUI 组件 ---
        main_frame = ttk.Frame(self.root, padding="10 10 10 10")
        main_frame.pack(fill=tk.BOTH, expand=True)

        # 1. 文件选择区
        select_file_button = ttk.Button(main_frame, text="1. 选择 Excel 文件", command=self.select_file)
        select_file_button.pack(pady=5, fill=tk.X)
        self.file_label = ttk.Label(main_frame, text="尚未选择文件")
        self.file_label.pack(pady=5)

        # 2. 方案选择区
        scheme_frame = ttk.LabelFrame(main_frame, text="2. 选择生成方案")
        scheme_frame.pack(pady=10, fill=tk.X, padx=5)
        
        self.scheme_var = tk.StringVar(value="scheme1") # 默认选择方案一

        # --- 修改点:更新方案一的描述 ---
        scheme1_rb = ttk.Radiobutton(scheme_frame, text="方案一:按序号生成", variable=self.scheme_var, value="scheme1")
        scheme1_rb.pack(anchor=tk.W, padx=10, pady=5)
        
        # --- 修改点:更新方案二的描述 ---
        scheme2_rb = ttk.Radiobutton(scheme_frame, text="方案二:按总数生成", variable=self.scheme_var, value="scheme2")
        scheme2_rb.pack(anchor=tk.W, padx=10, pady=5)

        # 3. 操作按钮区
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(pady=10, fill=tk.X, expand=True)

        self.generate_word_button = ttk.Button(button_frame, text="3. 生成 Word (.docx)", command=lambda: self.start_task('word'))
        self.generate_word_button.pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X)
        
        self.generate_pdf_button = ttk.Button(button_frame, text="3. 生成 PDF (.pdf)", command=lambda: self.start_task('pdf'))
        self.generate_pdf_button.pack(side=tk.RIGHT, padx=5, expand=True, fill=tk.X)

        # 4. 状态栏
        self.status_label = ttk.Label(main_frame, text="准备就绪", relief=tk.SUNKEN, anchor=tk.W)
        self.status_label.pack(side=tk.BOTTOM, fill=tk.X, pady=(10,0))
        
        self.process_queue()

    def select_file(self):
        path = filedialog.askopenfilename(
            title="请选择包含标签数据的 Excel 文件",
            filetypes=[("Excel Files", "*.xlsx *.xls")]
        )
        if path:
            self.excel_path = path
            filename = os.path.basename(path)
            self.file_label.config(text=f"已选择: {filename}")
            self.status_label.config(text="文件已加载,请选择生成方案和格式。")

    def process_queue(self):
        try:
            task = self.task_queue.get(block=False)
            # 根据任务类型更新GUI
            if task[0] == 'update_status':
                self.status_label.config(text=task[1])
            elif task[0] == 'task_done':
                self.enable_buttons()
                messagebox.showinfo(task[1], task[2])
                self.status_label.config(text=task[3])
            elif task[0] == 'task_error':
                self.enable_buttons()
                messagebox.showerror(task[1], task[2])
                self.status_label.config(text=task[3])
        except queue.Empty:
            pass
        finally:
            self.root.after(100, self.process_queue)

    def start_task(self, task_type):
        if not self.excel_path:
            messagebox.showerror("错误", "请先选择一个 Excel 文件!")
            return
            
        self.disable_buttons()
        # 将选择的方案和任务类型传递给后台线程
        scheme = self.scheme_var.get()
        thread = threading.Thread(target=self.run_generation_task, args=(scheme, task_type))
        thread.daemon = True
        thread.start()

    def disable_buttons(self):
        self.generate_word_button.config(state=tk.DISABLED)
        self.generate_pdf_button.config(state=tk.DISABLED)

    def enable_buttons(self):
        self.generate_word_button.config(state=tk.NORMAL)
        self.generate_pdf_button.config(state=tk.NORMAL)
        
    def add_formatted_paragraph(self, document, text, font_size_pt, is_bold=False):
        p = document.add_paragraph()
        p.alignment = WD_ALIGN_PARAGRAPH.LEFT
        p_format = p.paragraph_format
        p_format.line_spacing = 1.0
        p_format.space_before = Pt(0)
        p_format.space_after = Pt(0)
        run = p.add_run(text)
        font = run.font
        font.name = 'Calibri'
        font.size = Pt(font_size_pt)
        font.bold = is_bold

    def run_generation_task(self, scheme, task_type):
        """根据选择的方案执行不同的生成逻辑"""
        # 为后台线程初始化COM库
        pythoncom.CoInitialize()
        try:
            self.task_queue.put(('update_status', "正在读取Excel文件..."))
            df = pd.read_excel(self.excel_path, engine='openpyxl')
            
            required_columns = ['PI', 'ASIN', 'SKU', '箱数']
            if not all(col in df.columns for col in required_columns):
                raise ValueError(f"Excel 文件必须包含以下列: {', '.join(required_columns)}")

            # 清理'箱数'列,将非数字转为0
            df['箱数'] = pd.to_numeric(df['箱数'], errors='coerce').fillna(0).astype(int)
            # 过滤掉箱数为0的行
            df = df[df['箱数'] > 0]
            if df.empty:
                raise ValueError("未找到任何箱数大于0的有效数据行。")

            if scheme == 'scheme1':
                self.run_scheme1(df, task_type)
            elif scheme == 'scheme2':
                self.run_scheme2(df, task_type)

        except Exception as e:
            error_message = f"处理过程中出现错误: \n{traceback.format_exc()}"
            self.task_queue.put(('task_error', "发生错误", error_message, "操作失败。"))
        finally:
            # 确保COM库被正确释放
            pythoncom.CoUninitialize() 

    def convert_to_pdf_single(self, docx_path, pdf_path):
        """
        启动一个独立的Word进程来执行单个文件转换,以确保最大稳定性。
        """
        word_app = None
        word_doc = None
        try:
            word_app = win32com.client.gencache.EnsureDispatch("Word.Application")
            word_app.Visible = False
            
            abs_docx_path = os.path.abspath(docx_path)
            abs_pdf_path = os.path.abspath(pdf_path)
            
            word_doc = word_app.Documents.Open(abs_docx_path)
            
            # 强制设置为10x10cm
            points_10_cm = 283.5 
            word_doc.PageSetup.PageWidth = points_10_cm
            word_doc.PageSetup.PageHeight = points_10_cm
            
            word_doc.SaveAs(abs_pdf_path, FileFormat=17) # 17代表PDF格式
        finally:
            if word_doc:
                word_doc.Close(False) # 关闭文档,不保存更改
            if word_app:
                word_app.Quit()

    def run_scheme1(self, df, task_type):
        """方案一:按序号生成"""
        doc = Document()
        section = doc.sections[0]
        section.page_width = Cm(10); section.page_height = Cm(10)
        section.left_margin = Cm(1.27); section.right_margin = Cm(1.27)
        section.top_margin = Cm(1.27); section.bottom_margin = Cm(1.27)
        
        self.task_queue.put(('update_status', "方案一:正在生成标签内容..."))
        total_labels_generated = 0
        for _, row in df.iterrows():
            pi = str(row['PI']); asin = str(row['ASIN']); sku = str(row['SKU'])
            carton_count = row['箱数']
            for i in range(1, carton_count + 1):
                if total_labels_generated > 0: doc.add_page_break()
                self.add_formatted_paragraph(doc, "Customer ID: FD", FONT_SIZES_CN['二号'])
                doc.add_paragraph().paragraph_format.space_after = Pt(0)
                self.add_formatted_paragraph(doc, f"ASIN: {asin}", FONT_SIZES_CN['二号'], is_bold=True)
                sku_len = len(sku)
                if sku_len <= 9: sku_font_size = FONT_SIZES_CN['一号']
                elif sku_len <= 10: sku_font_size = FONT_SIZES_CN['二号']
                elif sku_len <= 14: sku_font_size = FONT_SIZES_CN['小二']
                elif sku_len <= 18: sku_font_size = FONT_SIZES_CN['三号']
                else: sku_font_size = FONT_SIZES_CN['小三']
                self.add_formatted_paragraph(doc, f"SKU: {sku}", sku_font_size, is_bold=False)
                pi_len = len(pi)
                if pi_len <= 12: pi_font_size = 28
                elif pi_len <= 15: pi_font_size = 22
                elif pi_len <= 19: pi_font_size = 18
                elif pi_len <= 24: pi_font_size = 14
                else: pi_font_size = 12
                self.add_formatted_paragraph(doc, f"PI: {pi}", pi_font_size, is_bold=True)
                self.add_formatted_paragraph(doc, f"CARTON QTY: {i}/{carton_count}", 20)
                self.add_formatted_paragraph(doc, "MADE IN CHINA", FONT_SIZES_CN['二号'])
                total_labels_generated += 1
        
        if task_type == 'word':
            output_path = filedialog.asksaveasfilename(title="保存 Word 文件", defaultextension=".docx", filetypes=[("Word Document", "*.docx")])
            if output_path:
                doc.save(output_path)
                self.task_queue.put(('task_done', "成功", f"成功生成 {total_labels_generated} 页!\nWord文件已保存。", "Word文件生成成功!"))
        elif task_type == 'pdf':
            pdf_path = filedialog.asksaveasfilename(title="保存 PDF 文件", defaultextension=".pdf", filetypes=[("PDF Document", "*.pdf")])
            if pdf_path:
                temp_docx_path = os.path.splitext(pdf_path)[0] + "_temp.docx"
                try:
                    doc.save(temp_docx_path)
                    self.task_queue.put(('update_status', "正在调用Word程序转换PDF..."))
                    self.convert_to_pdf_single(temp_docx_path, pdf_path)
                    self.task_queue.put(('task_done', "成功", f"成功生成 {total_labels_generated} 页!\nPDF文件已保存。", "PDF文件生成成功!"))
                finally:
                    if os.path.exists(temp_docx_path): os.remove(temp_docx_path)

    def run_scheme2(self, df, task_type):
        """方案二:按总数生成"""
        doc = Document()
        section = doc.sections[0]
        section.page_width = Cm(10); section.page_height = Cm(10)
        section.left_margin = Cm(1.27); section.right_margin = Cm(1.27)
        section.top_margin = Cm(1.27); section.bottom_margin = Cm(1.27)
        
        self.task_queue.put(('update_status', "方案二:正在生成概览内容..."))
        
        grand_total_cartons = df['箱数'].sum()
        
        for i, (index, row) in enumerate(df.iterrows()):
            pi = str(row['PI']); asin = str(row['ASIN']); sku = str(row['SKU'])
            carton_count = row['箱数']

            # 如果不是第一页,就添加一个分页符
            if i > 0:
                doc.add_page_break()

            carton_qty_text = f"CARTON QTY: {carton_count}/{grand_total_cartons}"
            
            self.add_formatted_paragraph(doc, "Customer ID: FD", FONT_SIZES_CN['二号'])
            doc.add_paragraph().paragraph_format.space_after = Pt(0)
            self.add_formatted_paragraph(doc, f"ASIN: {asin}", FONT_SIZES_CN['二号'], is_bold=True)
            sku_len = len(sku)
            if sku_len <= 9: sku_font_size = FONT_SIZES_CN['一号']
            elif sku_len <= 10: sku_font_size = FONT_SIZES_CN['二号']
            elif sku_len <= 14: sku_font_size = FONT_SIZES_CN['小二']
            elif sku_len <= 18: sku_font_size = FONT_SIZES_CN['三号']
            else: sku_font_size = FONT_SIZES_CN['小三']
            self.add_formatted_paragraph(doc, f"SKU: {sku}", sku_font_size, is_bold=False)
            pi_len = len(pi)
            if pi_len <= 12: pi_font_size = 28
            elif pi_len <= 15: pi_font_size = 22
            elif pi_len <= 19: pi_font_size = 18
            elif pi_len <= 24: pi_font_size = 14
            else: pi_font_size = 12
            self.add_formatted_paragraph(doc, f"PI: {pi}", pi_font_size, is_bold=True)
            self.add_formatted_paragraph(doc, carton_qty_text, 20)
            self.add_formatted_paragraph(doc, "MADE IN CHINA", FONT_SIZES_CN['二号'])

        total_pages = len(df)
        if task_type == 'word':
            output_path = filedialog.asksaveasfilename(title="保存 Word 文件", defaultextension=".docx", filetypes=[("Word Document", "*.docx")])
            if output_path:
                doc.save(output_path)
                self.task_queue.put(('task_done', "成功", f"成功生成 {total_pages} 页!\nWord文件已保存。", "Word文件生成成功!"))
        elif task_type == 'pdf':
            pdf_path = filedialog.asksaveasfilename(title="保存 PDF 文件", defaultextension=".pdf", filetypes=[("PDF Document", "*.pdf")])
            if pdf_path:
                temp_docx_path = os.path.splitext(pdf_path)[0] + "_temp.docx"
                try:
                    doc.save(temp_docx_path)
                    self.task_queue.put(('update_status', "正在调用Word程序转换PDF..."))
                    self.convert_to_pdf_single(temp_docx_path, pdf_path)
                    self.task_queue.put(('task_done', "成功", f"成功生成 {total_pages} 页!\nPDF文件已保存。", "PDF文件生成成功!"))
                finally:
                    if os.path.exists(temp_docx_path): os.remove(temp_docx_path)


if __name__ == "__main__":
    # 检查依赖是否成功导入
    try:
        import win32com.client
        import pythoncom
    except ImportError:
        root = tk.Tk()
        root.withdraw() # 隐藏主窗口
        messagebox.showerror("缺少关键依赖", "启动失败!\n\n缺少 pywin32 库。\n请先在命令行运行: pip install pywin32")
    else:
        root = tk.Tk()
        app = LabelGeneratorApp(root)
        root.mainloop()