Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import os | |
| import subprocess | |
| import tempfile | |
| import shutil | |
| import re | |
| import warnings | |
| import pandas as pd | |
| from datetime import datetime | |
| from typing import List, Dict, Optional, Iterable, Union | |
| from pathlib import Path | |
| import json | |
| # ============================================================================== | |
| # 核心功能函数 | |
| # ============================================================================== | |
| def escape_latex(text): | |
| """对字符串中的LaTeX特殊字符进行转义。""" | |
| if not isinstance(text, str): | |
| text = str(text) | |
| conv = { | |
| '\\': r'\textbackslash{}', '&': r'\&', '%': r'\%', '$': r'\$', | |
| '#': r'\#', '_': r'\_', '{': r'\{', '}': r'\}', | |
| '~': r'\textasciitilde{}', '^': r'\textasciicircum{}', | |
| } | |
| regex = re.compile('|'.join(re.escape(key) for key in sorted(conv.keys(), key=len, reverse=True))) | |
| return regex.sub(lambda match: conv[match.group()], text) | |
| def generate_quote_pdf(data, output_pdf_path, template_path): | |
| """根据提供的数据填充LaTeX模板并生成PDF报价单。""" | |
| if not shutil.which("xelatex"): | |
| raise gr.Error("错误: 'xelatex' 命令未找到。请确保已安装 TeX Live 或 MiKTeX 并将其添加至系统路径。") | |
| try: | |
| with open(template_path, 'r', encoding='utf-8') as f: | |
| template_content = f.read() | |
| except FileNotFoundError: | |
| raise gr.Error(f"错误: 模板文件 '{template_path}' 未找到。请确保它和脚本在同一目录下。") | |
| filled_content = template_content | |
| for key, value in data.items(): | |
| # 对于非LaTeX代码的值进行转义 | |
| if key not in ["discount_section", "discount_terms_note"]: | |
| safe_value = escape_latex(value) | |
| else: | |
| safe_value = value # 这些值本身包含LaTeX代码,不应转义 | |
| placeholder = f"{{{{{key}}}}}" | |
| filled_content = filled_content.replace(placeholder, safe_value) | |
| with tempfile.TemporaryDirectory() as temp_dir: | |
| temp_tex_path = os.path.join(temp_dir, 'quote.tex') | |
| with open(temp_tex_path, 'w', encoding='utf-8') as f: | |
| f.write(filled_content) | |
| for i in range(2): | |
| process = subprocess.run( | |
| ['xelatex', '-interaction=nonstopmode', 'quote.tex'], | |
| cwd=temp_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
| text=True, encoding='utf-8' | |
| ) | |
| if process.returncode != 0: | |
| log_path = os.path.join(temp_dir, 'quote.log') | |
| log_content = "" | |
| if os.path.exists(log_path): | |
| with open(log_path, 'r', encoding='utf-8') as log_file: | |
| log_content = log_file.read() | |
| raise gr.Error(f"XeLaTeX 编译失败!请检查.log获取详情。\n\n{log_content[-2000:]}") | |
| temp_pdf_path = os.path.join(temp_dir, 'quote.pdf') | |
| output_dir = os.path.dirname(output_pdf_path) | |
| if output_dir: | |
| os.makedirs(output_dir, exist_ok=True) | |
| shutil.move(temp_pdf_path, output_pdf_path) | |
| print(f"PDF生成成功!文件已保存至: {os.path.abspath(output_pdf_path)}") | |
| return output_pdf_path | |
| # ============================================================================== | |
| # Gradio 辅助函数 | |
| # ============================================================================== | |
| TBL_LAYOUT_PRICES = { | |
| "无": (0.023, 0.031, 0.039, 0.051, 0.059, 0.070), | |
| "简排 ": (0.028, 0.037, 0.047, 0.061, 0.071, 0.084), | |
| "常规": (0.034, 0.044, 0.056, 0.073, 0.085, 0.101), | |
| "密集": (0.041, 0.053, 0.067, 0.088, 0.102, 0.121), | |
| "复杂": (0.049, 0.064, 0.080, 0.106, 0.122, 0.145), | |
| "深度": (0.059, 0.077, 0.096, 0.127, 0.146, 0.174), | |
| } | |
| SRT_LAYOUT_PRICES = { | |
| "无": (8, 12, 15, 22, 28, 40), | |
| "简排": (11, 16, 19, 28, 35, 50), | |
| "常规": (14, 20, 24, 35, 43, 62), | |
| "密集": (17, 24, 29, 42, 52, 75), | |
| "复杂": (20, 28, 34, 50, 62, 90), | |
| "深度": (23, 33, 40, 60, 75, 110), | |
| } | |
| COMPLEX_LAYOUT_PRICES = { | |
| "简排": (0.6, 0.7, 0.9, 1.0, 1.3, 1.7), | |
| "常规": (0.9, 1.1, 1.5, 1.9, 2.1, 3.0), | |
| "密集": (1.6, 2.0, 2.4, 3.5, 3.5, 5.0), | |
| "复杂": (2.7, 3.5, 3.9, 6.5, 6.0, 8.7), | |
| "深度": (4.6, 6.2, 6.2, 11.5, 10.0, 15.0), | |
| } | |
| PROJECT_LAYOUT_PRICES = { | |
| "简排": (45, 60, 70, 95, 100, 140), | |
| "常规": (60, 80, 90, 125, 130, 180), | |
| "密集": (75, 100, 115, 155, 165, 220), | |
| "复杂": (90, 125, 140, 190, 200, 265), | |
| "深度": (110, 150, 170, 230, 240, 320), | |
| } | |
| # 定义排版选项列表,用于UI显示 | |
| tbl_layout_options = list(TBL_LAYOUT_PRICES.keys()) | |
| srt_layout_options = list(SRT_LAYOUT_PRICES.keys()) | |
| complex_layout_options = list(COMPLEX_LAYOUT_PRICES.keys()) | |
| project_layout_options = list(PROJECT_LAYOUT_PRICES.keys()) | |
| # 2. 动态更新单价的辅助函数 | |
| def tbl_update_layout_prices(layout_choice): | |
| """根据选择的排版选项,返回对应的六个单价。""" | |
| prices = TBL_LAYOUT_PRICES.get(layout_choice, (0, 0, 0, 0, 0, 0)) | |
| # 返回一个更新列表,用于更新Gradio UI组件 | |
| # Gradio的change函数可以返回一个值列表来更新多个组件 | |
| return [gr.Textbox(value=f"¥ {p}") for p in prices] | |
| def srt_update_layout_prices(layout_choice): | |
| """根据选择的排版选项,返回对应的六个单价。""" | |
| prices = SRT_LAYOUT_PRICES.get(layout_choice, (0, 0, 0, 0, 0, 0)) | |
| # 返回一个更新列表,用于更新Gradio UI组件 | |
| # Gradio的change函数可以返回一个值列表来更新多个组件 | |
| return [gr.Textbox(value=f"¥ {p}") for p in prices] | |
| def complex_update_layout_prices(layout_choice): | |
| """根据选择的排版选项,返回对应的六个单价。""" | |
| prices = COMPLEX_LAYOUT_PRICES.get(layout_choice, (0, 0, 0, 0, 0, 0)) | |
| # 返回一个更新列表,用于更新Gradio UI组件 | |
| # Gradio的change函数可以返回一个值列表来更新多个组件 | |
| return [gr.Textbox(value=f"¥ {p}") for p in prices] | |
| def project_update_layout_prices(layout_choice): | |
| """根据选择的排版选项,返回对应的六个单价。""" | |
| prices = PROJECT_LAYOUT_PRICES.get(layout_choice, (0, 0, 0, 0, 0, 0)) | |
| # 返回一个更新列表,用于更新Gradio UI组件 | |
| # Gradio的change函数可以返回一个值列表来更新多个组件 | |
| return [gr.Textbox(value=f"¥ {p}") for p in prices] | |
| def parse_price(price_str): | |
| """从价格字符串(如 '¥ 80' 或 '80')中提取数值。""" | |
| try: | |
| numbers = re.findall(r"(\d+\.?\d*)", str(price_str).replace(',', '')) | |
| return float(numbers[0]) if numbers else 0.0 | |
| except (ValueError, IndexError): | |
| return 0.0 | |
| def calculate_total(quantity, unit_price_str): | |
| """根据数量和单价字符串计算总价。""" | |
| unit_price = parse_price(unit_price_str) | |
| total = float(quantity or 0) * unit_price | |
| return f"¥ {total:,.2f}" | |
| def update_final_price(service_choice, discount_choice): | |
| """ | |
| 根据选择的服务和折扣,计算并格式化最终应付总额。 | |
| 【已优化】增加了新客优惠逻辑。 | |
| 【已优化】增加了最终价格不得低于 ¥20 的最低消费规则。 | |
| """ | |
| if not service_choice or not isinstance(service_choice, str): | |
| return "¥ 0.00" | |
| # 1. 解析原始价格 | |
| original_price = parse_price(service_choice.split('|')[0]) | |
| # 2. 确定折扣率和优惠描述 | |
| discount_multiplier = 1.0 | |
| applied_discount_desc = "" | |
| # 使用 if/elif 结构确保优惠不叠加 | |
| if "新客优惠" in discount_choice and 50 <= original_price <= 200: | |
| discount_multiplier = 0.9 | |
| applied_discount_desc = "新客优惠 (9折)" | |
| elif "学生优惠" in discount_choice: | |
| discount_multiplier = 0.7 | |
| applied_discount_desc = "学生优惠 (7折)" | |
| elif "译者优惠" in discount_choice: | |
| discount_multiplier = 0.8 | |
| applied_discount_desc = "译者优惠 (8折)" | |
| # 3. 计算应用优惠后的价格 | |
| calculated_price = original_price * discount_multiplier | |
| # 4. 【关键改动】应用最低消费规则(最后一步执行) | |
| min_charge_applied_desc = "" | |
| final_price = calculated_price | |
| # 无论是否有优惠,只要计算出的价格低于20,就强制设为20 | |
| if final_price < 20.0: | |
| final_price = 20.0 | |
| min_charge_applied_desc = "应用最低消费" | |
| # 5. 格式化输出字符串 | |
| service_desc = service_choice.split('|')[1].strip() | |
| # 构建括号内的详细描述 | |
| details = [f"已选: {service_desc}"] | |
| if applied_discount_desc: # 仍然显示应用的优惠,保持透明 | |
| details.append(applied_discount_desc) | |
| if min_charge_applied_desc: | |
| details.append(min_charge_applied_desc) | |
| details_str = ", ".join(details) | |
| return f"¥ {final_price:,.2f} ({details_str})" | |
| # 【新增】根据段落数和字数计算计费数量(1k Token)的函数 | |
| def calculate_quantity_from_details(paragraphs, chars): | |
| """ | |
| 根据段落数和字数计算计费数量 (1k Token) | |
| 公式: (段落数 * 1000 + 文本数 * 5) / 1000 | |
| """ | |
| # 确保输入是数字 | |
| para_count = paragraphs if isinstance(paragraphs, (int, float)) else 0 | |
| char_count = chars if isinstance(chars, (int, float)) else 0 | |
| quantity_val = (para_count * 1000 + char_count * 5) / 1000 | |
| return quantity_val | |
| # ============================================================================== | |
| # Gradio 主界面 | |
| # ============================================================================== | |
| def create_gradio_app(): | |
| default_quote_id = f"Q-{datetime.now().strftime('%Y%m%d')}-工号-今日第几单" | |
| # 【修改】增加新客优惠选项 | |
| discount_options = ["无优惠", "新客优惠 (总价50-200元享9折)", "学生优惠 (7折)", "译者优惠 (8折)"] | |
| with gr.Blocks(theme=gr.themes.Soft(), title="AI翻译报价单生成器") as demo: | |
| gr.Markdown("# 艾写科技 · AI翻译报价单生成器") | |
| gr.Markdown("填写以下信息,实时计算价格并一键生成专业的PDF报价单。") | |
| with gr.Tab("纯文本类"): | |
| with gr.Row(): | |
| # 左侧输入区域 | |
| with gr.Column(scale=2): | |
| with gr.Accordion("第一步:填写基本信息", open=True): | |
| with gr.Row(): | |
| customer_name = gr.Textbox(label="客户名称", value="用户名称或企业名称") | |
| quote_id = gr.Textbox(label="报价单号", value=default_quote_id) | |
| with gr.Row(): | |
| validity_period = gr.Textbox(label="报价有效期", value="14天") | |
| delivery_time = gr.Textbox(label="预计交付时间", value="当日") | |
| with gr.Accordion("第二步:定义项目详情与计费", open=True): | |
| content_type = gr.Textbox(label="翻译内容", value="学术论文(主题名:)") | |
| with gr.Row(): | |
| source_language = gr.Textbox(label="源语言", value="简体中文") | |
| target_language = gr.Textbox(label="目标语言", value="英语 (美式)") | |
| # 【新增】段落数和文本数字段 | |
| with gr.Row(): | |
| paragraph_count = gr.Number(label="段落数", value=10, step=1, minimum=0) | |
| char_count = gr.Number(label="文本数 (字)", value=10000, step=100, minimum=0) | |
| with gr.Row(): | |
| billing_unit = gr.Textbox(label="计费单位", value="1k Token", interactive=False) | |
| # 【修改】计费数量现在是根据上面输入自动计算的,不可手动编辑 | |
| quantity = gr.Number(label="计费数量 (自动计算)", value=15, interactive=False) | |
| with gr.Accordion("第三步:设置各等级服务单价", open=True): | |
| gr.Markdown("请在此处输入各项服务的 **单价** (基于计费单位)。右侧的总价将自动计算。") | |
| with gr.Row(): | |
| std_fast_unit = gr.Textbox(label="标准版-快速 (单价)", value="¥ 0.023") | |
| std_deep_unit = gr.Textbox(label="标准版-深度 (单价)", value="¥ 0.031") | |
| with gr.Row(): | |
| pro_fast_unit = gr.Textbox(label="专业版-快速 (单价)", value="¥ 0.039") | |
| pro_deep_unit = gr.Textbox(label="专业版-深度 (单价)", value="¥ 0.051") | |
| with gr.Row(): | |
| expert_fast_unit = gr.Textbox(label="专家版-快速 (单价)", value="¥ 0.059") | |
| expert_deep_unit = gr.Textbox(label="专家版-深度 (单价)", value="¥ 0.07") | |
| # 右侧预览与生成区域 | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 价格预览与最终选择") | |
| with gr.Group(): | |
| std_fast_total = gr.Textbox(label="标准版-快速 (总价)", interactive=False) | |
| std_deep_total = gr.Textbox(label="标准版-深度 (总价)", interactive=False) | |
| pro_fast_total = gr.Textbox(label="专业版-快速 (总价)", interactive=False) | |
| pro_deep_total = gr.Textbox(label="专业版-深度 (总价)", interactive=False) | |
| expert_fast_total = gr.Textbox(label="专家版-快速 (总价)", interactive=False) | |
| expert_deep_total = gr.Textbox(label="专家版-深度 (总价)", interactive=False) | |
| service_choice_radio = gr.Radio( | |
| label="第四步:请选择服务方案", | |
| interactive=True | |
| ) | |
| discount_selector = gr.Radio( | |
| label="第五步:选择优惠(不同优惠不叠加)", | |
| choices=discount_options, | |
| value="无优惠", | |
| interactive=True | |
| ) | |
| grand_total_display = gr.Textbox(label="应付总额", interactive=False) | |
| generate_button = gr.Button("🚀 生成报价单PDF", variant="primary") | |
| pdf_output = gr.File(label="下载生成的PDF文件") | |
| # ==================== 事件绑定与逻辑 ==================== | |
| unit_prices = [std_fast_unit, std_deep_unit, pro_fast_unit, pro_deep_unit, expert_fast_unit, | |
| expert_deep_unit] | |
| total_prices = [std_fast_total, std_deep_total, pro_fast_total, pro_deep_total, expert_fast_total, | |
| expert_deep_total] | |
| service_names = ["标准版 - 快速", "标准版 - 深度", "专业版 - 快速", "专业版 - 深度", "专家版 - 快速", | |
| "专家版 - 深度"] | |
| # 【修改】核心逻辑:当输入变化时,触发一个统一的更新函数 | |
| # 这个函数会重新计算所有依赖值:计费数量、各项总价、服务选项和最终总额 | |
| def update_all_calculations(paragraphs, chars, discount, *units): | |
| # 1. 计算新的计费数量 | |
| new_quantity = calculate_quantity_from_details(paragraphs, chars) | |
| # 2. 计算所有服务的总价 | |
| new_totals_values = [calculate_total(new_quantity, unit) for unit in units] | |
| # 3. 更新服务选项 | |
| new_radio_choices = [f"{total} | {name}" for total, name in zip(new_totals_values, service_names)] | |
| # 默认选择最后一项(最贵的) | |
| current_choice = new_radio_choices[-1] | |
| # 4. 计算最终总额 | |
| final_price = update_final_price(current_choice, discount) | |
| # 5. 返回所有需要更新的组件的值 | |
| return [ | |
| new_quantity, | |
| *new_totals_values, | |
| gr.Radio(choices=new_radio_choices, value=current_choice), | |
| final_price | |
| ] | |
| # 定义需要触发更新的所有输入组件 | |
| update_triggers = [paragraph_count, char_count, discount_selector] + unit_prices | |
| # 定义所有需要被更新的输出组件 | |
| all_outputs = [quantity] + total_prices + [service_choice_radio, grand_total_display] | |
| # 【修改】绑定事件:段落、字数、单价变化时,更新所有价格 | |
| for trigger in [paragraph_count, char_count] + unit_prices: | |
| trigger.change( | |
| fn=update_all_calculations, | |
| inputs=[paragraph_count, char_count, discount_selector] + unit_prices, | |
| outputs=all_outputs | |
| ) | |
| # 【修改】绑定事件:当服务选项或折扣变化时,只更新最终价格 | |
| def update_final_price_only(choice, discount): | |
| return update_final_price(choice, discount) | |
| service_choice_radio.change( | |
| fn=update_final_price_only, | |
| inputs=[service_choice_radio, discount_selector], | |
| outputs=grand_total_display | |
| ) | |
| discount_selector.change( | |
| fn=update_final_price_only, | |
| inputs=[service_choice_radio, discount_selector], | |
| outputs=grand_total_display | |
| ) | |
| # 【修改】所有输入字段,用于最终生成PDF | |
| all_inputs = [ | |
| customer_name, quote_id, validity_period, content_type, source_language, | |
| target_language, billing_unit, quantity, delivery_time, paragraph_count, char_count, | |
| # <-- 新增字段 | |
| grand_total_display, service_choice_radio, discount_selector | |
| ] + unit_prices + total_prices | |
| generate_button.click(fn=text_handle_form_submission, inputs=all_inputs, outputs=pdf_output) | |
| # 【修改】初始加载逻辑 | |
| demo.load( # 如果在独立脚本中运行,需要这行 | |
| fn=update_all_calculations, | |
| inputs=[paragraph_count, char_count, discount_selector] + unit_prices, | |
| outputs=all_outputs | |
| ) | |
| with gr.Tab("表格类"): | |
| with gr.Row(): | |
| # 左侧输入区域 | |
| with gr.Column(scale=2): | |
| with gr.Accordion("第一步:填写基本信息", open=True): | |
| with gr.Row(): | |
| # 使用 tbl_ 前缀以避免与纯文本类组件冲突 | |
| tbl_customer_name = gr.Textbox(label="客户名称", value="用户名称或企业名称") | |
| tbl_quote_id = gr.Textbox(label="报价单号", value=default_quote_id) | |
| with gr.Row(): | |
| tbl_validity_period = gr.Textbox(label="报价有效期", value="14天") | |
| tbl_delivery_time = gr.Textbox(label="预计交付时间", value="5个工作日") | |
| with gr.Accordion("第二步:定义项目详情与计费", open=True): | |
| tbl_content_type = gr.Textbox(label="翻译内容", value="表(主题名:)") | |
| with gr.Row(): | |
| tbl_source_language = gr.Textbox(label="源语言", value="简体中文") | |
| tbl_target_language = gr.Textbox(label="目标语言", value="英语 (美式)") | |
| # 【新增】排版选项 | |
| tbl_layout_selector = gr.Radio( | |
| label="选择排版复杂度", | |
| choices=tbl_layout_options, | |
| value=tbl_layout_options[0] # 默认选择“无” | |
| ) | |
| with gr.Row(): | |
| tbl_billing_unit = gr.Textbox(label="计费单位", value="1k Token") | |
| tbl_quantity = gr.Number(label="计费数量", value=15) | |
| with gr.Accordion("第三步:各等级服务单价 (根据排版选项自动更新)", open=True): | |
| gr.Markdown("单价会根据您在 **第二步** 中选择的 **排版复杂度** 自动填充。") | |
| with gr.Row(): | |
| tbl_std_fast_unit = gr.Textbox(label="标准版-快速 (单价)", interactive=False) | |
| tbl_std_deep_unit = gr.Textbox(label="标准版-深度 (单价)", interactive=False) | |
| with gr.Row(): | |
| tbl_pro_fast_unit = gr.Textbox(label="专业版-快速 (单价)", interactive=False) | |
| tbl_pro_deep_unit = gr.Textbox(label="专业版-深度 (单价)", interactive=False) | |
| with gr.Row(): | |
| tbl_expert_fast_unit = gr.Textbox(label="专家版-快速 (单价)", interactive=False) | |
| tbl_expert_deep_unit = gr.Textbox(label="专家版-深度 (单价)", interactive=False) | |
| # 右侧预览与生成区域 | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 价格预览与最终选择") | |
| with gr.Group(): | |
| tbl_std_fast_total = gr.Textbox(label="标准版-快速 (总价)", interactive=False) | |
| tbl_std_deep_total = gr.Textbox(label="标准版-深度 (总价)", interactive=False) | |
| tbl_pro_fast_total = gr.Textbox(label="专业版-快速 (总价)", interactive=False) | |
| tbl_pro_deep_total = gr.Textbox(label="专业版-深度 (总价)", interactive=False) | |
| tbl_expert_fast_total = gr.Textbox(label="专家版-快速 (总价)", interactive=False) | |
| tbl_expert_deep_total = gr.Textbox(label="专家版-深度 (总价)", interactive=False) | |
| tbl_service_choice_radio = gr.Radio(label="第四步:请选择服务方案", interactive=True) | |
| tbl_discount_selector = gr.Radio( | |
| label="第五步:选择优惠(不同优惠不叠加)", | |
| choices=discount_options, | |
| value="无优惠", | |
| interactive=True | |
| ) | |
| tbl_grand_total_display = gr.Textbox(label="应付总额", interactive=False) | |
| # 【新增】添加一个隐藏的文本框,用于在事件之间安全地传递状态 | |
| tbl_selected_value_hidden = gr.Textbox(visible=False) | |
| tbl_generate_button = gr.Button("🚀 生成表格类报价单PDF", variant="primary") | |
| tbl_pdf_output = gr.File(label="下载生成的PDF文件") | |
| # ==================== 表格类:事件绑定与逻辑 ==================== | |
| tbl_unit_prices = [tbl_std_fast_unit, tbl_std_deep_unit, tbl_pro_fast_unit, tbl_pro_deep_unit, | |
| tbl_expert_fast_unit, tbl_expert_deep_unit] | |
| tbl_total_prices = [tbl_std_fast_total, tbl_std_deep_total, tbl_pro_fast_total, tbl_pro_deep_total, | |
| tbl_expert_fast_total, tbl_expert_deep_total] | |
| # service_names 可以在两个选项卡中复用 | |
| # service_names = ["标准版 - 快速", "标准版 - 深度", "专业版 - 快速", "专业版 - 深度", "专家版 - 快速", "专家版 - 深度"] | |
| # --- 核心联动逻辑 --- | |
| # 1. 当排版选项改变时,调用函数更新六个单价输入框 | |
| tbl_layout_selector.change( | |
| fn=tbl_update_layout_prices, | |
| inputs=tbl_layout_selector, | |
| outputs=tbl_unit_prices | |
| ) | |
| # 2. 当数量或任一单价改变时,重新计算对应的总价 | |
| # (注意:单价的改变是由上一步触发的,这样就形成了事件链) | |
| for unit_input, total_output in zip(tbl_unit_prices, tbl_total_prices): | |
| tbl_quantity.change(calculate_total, inputs=[tbl_quantity, unit_input], outputs=total_output) | |
| unit_input.change(calculate_total, inputs=[tbl_quantity, unit_input], outputs=total_output) | |
| # 3. 当任一总价改变时,更新服务方案选项,并接着更新最终应付总额 | |
| def tbl_update_radio_choices(*totals): | |
| choices = [f"{total} | {name}" for total, name in zip(totals, service_names)] | |
| # 默认选择价格最高的方案 | |
| new_value = choices[-1] if choices else None | |
| # 【修改】返回两个值:更新后的Radio组件 和 它最新的值 | |
| return gr.Radio(choices=choices, value=new_value), new_value | |
| for total_output in tbl_total_prices: | |
| total_output.change( | |
| fn=tbl_update_radio_choices, | |
| inputs=tbl_total_prices, | |
| # 【修改】函数的两个返回值分别更新Radio组件和隐藏文本框 | |
| outputs=[tbl_service_choice_radio, tbl_selected_value_hidden] | |
| ).then( | |
| fn=update_final_price, | |
| # 【修改】关键!让 update_final_price 从隐藏文本框获取稳定、正确的值 | |
| inputs=[tbl_selected_value_hidden, tbl_discount_selector], | |
| outputs=tbl_grand_total_display | |
| ) | |
| # 4. 当服务方案或折扣选项直接改变时,也更新最终应付总额 | |
| tbl_service_choice_radio.change(update_final_price, | |
| inputs=[tbl_service_choice_radio, tbl_discount_selector], | |
| outputs=tbl_grand_total_display) | |
| tbl_discount_selector.change(update_final_price, inputs=[tbl_service_choice_radio, tbl_discount_selector], | |
| outputs=tbl_grand_total_display) | |
| # 5. 定义生成PDF的点击事件 | |
| tbl_all_inputs = [ | |
| tbl_customer_name, tbl_quote_id, tbl_validity_period, tbl_content_type, | |
| tbl_source_language, | |
| tbl_target_language, tbl_billing_unit, tbl_quantity, tbl_delivery_time, | |
| tbl_grand_total_display, tbl_service_choice_radio, tbl_discount_selector, | |
| tbl_layout_selector # 【新增】将排版选项也作为输入 | |
| ] + tbl_unit_prices + tbl_total_prices | |
| # 假设你为表格创建了一个新的处理函数 table_handle_form_submission 和一个新的模板 table.tex | |
| # generate_button.click(fn=table_handle_form_submission, inputs=all_inputs, outputs=pdf_output) | |
| # 绑定点击事件到新的包装函数 | |
| tbl_generate_button.click(fn=table_handle_form_submission, inputs=tbl_all_inputs, outputs=tbl_pdf_output) | |
| # 6. 定义此选项卡的初始加载函数 | |
| def tbl_initial_load(q, layout_choice): | |
| # 1. 根据默认排版获取单价 | |
| unit_prices_list = [f"¥ {p}" for p in TBL_LAYOUT_PRICES.get(layout_choice, (0,) * 6)] | |
| # 2. 计算所有总价 | |
| totals = [calculate_total(q, unit) for unit in unit_prices_list] | |
| # 3. 生成Radio选项 | |
| radio_choices = [f"{total} | {name}" for total, name in zip(totals, service_names)] | |
| # 4. 计算最终价格 | |
| initial_final_price = update_final_price(radio_choices[-1] if radio_choices else None, | |
| discount_options[0]) | |
| # 返回所有需要更新的组件值 | |
| return [ | |
| *unit_prices_list, | |
| *totals, | |
| gr.Radio(choices=radio_choices, value=radio_choices[-1] if radio_choices else None), | |
| initial_final_price | |
| ] | |
| demo.load( | |
| fn=tbl_initial_load, | |
| inputs=[tbl_quantity, tbl_layout_selector], | |
| outputs=tbl_unit_prices + tbl_total_prices + [tbl_service_choice_radio, tbl_grand_total_display] | |
| ) | |
| with gr.Tab("纯净PDF"): | |
| with gr.Row(): | |
| # 左侧输入区域 | |
| with gr.Column(scale=2): | |
| with gr.Accordion("第一步:填写基本信息", open=True): | |
| with gr.Row(): | |
| customer_name = gr.Textbox(label="客户名称", value="用户名称或企业名称") | |
| quote_id = gr.Textbox(label="报价单号", value=default_quote_id) | |
| with gr.Row(): | |
| validity_period = gr.Textbox(label="报价有效期", value="14天") | |
| delivery_time = gr.Textbox(label="预计交付时间", value="当日") | |
| with gr.Accordion("第二步:定义项目详情与计费", open=True): | |
| content_type = gr.Textbox(label="翻译内容", value="学术论文(主题名:)") | |
| with gr.Row(): | |
| source_language = gr.Textbox(label="源语言", value="简体中文") | |
| target_language = gr.Textbox(label="目标语言", value="英语 (美式)") | |
| with gr.Row(): | |
| billing_unit = gr.Textbox(label="计费单位", value="页") | |
| quantity = gr.Number(label="计费数量", value=15) | |
| with gr.Accordion("第三步:设置各等级服务单价", open=True): | |
| gr.Markdown("请在此处输入各项服务的 **单价** (基于计费单位)。右侧的总价将自动计算。") | |
| with gr.Row(): | |
| std_fast_unit = gr.Textbox(label="标准版-快速 (单价)", value="¥ 0.4") | |
| std_deep_unit = gr.Textbox(label="标准版-深度 (单价)", value="¥ 0.6") | |
| with gr.Row(): | |
| pro_fast_unit = gr.Textbox(label="专业版-快速 (单价)", value="¥ 0.85") | |
| pro_deep_unit = gr.Textbox(label="专业版-深度 (单价)", value="¥ 0.9") | |
| with gr.Row(): | |
| expert_fast_unit = gr.Textbox(label="专家版-快速 (单价)", value="¥ 1.2") | |
| expert_deep_unit = gr.Textbox(label="专家版-深度 (单价)", value="¥ 1.5") | |
| # 右侧预览与生成区域 | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 价格预览与最终选择") | |
| with gr.Group(): | |
| std_fast_total = gr.Textbox(label="标准版-快速 (总价)", interactive=False) | |
| std_deep_total = gr.Textbox(label="标准版-深度 (总价)", interactive=False) | |
| pro_fast_total = gr.Textbox(label="专业版-快速 (总价)", interactive=False) | |
| pro_deep_total = gr.Textbox(label="专业版-深度 (总价)", interactive=False) | |
| expert_fast_total = gr.Textbox(label="专家版-快速 (总价)", interactive=False) | |
| expert_deep_total = gr.Textbox(label="专家版-深度 (总价)", interactive=False) | |
| service_choice_radio = gr.Radio( | |
| label="第四步:请选择服务方案", | |
| interactive=True | |
| ) | |
| discount_selector = gr.Radio( | |
| label="第五步:选择优惠(不同优惠不叠加)", # 【优化】提示语 | |
| choices=discount_options, | |
| value="无优惠", | |
| interactive=True | |
| ) | |
| grand_total_display = gr.Textbox(label="应付总额", interactive=False) | |
| generate_button = gr.Button("🚀 生成报价单PDF", variant="primary") | |
| pdf_output = gr.File(label="下载生成的PDF文件") | |
| # ==================== 事件绑定与逻辑 ==================== | |
| unit_prices = [std_fast_unit, std_deep_unit, pro_fast_unit, pro_deep_unit, expert_fast_unit, | |
| expert_deep_unit] | |
| total_prices = [std_fast_total, std_deep_total, pro_fast_total, pro_deep_total, expert_fast_total, | |
| expert_deep_total] | |
| service_names = ["标准版 - 快速", "标准版 - 深度", "专业版 - 快速", "专业版 - 深度", "专家版 - 快速", | |
| "专家版 - 深度"] | |
| for unit_input, total_output in zip(unit_prices, total_prices): | |
| quantity.change(calculate_total, inputs=[quantity, unit_input], outputs=total_output) | |
| unit_input.change(calculate_total, inputs=[quantity, unit_input], outputs=total_output) | |
| def update_radio_choices(*totals): | |
| choices = [f"{total} | {name}" for total, name in zip(totals, service_names)] | |
| return gr.Radio(choices=choices, value=choices[-1]) | |
| for total_output in total_prices: | |
| # 【优化】当服务价格变化时,也触发最终价格的更新 | |
| total_output.change(update_radio_choices, inputs=total_prices, outputs=service_choice_radio).then( | |
| fn=update_final_price, | |
| inputs=[service_choice_radio, discount_selector], | |
| outputs=grand_total_display | |
| ) | |
| service_choice_radio.change(update_final_price, inputs=[service_choice_radio, discount_selector], | |
| outputs=grand_total_display) | |
| discount_selector.change(update_final_price, inputs=[service_choice_radio, discount_selector], | |
| outputs=grand_total_display) | |
| all_inputs = [ | |
| customer_name, quote_id, validity_period, content_type, source_language, | |
| target_language, billing_unit, quantity, delivery_time, | |
| grand_total_display, service_choice_radio, discount_selector | |
| ] + unit_prices + total_prices | |
| generate_button.click(fn=chun_pdf_handle_form_submission, inputs=all_inputs, outputs=pdf_output) | |
| def initial_load(q, *units): | |
| totals = [calculate_total(q, unit) for unit in units] | |
| radio_choices = [f"{total} | {name}" for total, name in zip(totals, service_names)] | |
| # 【修改】确保初始加载时也使用最新的优惠选项 | |
| initial_final_price = update_final_price(radio_choices[-1], discount_options[0]) | |
| return [ | |
| *totals, | |
| gr.Radio(choices=radio_choices, value=radio_choices[-1]), | |
| initial_final_price | |
| ] | |
| demo.load( | |
| fn=initial_load, | |
| inputs=[quantity] + unit_prices, | |
| outputs=total_prices + [service_choice_radio, grand_total_display] | |
| ) | |
| with gr.Tab("复杂文档"): | |
| with gr.Row(): | |
| # 左侧输入区域 | |
| with gr.Column(scale=2): | |
| with gr.Accordion("第一步:填写基本信息", open=True): | |
| with gr.Row(): | |
| # 使用 complex_ 前缀以避免与纯文本类组件冲突 | |
| complex_customer_name = gr.Textbox(label="客户名称", value="用户名称或企业名称") | |
| complex_quote_id = gr.Textbox(label="报价单号", value=default_quote_id) | |
| with gr.Row(): | |
| complex_validity_period = gr.Textbox(label="报价有效期", value="14天") | |
| complex_delivery_time = gr.Textbox(label="预计交付时间", value="5个工作日") | |
| with gr.Accordion("第二步:定义项目详情与计费", open=True): | |
| complex_content_type = gr.Textbox(label="翻译内容", value="") | |
| with gr.Row(): | |
| complex_source_language = gr.Textbox(label="源语言", value="简体中文") | |
| complex_target_language = gr.Textbox(label="目标语言", value="英语 (美式)") | |
| # 【新增】排版选项 | |
| complex_layout_selector = gr.Radio( | |
| label="选择排版复杂度", | |
| choices=complex_layout_options, # 假设 tbl_layout_options 是一个外部定义的变量 | |
| value=complex_layout_options[0] # 默认选择“无” | |
| ) | |
| with gr.Row(): | |
| complex_billing_unit = gr.Textbox(label="计费单位", value="页") | |
| complex_quantity = gr.Number(label="计费数量", value=15) | |
| with gr.Accordion("第三步:各等级服务单价 (根据排版选项自动更新)", open=True): | |
| gr.Markdown("单价会根据您在 **第二步** 中选择的 **排版复杂度** 自动填充。") | |
| with gr.Row(): | |
| complex_std_fast_unit = gr.Textbox(label="标准版-快速 (单价)", interactive=False) | |
| complex_std_deep_unit = gr.Textbox(label="标准版-深度 (单价)", interactive=False) | |
| with gr.Row(): | |
| complex_pro_fast_unit = gr.Textbox(label="专业版-快速 (单价)", interactive=False) | |
| complex_pro_deep_unit = gr.Textbox(label="专业版-深度 (单价)", interactive=False) | |
| with gr.Row(): | |
| complex_expert_fast_unit = gr.Textbox(label="专家版-快速 (单价)", interactive=False) | |
| complex_expert_deep_unit = gr.Textbox(label="专家版-深度 (单价)", interactive=False) | |
| # 右侧预览与生成区域 | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 价格预览与最终选择") | |
| with gr.Group(): | |
| complex_std_fast_total = gr.Textbox(label="标准版-快速 (总价)", interactive=False) | |
| complex_std_deep_total = gr.Textbox(label="标准版-深度 (总价)", interactive=False) | |
| complex_pro_fast_total = gr.Textbox(label="专业版-快速 (总价)", interactive=False) | |
| complex_pro_deep_total = gr.Textbox(label="专业版-深度 (总价)", interactive=False) | |
| complex_expert_fast_total = gr.Textbox(label="专家版-快速 (总价)", interactive=False) | |
| complex_expert_deep_total = gr.Textbox(label="专家版-深度 (总价)", interactive=False) | |
| complex_service_choice_radio = gr.Radio(label="第四步:请选择服务方案", interactive=True) | |
| complex_discount_selector = gr.Radio( | |
| label="第五步:选择优惠(不同优惠不叠加)", | |
| choices=discount_options, | |
| value="无优惠", | |
| interactive=True | |
| ) | |
| complex_grand_total_display = gr.Textbox(label="应付总额", interactive=False) | |
| # 【新增】添加一个隐藏的文本框,用于在事件之间安全地传递状态 | |
| complex_selected_value_hidden = gr.Textbox(visible=False) | |
| complex_generate_button = gr.Button("🚀 生成复杂文档报价单PDF", variant="primary") | |
| complex_pdf_output = gr.File(label="下载生成的PDF文件") | |
| # ==================== 复杂文档类:事件绑定与逻辑 ==================== | |
| complex_unit_prices = [complex_std_fast_unit, complex_std_deep_unit, complex_pro_fast_unit, | |
| complex_pro_deep_unit, | |
| complex_expert_fast_unit, complex_expert_deep_unit] | |
| complex_total_prices = [complex_std_fast_total, complex_std_deep_total, complex_pro_fast_total, | |
| complex_pro_deep_total, | |
| complex_expert_fast_total, complex_expert_deep_total] | |
| # service_names 可以在两个选项卡中复用 | |
| # service_names = ["标准版 - 快速", "标准版 - 深度", "专业版 - 快速", "专业版 - 深度", "专家版 - 快速", "专家版 - 深度"] | |
| # --- 核心联动逻辑 --- | |
| # 1. 当排版选项改变时,调用函数更新六个单价输入框 | |
| complex_layout_selector.change( | |
| fn=complex_update_layout_prices, # 假设函数 tbl_update_layout_prices 也重命名为 complex_update_layout_prices | |
| inputs=complex_layout_selector, | |
| outputs=complex_unit_prices | |
| ) | |
| # 2. 当数量或任一单价改变时,重新计算对应的总价 | |
| # (注意:单价的改变是由上一步触发的,这样就形成了事件链) | |
| for unit_input, total_output in zip(complex_unit_prices, complex_total_prices): | |
| complex_quantity.change(calculate_total, inputs=[complex_quantity, unit_input], outputs=total_output) | |
| unit_input.change(calculate_total, inputs=[complex_quantity, unit_input], outputs=total_output) | |
| # 3. 当任一总价改变时,更新服务方案选项,并接着更新最终应付总额 | |
| def complex_update_radio_choices(*totals): | |
| choices = [f"{total} | {name}" for total, name in zip(totals, service_names)] | |
| # 默认选择价格最高的方案 | |
| new_value = choices[-1] if choices else None | |
| # 【修改】返回两个值:更新后的Radio组件 和 它最新的值 | |
| return gr.Radio(choices=choices, value=new_value), new_value | |
| for total_output in complex_total_prices: | |
| total_output.change( | |
| fn=complex_update_radio_choices, | |
| inputs=complex_total_prices, | |
| # 【修改】函数的两个返回值分别更新Radio组件和隐藏文本框 | |
| outputs=[complex_service_choice_radio, complex_selected_value_hidden] | |
| ).then( | |
| fn=update_final_price, | |
| # 【修改】关键!让 update_final_price 从隐藏文本框获取稳定、正确的值 | |
| inputs=[complex_selected_value_hidden, complex_discount_selector], | |
| outputs=complex_grand_total_display | |
| ) | |
| # 4. 当服务方案或折扣选项直接改变时,也更新最终应付总额 | |
| complex_service_choice_radio.change(update_final_price, | |
| inputs=[complex_service_choice_radio, complex_discount_selector], | |
| outputs=complex_grand_total_display) | |
| complex_discount_selector.change(update_final_price, | |
| inputs=[complex_service_choice_radio, complex_discount_selector], | |
| outputs=complex_grand_total_display) | |
| # 5. 定义生成PDF的点击事件 | |
| complex_all_inputs = [ | |
| complex_customer_name, complex_quote_id, complex_validity_period, | |
| complex_content_type, | |
| complex_source_language, | |
| complex_target_language, complex_billing_unit, complex_quantity, | |
| complex_delivery_time, | |
| complex_grand_total_display, complex_service_choice_radio, | |
| complex_discount_selector, | |
| complex_layout_selector # 【新增】将排版选项也作为输入 | |
| ] + complex_unit_prices + complex_total_prices | |
| # 假设你为复杂文档创建了一个新的处理函数 complex_handle_form_submission 和一个新的模板 complex.tex | |
| # generate_button.click(fn=complex_handle_form_submission, inputs=all_inputs, outputs=pdf_output) | |
| # 绑定点击事件到新的包装函数 | |
| complex_generate_button.click(fn=complex_handle_form_submission, inputs=complex_all_inputs, | |
| outputs=complex_pdf_output) # 假设函数 table_handle_form_submission 也重命名 | |
| # 6. 定义此选项卡的初始加载函数 | |
| def complex_initial_load(q, layout_choice): | |
| # 1. 根据默认排版获取单价 | |
| # 假设常量 TBL_LAYOUT_PRICES 也重命名为 COMPLEX_LAYOUT_PRICES | |
| unit_prices_list = [f"¥ {p}" for p in COMPLEX_LAYOUT_PRICES.get(layout_choice, (0,) * 6)] | |
| # 2. 计算所有总价 | |
| totals = [calculate_total(q, unit) for unit in unit_prices_list] | |
| # 3. 生成Radio选项 | |
| radio_choices = [f"{total} | {name}" for total, name in zip(totals, service_names)] | |
| # 4. 计算最终价格 | |
| initial_final_price = update_final_price(radio_choices[-1] if radio_choices else None, | |
| discount_options[0]) | |
| # 返回所有需要更新的组件值 | |
| return [ | |
| *unit_prices_list, | |
| *totals, | |
| gr.Radio(choices=radio_choices, value=radio_choices[-1] if radio_choices else None), | |
| initial_final_price | |
| ] | |
| demo.load( | |
| fn=complex_initial_load, | |
| inputs=[complex_quantity, complex_layout_selector], | |
| outputs=complex_unit_prices + complex_total_prices + [complex_service_choice_radio, | |
| complex_grand_total_display] | |
| ) | |
| with gr.Tab("工程图"): | |
| with gr.Row(): | |
| # 左侧输入区域 | |
| with gr.Column(scale=2): | |
| with gr.Accordion("第一步:填写基本信息", open=True): | |
| with gr.Row(): | |
| # 使用 project_ 前缀以避免与其他组件冲突 | |
| project_customer_name = gr.Textbox(label="客户名称", value="用户名称或企业名称") | |
| project_quote_id = gr.Textbox(label="报价单号", value=default_quote_id) | |
| with gr.Row(): | |
| project_validity_period = gr.Textbox(label="报价有效期", value="14天") | |
| project_delivery_time = gr.Textbox(label="预计交付时间", value="5个工作日") | |
| with gr.Accordion("第二步:定义项目详情与计费", open=True): | |
| project_content_type = gr.Textbox(label="翻译内容", value="工程图(主题名:)") | |
| with gr.Row(): | |
| project_source_language = gr.Textbox(label="源语言", value="简体中文") | |
| project_target_language = gr.Textbox(label="目标语言", value="英语 (美式)") | |
| # 【新增】排版选项 | |
| project_layout_selector = gr.Radio( | |
| label="选择排版复杂度", | |
| # 假设 tbl_layout_options 也重命名为 project_layout_options | |
| choices=project_layout_options, | |
| value=project_layout_options[0] # 默认选择“无” | |
| ) | |
| with gr.Row(): | |
| project_billing_unit = gr.Textbox(label="计费单位", value="页") | |
| project_quantity = gr.Number(label="计费数量", value=15) | |
| with gr.Accordion("第三步:各等级服务单价 (根据排版选项自动更新)", open=True): | |
| gr.Markdown("单价会根据您在 **第二步** 中选择的 **排版复杂度** 自动填充。") | |
| with gr.Row(): | |
| project_std_fast_unit = gr.Textbox(label="标准版-快速 (单价)", interactive=False) | |
| project_std_deep_unit = gr.Textbox(label="标准版-深度 (单价)", interactive=False) | |
| with gr.Row(): | |
| project_pro_fast_unit = gr.Textbox(label="专业版-快速 (单价)", interactive=False) | |
| project_pro_deep_unit = gr.Textbox(label="专业版-深度 (单价)", interactive=False) | |
| with gr.Row(): | |
| project_expert_fast_unit = gr.Textbox(label="专家版-快速 (单价)", interactive=False) | |
| project_expert_deep_unit = gr.Textbox(label="专家版-深度 (单价)", interactive=False) | |
| # 右侧预览与生成区域 | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 价格预览与最终选择") | |
| with gr.Group(): | |
| project_std_fast_total = gr.Textbox(label="标准版-快速 (总价)", interactive=False) | |
| project_std_deep_total = gr.Textbox(label="标准版-深度 (总价)", interactive=False) | |
| project_pro_fast_total = gr.Textbox(label="专业版-快速 (总价)", interactive=False) | |
| project_pro_deep_total = gr.Textbox(label="专业版-深度 (总价)", interactive=False) | |
| project_expert_fast_total = gr.Textbox(label="专家版-快速 (总价)", interactive=False) | |
| project_expert_deep_total = gr.Textbox(label="专家版-深度 (总价)", interactive=False) | |
| project_service_choice_radio = gr.Radio(label="第四步:请选择服务方案", interactive=True) | |
| project_discount_selector = gr.Radio( | |
| label="第五步:选择优惠(不同优惠不叠加)", | |
| choices=discount_options, | |
| value="无优惠", | |
| interactive=True | |
| ) | |
| project_grand_total_display = gr.Textbox(label="应付总额", interactive=False) | |
| # 【新增】添加一个隐藏的文本框,用于在事件之间安全地传递状态 | |
| project_selected_value_hidden = gr.Textbox(visible=False) | |
| project_generate_button = gr.Button("🚀 生成工程图类报价单PDF", variant="primary") | |
| project_pdf_output = gr.File(label="下载生成的PDF文件") | |
| # ==================== 工程图类:事件绑定与逻辑 ==================== | |
| project_unit_prices = [project_std_fast_unit, project_std_deep_unit, project_pro_fast_unit, | |
| project_pro_deep_unit, | |
| project_expert_fast_unit, project_expert_deep_unit] | |
| project_total_prices = [project_std_fast_total, project_std_deep_total, project_pro_fast_total, | |
| project_pro_deep_total, | |
| project_expert_fast_total, project_expert_deep_total] | |
| # service_names 可以在两个选项卡中复用 | |
| # service_names = ["标准版 - 快速", "标准版 - 深度", "专业版 - 快速", "专业版 - 深度", "专家版 - 快速", "专家版 - 深度"] | |
| # --- 核心联动逻辑 --- | |
| # 1. 当排版选项改变时,调用函数更新六个单价输入框 (假设函数也已重命名) | |
| project_layout_selector.change( | |
| fn=project_update_layout_prices, # 原 tbl_update_layout_prices | |
| inputs=project_layout_selector, | |
| outputs=project_unit_prices | |
| ) | |
| # 2. 当数量或任一单价改变时,重新计算对应的总价 | |
| # (注意:单价的改变是由上一步触发的,这样就形成了事件链) | |
| for unit_input, total_output in zip(project_unit_prices, project_total_prices): | |
| project_quantity.change(calculate_total, inputs=[project_quantity, unit_input], outputs=total_output) | |
| unit_input.change(calculate_total, inputs=[project_quantity, unit_input], outputs=total_output) | |
| # 3. 当任一总价改变时,更新服务方案选项,并接着更新最终应付总额 | |
| def project_update_radio_choices(*totals): # 原 tbl_update_radio_choices | |
| choices = [f"{total} | {name}" for total, name in zip(totals, service_names)] | |
| # 默认选择价格最高的方案 | |
| new_value = choices[-1] if choices else None | |
| # 【修改】返回两个值:更新后的Radio组件 和 它最新的值 | |
| return gr.Radio(choices=choices, value=new_value), new_value | |
| for total_output in project_total_prices: | |
| total_output.change( | |
| fn=project_update_radio_choices, | |
| inputs=project_total_prices, | |
| # 【修改】函数的两个返回值分别更新Radio组件和隐藏文本框 | |
| outputs=[project_service_choice_radio, project_selected_value_hidden] | |
| ).then( | |
| fn=update_final_price, | |
| # 【修改】关键!让 update_final_price 从隐藏文本框获取稳定、正确的值 | |
| inputs=[project_selected_value_hidden, project_discount_selector], | |
| outputs=project_grand_total_display | |
| ) | |
| # 4. 当服务方案或折扣选项直接改变时,也更新最终应付总额 | |
| project_service_choice_radio.change(update_final_price, | |
| inputs=[project_service_choice_radio, project_discount_selector], | |
| outputs=project_grand_total_display) | |
| project_discount_selector.change(update_final_price, | |
| inputs=[project_service_choice_radio, project_discount_selector], | |
| outputs=project_grand_total_display) | |
| # 5. 定义生成PDF的点击事件 | |
| project_all_inputs = [ | |
| project_customer_name, project_quote_id, project_validity_period, | |
| project_content_type, | |
| project_source_language, | |
| project_target_language, project_billing_unit, project_quantity, | |
| project_delivery_time, | |
| project_grand_total_display, project_service_choice_radio, | |
| project_discount_selector, | |
| project_layout_selector # 【新增】将排版选项也作为输入 | |
| ] + project_unit_prices + project_total_prices | |
| # 假设你为工程图创建了一个新的处理函数 project_handle_form_submission 和一个新的模板 project.tex | |
| # 绑定点击事件到新的包装函数 | |
| project_generate_button.click(fn=project_handle_form_submission, inputs=project_all_inputs, | |
| outputs=project_pdf_output) # 原 table_handle_form_submission | |
| # 6. 定义此选项卡的初始加载函数 | |
| def project_initial_load(q, layout_choice): # 原 tbl_initial_load | |
| # 1. 根据默认排版获取单价 (假设 TBL_LAYOUT_PRICES 也重命名) | |
| unit_prices_list = [f"¥ {p}" for p in PROJECT_LAYOUT_PRICES.get(layout_choice, (0,) * 6)] | |
| # 2. 计算所有总价 | |
| totals = [calculate_total(q, unit) for unit in unit_prices_list] | |
| # 3. 生成Radio选项 | |
| radio_choices = [f"{total} | {name}" for total, name in zip(totals, service_names)] | |
| # 4. 计算最终价格 | |
| initial_final_price = update_final_price(radio_choices[-1] if radio_choices else None, | |
| discount_options[0]) | |
| # 返回所有需要更新的组件值 | |
| return [ | |
| *unit_prices_list, | |
| *totals, | |
| gr.Radio(choices=radio_choices, value=radio_choices[-1] if radio_choices else None), | |
| initial_final_price | |
| ] | |
| demo.load( | |
| fn=project_initial_load, | |
| inputs=[project_quantity, project_layout_selector], | |
| outputs=project_unit_prices + project_total_prices + [project_service_choice_radio, | |
| project_grand_total_display] | |
| ) | |
| with gr.Tab("字幕文件"): | |
| with gr.Row(): | |
| # 左侧输入区域 | |
| with gr.Column(scale=2): | |
| with gr.Accordion("第一步:填写基本信息", open=True): | |
| with gr.Row(): | |
| # 使用 tbl_ 前缀以避免与纯文本类组件冲突 | |
| srt_customer_name = gr.Textbox(label="客户名称", value="用户名称或企业名称") | |
| srt_quote_id = gr.Textbox(label="报价单号", value=default_quote_id) | |
| with gr.Row(): | |
| srt_validity_period = gr.Textbox(label="报价有效期", value="14天") | |
| srt_delivery_time = gr.Textbox(label="预计交付时间", value="2个工作日") | |
| with gr.Accordion("第二步:定义项目详情与计费", open=True): | |
| srt_content_type = gr.Textbox(label="翻译内容", value="字幕(主题名:)") | |
| with gr.Row(): | |
| srt_source_language = gr.Textbox(label="源语言", value="简体中文") | |
| srt_target_language = gr.Textbox(label="目标语言", value="英语 (美式)") | |
| # 【新增】排版选项 | |
| srt_layout_selector = gr.Radio( | |
| label="选择排版复杂度", | |
| choices=srt_layout_options, | |
| value=srt_layout_options[0] # 默认选择“无” | |
| ) | |
| with gr.Row(): | |
| srt_billing_unit = gr.Textbox(label="计费单位", value="分钟") | |
| srt_quantity = gr.Number(label="计费数量", value=15) | |
| with gr.Accordion("第三步:各等级服务单价 (根据排版选项自动更新)", open=True): | |
| gr.Markdown("单价会根据您在 **第二步** 中选择的 **排版复杂度** 自动填充。") | |
| with gr.Row(): | |
| srt_std_fast_unit = gr.Textbox(label="标准版-快速 (单价)", interactive=False) | |
| srt_std_deep_unit = gr.Textbox(label="标准版-深度 (单价)", interactive=False) | |
| with gr.Row(): | |
| srt_pro_fast_unit = gr.Textbox(label="专业版-快速 (单价)", interactive=False) | |
| srt_pro_deep_unit = gr.Textbox(label="专业版-深度 (单价)", interactive=False) | |
| with gr.Row(): | |
| srt_expert_fast_unit = gr.Textbox(label="专家版-快速 (单价)", interactive=False) | |
| srt_expert_deep_unit = gr.Textbox(label="专家版-深度 (单价)", interactive=False) | |
| # 右侧预览与生成区域 | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 价格预览与最终选择") | |
| with gr.Group(): | |
| srt_std_fast_total = gr.Textbox(label="标准版-快速 (总价)", interactive=False) | |
| srt_std_deep_total = gr.Textbox(label="标准版-深度 (总价)", interactive=False) | |
| srt_pro_fast_total = gr.Textbox(label="专业版-快速 (总价)", interactive=False) | |
| srt_pro_deep_total = gr.Textbox(label="专业版-深度 (总价)", interactive=False) | |
| srt_expert_fast_total = gr.Textbox(label="专家版-快速 (总价)", interactive=False) | |
| srt_expert_deep_total = gr.Textbox(label="专家版-深度 (总价)", interactive=False) | |
| srt_service_choice_radio = gr.Radio(label="第四步:请选择服务方案", interactive=True) | |
| srt_discount_selector = gr.Radio( | |
| label="第五步:选择优惠(不同优惠不叠加)", | |
| choices=discount_options, | |
| value="无优惠", | |
| interactive=True | |
| ) | |
| srt_grand_total_display = gr.Textbox(label="应付总额", interactive=False) | |
| # 【新增】添加一个隐藏的文本框,用于在事件之间安全地传递状态 | |
| srt_selected_value_hidden = gr.Textbox(visible=False) | |
| srt_generate_button = gr.Button("🚀 生成表格类报价单PDF", variant="primary") | |
| srt_pdf_output = gr.File(label="下载生成的PDF文件") | |
| # ==================== 表格类:事件绑定与逻辑 ==================== | |
| srt_unit_prices = [srt_std_fast_unit, srt_std_deep_unit, srt_pro_fast_unit, srt_pro_deep_unit, | |
| srt_expert_fast_unit, srt_expert_deep_unit] | |
| srt_total_prices = [srt_std_fast_total, srt_std_deep_total, srt_pro_fast_total, srt_pro_deep_total, | |
| srt_expert_fast_total, srt_expert_deep_total] | |
| # service_names 可以在两个选项卡中复用 | |
| # service_names = ["标准版 - 快速", "标准版 - 深度", "专业版 - 快速", "专业版 - 深度", "专家版 - 快速", "专家版 - 深度"] | |
| # --- 核心联动逻辑 --- | |
| # 1. 当排版选项改变时,调用函数更新六个单价输入框 | |
| srt_layout_selector.change( | |
| fn=srt_update_layout_prices, | |
| inputs=srt_layout_selector, | |
| outputs=srt_unit_prices | |
| ) | |
| # 2. 当数量或任一单价改变时,重新计算对应的总价 | |
| # (注意:单价的改变是由上一步触发的,这样就形成了事件链) | |
| for unit_input, total_output in zip(srt_unit_prices, srt_total_prices): | |
| srt_quantity.change(calculate_total, inputs=[srt_quantity, unit_input], outputs=total_output) | |
| unit_input.change(calculate_total, inputs=[srt_quantity, unit_input], outputs=total_output) | |
| # 3. 当任一总价改变时,更新服务方案选项,并接着更新最终应付总额 | |
| def srt_update_radio_choices(*totals): | |
| choices = [f"{total} | {name}" for total, name in zip(totals, service_names)] | |
| # 默认选择价格最高的方案 | |
| new_value = choices[-1] if choices else None | |
| # 【修改】返回两个值:更新后的Radio组件 和 它最新的值 | |
| return gr.Radio(choices=choices, value=new_value), new_value | |
| for total_output in srt_total_prices: | |
| total_output.change( | |
| fn=srt_update_radio_choices, | |
| inputs=srt_total_prices, | |
| # 【修改】函数的两个返回值分别更新Radio组件和隐藏文本框 | |
| outputs=[srt_service_choice_radio, srt_selected_value_hidden] | |
| ).then( | |
| fn=update_final_price, | |
| # 【修改】关键!让 update_final_price 从隐藏文本框获取稳定、正确的值 | |
| inputs=[srt_selected_value_hidden, srt_discount_selector], | |
| outputs=srt_grand_total_display | |
| ) | |
| # 4. 当服务方案或折扣选项直接改变时,也更新最终应付总额 | |
| srt_service_choice_radio.change(update_final_price, | |
| inputs=[srt_service_choice_radio, srt_discount_selector], | |
| outputs=srt_grand_total_display) | |
| srt_discount_selector.change(update_final_price, inputs=[srt_service_choice_radio, srt_discount_selector], | |
| outputs=srt_grand_total_display) | |
| # 5. 定义生成PDF的点击事件 | |
| srt_all_inputs = [ | |
| srt_customer_name, srt_quote_id, srt_validity_period, srt_content_type, | |
| srt_source_language, | |
| srt_target_language, srt_billing_unit, srt_quantity, srt_delivery_time, | |
| srt_grand_total_display, srt_service_choice_radio, srt_discount_selector, | |
| srt_layout_selector # 【新增】将排版选项也作为输入 | |
| ] + srt_unit_prices + srt_total_prices | |
| # 假设你为表格创建了一个新的处理函数 table_handle_form_submission 和一个新的模板 table.tex | |
| # generate_button.click(fn=table_handle_form_submission, inputs=all_inputs, outputs=pdf_output) | |
| # 绑定点击事件到新的包装函数 | |
| srt_generate_button.click(fn=srt_handle_form_submission, inputs=srt_all_inputs, outputs=srt_pdf_output) | |
| # 6. 定义此选项卡的初始加载函数 | |
| def srt_initial_load(q, layout_choice): | |
| # 1. 根据默认排版获取单价 | |
| unit_prices_list = [f"¥ {p}" for p in SRT_LAYOUT_PRICES.get(layout_choice, (0,) * 6)] | |
| # 2. 计算所有总价 | |
| totals = [calculate_total(q, unit) for unit in unit_prices_list] | |
| # 3. 生成Radio选项 | |
| radio_choices = [f"{total} | {name}" for total, name in zip(totals, service_names)] | |
| # 4. 计算最终价格 | |
| initial_final_price = update_final_price(radio_choices[-1] if radio_choices else None, | |
| discount_options[0]) | |
| # 返回所有需要更新的组件值 | |
| return [ | |
| *unit_prices_list, | |
| *totals, | |
| gr.Radio(choices=radio_choices, value=radio_choices[-1] if radio_choices else None), | |
| initial_final_price | |
| ] | |
| demo.load( | |
| fn=srt_initial_load, | |
| inputs=[srt_quantity, srt_layout_selector], | |
| outputs=srt_unit_prices + srt_total_prices + [srt_service_choice_radio, srt_grand_total_display] | |
| ) | |
| with gr.Tab("其他工具"): | |
| with gr.Tab("双语"): | |
| input_translated_json = gr.File(label="输入翻译获得的Json文件") | |
| output_one_lang_txt = gr.File(label="输出仅译文") | |
| output_double_lang_txt = gr.File(label="输出双语内容") | |
| double_gen_button = gr.Button("转换") | |
| double_gen_button.click(fn=translated_json2txt_file, inputs=[input_translated_json], | |
| outputs=[output_one_lang_txt, output_double_lang_txt]) | |
| with gr.Tab("LaTeX格式化"): | |
| input_LaTeX_text = gr.File(label="待格式化的LaTeX文本") | |
| output_LaTeX_txt = gr.File(label="格式化后文本") | |
| LaTeX_gen_button = gr.Button("格式化") | |
| LaTeX_gen_button.click(fn=latex2txt_blocks, inputs=[input_LaTeX_text], outputs=[output_LaTeX_txt]) | |
| with gr.Tab("想学的很多"): | |
| input_excel = gr.File(label="输入excel文件") | |
| output_csv = gr.File(label="待Notion的csv文件") | |
| free_button = gr.Button("免费队列") | |
| money_button = gr.Button("付费队列") | |
| free_button.click(fn=build_free, inputs=[input_excel],outputs=[output_csv]) | |
| money_button.click(fn=build_money, inputs=[input_excel], outputs=[output_csv]) | |
| return demo | |
| def text_handle_form_submission( | |
| # 基本信息 (5) | |
| customer_name, quote_id, validity_period, content_type, source_language, | |
| # 项目详情 (6) | |
| target_language, billing_unit, quantity, delivery_time, | |
| paragraph_count, char_count, | |
| # 价格与选择 (3) | |
| grand_total, service_choice, discount_choice, | |
| # 各项单价 (6) | |
| std_fast_unit, std_deep_unit, pro_fast_unit, pro_deep_unit, expert_fast_unit, expert_deep_unit, | |
| # 各项总价 (6) | |
| std_fast_total, std_deep_total, pro_fast_total, pro_deep_total, expert_fast_total, expert_deep_total | |
| ): | |
| """ | |
| 【已重构 v2】收集表单数据,为LaTex模板准备占位符,并调用PDF生成函数。 | |
| 此版本新增了最低消费逻辑和更专业的措辞。 | |
| """ | |
| # --- 1. 解析所需的核心数值 --- | |
| original_price_val = parse_price(service_choice.split('|')[0]) if service_choice and '|' in service_choice else 0 | |
| final_price_val = parse_price(grand_total) | |
| service_desc_text = service_choice.split('|')[1].strip() if service_choice and '|' in service_choice else "" | |
| # --- 2. 【重构】准备优惠相关的 LaTeX 字符串 --- | |
| discount_section_str = "" | |
| discount_terms_str = "" | |
| is_discounted = final_price_val < original_price_val | |
| if is_discounted: | |
| # ... (此部分优惠逻辑与您提供的代码相同,保持不变) ... | |
| discount_desc_text = "" | |
| # 根据选择的优惠类型,生成不同的描述和条款 | |
| if "新客优惠" in discount_choice: | |
| discount_desc_text = "新客优惠 (9折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”。" | |
| "此优惠适用于原始总价在 ¥ 50.00 至 ¥ 200.00 之间的订单。" | |
| ) | |
| elif "学生优惠" in discount_choice: | |
| discount_desc_text = "学生优惠 (7折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”," | |
| "请在合作前提供相关有效证明(如学生证等)。" | |
| ) | |
| elif "译者优惠" in discount_choice: | |
| discount_desc_text = "译者优惠 (8折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”," | |
| "请在合作前提供相关有效证明(如翻译资格证书等)。" | |
| ) | |
| # 生成通用的折扣明细 LaTeX 表格 | |
| if discount_desc_text: | |
| discount_amount_val = original_price_val - final_price_val | |
| discount_section_str = ( | |
| "\\begin{tabular}{@{} l r @{}}\n" | |
| f" 原始总计: & ¥ {original_price_val:,.2f} \\\\\n" | |
| f" {escape_latex(discount_desc_text)}: & - ¥ {discount_amount_val:,.2f} \\\\\n" | |
| " \\midrule[0.8pt]\n" | |
| "\\end{tabular}\n" | |
| ) | |
| # --- 3. 【新增】处理最低消费逻辑 --- | |
| minimum_charge_note_str = "" | |
| MINIMUM_CHARGE = 20.00 | |
| if 0 < final_price_val <= MINIMUM_CHARGE: | |
| # 如果最终价格低于最低消费,则更新价格并生成说明 | |
| final_price_val = MINIMUM_CHARGE | |
| minimum_charge_note_str = ( | |
| f"本次订单总额低于我们的最低起步价(¥ {MINIMUM_CHARGE:.2f}),已按最低起步价计费。" | |
| ) | |
| # --- 4. 准备最终的报价数据字典 --- | |
| quote_data = { | |
| # 基本信息 (已包含润色后的字段) | |
| "customer_name": customer_name, "quote_id": quote_id, "validity_period": validity_period, | |
| "content_type": content_type, "source_language": source_language, "target_language": target_language, | |
| "delivery_time": delivery_time, | |
| # 纯文本类特定字段 | |
| "billing_unit": billing_unit, | |
| "quantity": str(quantity), | |
| "paragraph_count": str(paragraph_count), | |
| "char_count": f"{char_count:,}", # 使用千位分隔符,更易读 | |
| # 价格明细 | |
| "std_fast_unit_price": f"{std_fast_unit} / {billing_unit}", | |
| "std_deep_unit_price": f"{std_deep_unit} / {billing_unit}", | |
| "pro_fast_unit_price": f"{pro_fast_unit} / {billing_unit}", | |
| "pro_deep_unit_price": f"{pro_deep_unit} / {billing_unit}", | |
| "expert_fast_unit_price": f"{expert_fast_unit} / {billing_unit}", | |
| "expert_deep_unit_price": f"{expert_deep_unit} / {billing_unit}", | |
| "std_fast_total_price": std_fast_total, "std_deep_total_price": std_deep_total, | |
| "pro_fast_total_price": pro_fast_total, "pro_deep_total_price": pro_deep_total, | |
| "expert_fast_total_price": expert_fast_total, "expert_deep_total_price": expert_deep_total, | |
| # 总价与优惠部分 (使用更新后的变量) | |
| "discount_section": discount_section_str, | |
| "grand_total_price": f"¥ {final_price_val:,.2f}", # 使用可能已更新的final_price_val | |
| "selected_plan": service_desc_text, | |
| "discount_terms_note": discount_terms_str, | |
| "minimum_charge_note": minimum_charge_note_str, # 新增字段 | |
| } | |
| # 定义输出文件名 | |
| safe_customer_name = re.sub(r'[\\/*?:"<>|]', "", customer_name) | |
| output_filename = f"Quote_{quote_id}_{safe_customer_name}.pdf" | |
| output_path = os.path.join(tempfile.gettempdir(), output_filename) | |
| # 调用核心函数生成PDF,并指定使用新模板 | |
| generated_file = generate_quote_pdf(quote_data, output_path, "text.tex") | |
| return generated_file | |
| def table_handle_form_submission( | |
| # --- 基本信息 (12个参数) --- | |
| customer_name, quote_id, validity_period, content_type, source_language, | |
| target_language, billing_unit, quantity, delivery_time, grand_total, | |
| service_choice, discount_choice, | |
| # --- 新增的第13个参数 --- | |
| layout_choice, | |
| # --- 价格明细 (12个参数) --- | |
| std_fast_unit, std_deep_unit, pro_fast_unit, pro_deep_unit, expert_fast_unit, expert_deep_unit, | |
| std_fast_total, std_deep_total, pro_fast_total, pro_deep_total, expert_fast_total, expert_deep_total): | |
| """ | |
| 【表格类处理函数 v2】收集表单数据,为表格类LaTex模板准备占位符。 | |
| 新增了对'layout_choice'的处理和最低消费逻辑。 | |
| """ | |
| # --- 1. 解析所需的核心数值 --- | |
| original_price_val = parse_price(service_choice.split('|')[0]) if service_choice and '|' in service_choice else 0 | |
| final_price_val = parse_price(grand_total) | |
| service_desc_text = service_choice.split('|')[1].strip() if service_choice and '|' in service_choice else "" | |
| # --- 2. 准备优惠相关的 LaTeX 字符串 --- | |
| discount_section_str = "" | |
| discount_terms_str = "" | |
| is_discounted = final_price_val < original_price_val | |
| if is_discounted: | |
| # ... (此部分优惠逻辑与您提供的代码相同,保持不变) ... | |
| discount_desc_text = "" | |
| # 根据选择的优惠类型,生成不同的描述和条款 | |
| if "新客优惠" in discount_choice: | |
| discount_desc_text = "新客优惠 (9折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”。" | |
| "此优惠适用于原始总价在 ¥ 50.00 至 ¥ 200.00 之间的订单。" | |
| ) | |
| elif "学生优惠" in discount_choice: | |
| discount_desc_text = "学生优惠 (7折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”," | |
| "请在合作前提供相关有效证明(如学生证等)。" | |
| ) | |
| elif "译者优惠" in discount_choice: | |
| discount_desc_text = "译者优惠 (8折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”," | |
| "请在合作前提供相关有效证明(如翻译资格证书等)。" | |
| ) | |
| # 生成通用的折扣明细 LaTeX 表格 | |
| if discount_desc_text: | |
| discount_amount_val = original_price_val - final_price_val | |
| discount_section_str = ( | |
| "\\begin{tabular}{@{} l r @{}}\n" | |
| f" 原始总计: & ¥ {original_price_val:,.2f} \\\\\n" | |
| f" {escape_latex(discount_desc_text)}: & - ¥ {discount_amount_val:,.2f} \\\\\n" | |
| " \\midrule[0.8pt]\n" | |
| "\\end{tabular}\n" | |
| ) | |
| # --- 3. 【新增】处理最低消费逻辑 --- | |
| minimum_charge_note_str = "" | |
| MINIMUM_CHARGE = 20.00 # 定义最低消费金额 | |
| if 0 < final_price_val <= MINIMUM_CHARGE: | |
| final_price_val = MINIMUM_CHARGE | |
| minimum_charge_note_str = ( | |
| f"本次订单总额低于我们的最低起步价(¥ {MINIMUM_CHARGE:.2f}),已按最低起步价计费。" | |
| ) | |
| # --- 4. 准备最终的报价数据字典 --- | |
| quote_data = { | |
| # 基本信息 | |
| "customer_name": customer_name, "quote_id": quote_id, "validity_period": validity_period, | |
| "content_type": content_type, "source_language": source_language, "target_language": target_language, | |
| "billing_unit": billing_unit, "quantity": str(quantity), "delivery_time": delivery_time, | |
| # --- 新增字段以匹配新模板 --- | |
| "layout_choice": escape_latex(layout_choice), | |
| # 价格明细 | |
| "std_fast_unit_price": f"{std_fast_unit} / {billing_unit}", | |
| "std_deep_unit_price": f"{std_deep_unit} / {billing_unit}", | |
| "pro_fast_unit_price": f"{pro_fast_unit} / {billing_unit}", | |
| "pro_deep_unit_price": f"{pro_deep_unit} / {billing_unit}", | |
| "expert_fast_unit_price": f"{expert_fast_unit} / {billing_unit}", | |
| "expert_deep_unit_price": f"{expert_deep_unit} / {billing_unit}", | |
| "std_fast_total_price": std_fast_total, "std_deep_total_price": std_deep_total, | |
| "pro_fast_total_price": pro_fast_total, "pro_deep_total_price": pro_deep_total, | |
| "expert_fast_total_price": expert_fast_total, "expert_deep_total_price": expert_deep_total, | |
| # 总价与优惠部分 (使用更新后的变量) | |
| "discount_section": discount_section_str, | |
| "grand_total_price": f"¥ {final_price_val:,.2f}", # 使用可能已更新的final_price_val | |
| "selected_plan": service_desc_text, | |
| "discount_terms_note": discount_terms_str, | |
| "minimum_charge_note": minimum_charge_note_str, # 新增字段 | |
| } | |
| # 定义输出文件名 | |
| safe_customer_name = re.sub(r'[\\/*?:"<>|]', "", customer_name) | |
| output_filename = f"Quote_{quote_id}_{safe_customer_name}_Table.pdf" # 建议加后缀区分 | |
| output_path = os.path.join(tempfile.gettempdir(), output_filename) | |
| # 调用核心函数生成PDF,并指定使用新的表格模板 | |
| generated_file = generate_quote_pdf(quote_data, output_path, "table.tex") | |
| return generated_file | |
| def chun_pdf_handle_form_submission( | |
| customer_name, quote_id, validity_period, content_type, source_language, | |
| target_language, billing_unit, quantity, delivery_time, grand_total, | |
| service_choice, discount_choice, | |
| std_fast_unit, std_deep_unit, pro_fast_unit, pro_deep_unit, expert_fast_unit, expert_deep_unit, | |
| std_fast_total, std_deep_total, pro_fast_total, pro_deep_total, expert_fast_total, expert_deep_total): | |
| """ | |
| 【纯净PDF处理函数 v2】收集表单数据,为LaTex模板准备占位符。 | |
| 新增了最低消费逻辑。 | |
| """ | |
| # --- 1. 解析所需的核心数值 --- | |
| original_price_val = parse_price(service_choice.split('|')[0]) if service_choice and '|' in service_choice else 0 | |
| final_price_val = parse_price(grand_total) | |
| service_desc_text = service_choice.split('|')[1].strip() if service_choice and '|' in service_choice else "" | |
| # --- 2. 准备优惠相关的 LaTeX 字符串 --- | |
| discount_section_str = "" | |
| discount_terms_str = "" | |
| is_discounted = final_price_val < original_price_val | |
| if is_discounted: | |
| # ... (此部分优惠逻辑与您提供的代码相同,保持不变) ... | |
| discount_desc_text = "" | |
| # 根据选择的优惠类型,生成不同的描述和条款 | |
| if "新客优惠" in discount_choice: | |
| discount_desc_text = "新客优惠 (9折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”。" | |
| "此优惠适用于原始总价在 ¥ 50.00 至 ¥ 200.00 之间的订单。" | |
| ) | |
| elif "学生优惠" in discount_choice: | |
| discount_desc_text = "学生优惠 (7折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”," | |
| "请在合作前提供相关有效证明(如学生证等)。" | |
| ) | |
| elif "译者优惠" in discount_choice: | |
| discount_desc_text = "译者优惠 (8折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”," | |
| "请在合作前提供相关有效证明(如翻译资格证书等)。" | |
| ) | |
| # 生成通用的折扣明细 LaTeX 表格 | |
| if discount_desc_text: | |
| discount_amount_val = original_price_val - final_price_val | |
| discount_section_str = ( | |
| "\\begin{tabular}{@{} l r @{}}\n" | |
| f" 原始总计: & ¥ {original_price_val:,.2f} \\\\\n" | |
| f" {escape_latex(discount_desc_text)}: & - ¥ {discount_amount_val:,.2f} \\\\\n" | |
| " \\midrule[0.8pt]\n" | |
| "\\end{tabular}\n" | |
| ) | |
| # --- 3. 【新增】处理最低消费逻辑 --- | |
| minimum_charge_note_str = "" | |
| MINIMUM_CHARGE = 20.00 | |
| if 0 < final_price_val <= MINIMUM_CHARGE: | |
| final_price_val = MINIMUM_CHARGE | |
| minimum_charge_note_str = ( | |
| f"本次订单总额低于我们的最低起步价(¥ {MINIMUM_CHARGE:.2f}),已按最低起步价计费。" | |
| ) | |
| # --- 4. 准备最终的报价数据字典 --- | |
| quote_data = { | |
| # 基本信息 | |
| "customer_name": customer_name, "quote_id": quote_id, "validity_period": validity_period, | |
| "content_type": content_type, "source_language": source_language, "target_language": target_language, | |
| "billing_unit": billing_unit, "quantity": str(quantity), "delivery_time": delivery_time, | |
| # 价格明细 | |
| "std_fast_unit_price": f"{std_fast_unit} / {billing_unit}", | |
| "std_deep_unit_price": f"{std_deep_unit} / {billing_unit}", | |
| "pro_fast_unit_price": f"{pro_fast_unit} / {billing_unit}", | |
| "pro_deep_unit_price": f"{pro_deep_unit} / {billing_unit}", | |
| "expert_fast_unit_price": f"{expert_fast_unit} / {billing_unit}", | |
| "expert_deep_unit_price": f"{expert_deep_unit} / {billing_unit}", | |
| "std_fast_total_price": std_fast_total, "std_deep_total_price": std_deep_total, | |
| "pro_fast_total_price": pro_fast_total, "pro_deep_total_price": pro_deep_total, | |
| "expert_fast_total_price": expert_fast_total, "expert_deep_total_price": expert_deep_total, | |
| # 总价与优惠部分 (使用更新后的变量) | |
| "discount_section": discount_section_str, | |
| "grand_total_price": f"¥ {final_price_val:,.2f}", # 使用可能已更新的final_price_val | |
| "selected_plan": service_desc_text, | |
| "discount_terms_note": discount_terms_str, | |
| "minimum_charge_note": minimum_charge_note_str, # 新增字段 | |
| } | |
| # 定义输出文件名 | |
| safe_customer_name = re.sub(r'[\\/*?:"<>|]', "", customer_name) | |
| output_filename = f"Quote_{quote_id}_{safe_customer_name}_PDF.pdf" | |
| output_path = os.path.join(tempfile.gettempdir(), output_filename) | |
| # 调用核心函数生成PDF,并指定使用新的模板 | |
| generated_file = generate_quote_pdf(quote_data, output_path, "chun_pdf.tex") | |
| return generated_file | |
| def srt_handle_form_submission( | |
| # --- 基本信息 (12个参数) --- | |
| customer_name, quote_id, validity_period, content_type, source_language, | |
| target_language, billing_unit, quantity, delivery_time, grand_total, | |
| service_choice, discount_choice, | |
| # --- 新增的第13个参数 --- | |
| layout_choice, | |
| # --- 价格明细 (12个参数) --- | |
| std_fast_unit, std_deep_unit, pro_fast_unit, pro_deep_unit, expert_fast_unit, expert_deep_unit, | |
| std_fast_total, std_deep_total, pro_fast_total, pro_deep_total, expert_fast_total, expert_deep_total): | |
| """ | |
| 【字幕文件处理函数 v2】收集表单数据,为LaTex模板准备占位符。 | |
| 新增了对'layout_choice'的处理和最低消费逻辑。 | |
| """ | |
| # --- 1. 解析所需的核心数值 --- | |
| original_price_val = parse_price(service_choice.split('|')[0]) if service_choice and '|' in service_choice else 0 | |
| final_price_val = parse_price(grand_total) | |
| service_desc_text = service_choice.split('|')[1].strip() if service_choice and '|' in service_choice else "" | |
| # --- 2. 准备优惠相关的 LaTeX 字符串 --- | |
| discount_section_str = "" | |
| discount_terms_str = "" | |
| is_discounted = final_price_val < original_price_val | |
| if is_discounted: | |
| # ... (此部分优惠逻辑与您提供的代码相同,保持不变) ... | |
| discount_desc_text = "" | |
| # 根据选择的优惠类型,生成不同的描述和条款 | |
| if "新客优惠" in discount_choice: | |
| discount_desc_text = "新客优惠 (9折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”。" | |
| "此优惠适用于原始总价在 ¥ 50.00 至 ¥ 200.00 之间的订单。" | |
| ) | |
| elif "学生优惠" in discount_choice: | |
| discount_desc_text = "学生优惠 (7折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”," | |
| "请在合作前提供相关有效证明(如学生证等)。" | |
| ) | |
| elif "译者优惠" in discount_choice: | |
| discount_desc_text = "译者优惠 (8折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”," | |
| "请在合作前提供相关有效证明(如翻译资格证书等)。" | |
| ) | |
| # 生成通用的折扣明细 LaTeX 表格 | |
| if discount_desc_text: | |
| discount_amount_val = original_price_val - final_price_val | |
| discount_section_str = ( | |
| "\\begin{tabular}{@{} l r @{}}\n" | |
| f" 原始总计: & ¥ {original_price_val:,.2f} \\\\\n" | |
| f" {escape_latex(discount_desc_text)}: & - ¥ {discount_amount_val:,.2f} \\\\\n" | |
| " \\midrule[0.8pt]\n" | |
| "\\end{tabular}\n" | |
| ) | |
| # --- 3. 【新增】处理最低消费逻辑 --- | |
| minimum_charge_note_str = "" | |
| MINIMUM_CHARGE = 20.00 | |
| if 0 < final_price_val <= MINIMUM_CHARGE: | |
| final_price_val = MINIMUM_CHARGE | |
| minimum_charge_note_str = ( | |
| f"本次订单总额低于我们的最低起步价(¥ {MINIMUM_CHARGE:.2f}),已按最低起步价计费。" | |
| ) | |
| # --- 4. 准备最终的报价数据字典 --- | |
| quote_data = { | |
| # 基本信息 | |
| "customer_name": customer_name, "quote_id": quote_id, "validity_period": validity_period, | |
| "content_type": content_type, "source_language": source_language, "target_language": target_language, | |
| "billing_unit": billing_unit, "quantity": str(quantity), "delivery_time": delivery_time, | |
| # --- 【修复】新增字段以匹配新模板 --- | |
| "layout_choice": escape_latex(layout_choice), | |
| # 价格明细 | |
| "std_fast_unit_price": f"{std_fast_unit} / {billing_unit}", | |
| "std_deep_unit_price": f"{std_deep_unit} / {billing_unit}", | |
| "pro_fast_unit_price": f"{pro_fast_unit} / {billing_unit}", | |
| "pro_deep_unit_price": f"{pro_deep_unit} / {billing_unit}", | |
| "expert_fast_unit_price": f"{expert_fast_unit} / {billing_unit}", | |
| "expert_deep_unit_price": f"{expert_deep_unit} / {billing_unit}", | |
| "std_fast_total_price": std_fast_total, "std_deep_total_price": std_deep_total, | |
| "pro_fast_total_price": pro_fast_total, "pro_deep_total_price": pro_deep_total, | |
| "expert_fast_total_price": expert_fast_total, "expert_deep_total_price": expert_deep_total, | |
| # 总价与优惠部分 (使用更新后的变量) | |
| "discount_section": discount_section_str, | |
| "grand_total_price": f"¥ {final_price_val:,.2f}", # 使用可能已更新的final_price_val | |
| "selected_plan": service_desc_text, | |
| "discount_terms_note": discount_terms_str, | |
| "minimum_charge_note": minimum_charge_note_str, # 新增字段 | |
| } | |
| # 定义输出文件名 | |
| safe_customer_name = re.sub(r'[\\/*?:"<>|]', "", customer_name) | |
| output_filename = f"Quote_{quote_id}_{safe_customer_name}_Subtitle.pdf" | |
| output_path = os.path.join(tempfile.gettempdir(), output_filename) | |
| # 调用核心函数生成PDF,并指定使用新的模板 | |
| generated_file = generate_quote_pdf(quote_data, output_path, "srt.tex") | |
| return generated_file | |
| def complex_handle_form_submission( | |
| # --- 基本信息 (12个参数) --- | |
| customer_name, quote_id, validity_period, content_type, source_language, | |
| target_language, billing_unit, quantity, delivery_time, grand_total, | |
| service_choice, discount_choice, | |
| # --- 新增的第13个参数 --- | |
| layout_choice, | |
| # --- 价格明细 (12个参数) --- | |
| std_fast_unit, std_deep_unit, pro_fast_unit, pro_deep_unit, expert_fast_unit, expert_deep_unit, | |
| std_fast_total, std_deep_total, pro_fast_total, pro_deep_total, expert_fast_total, expert_deep_total): | |
| """ | |
| 【复杂文档处理函数 v2】收集表单数据,为LaTex模板准备占位符。 | |
| 新增了对'layout_choice'的处理和最低消费逻辑。 | |
| """ | |
| # --- 1. 解析所需的核心数值 --- | |
| original_price_val = parse_price(service_choice.split('|')[0]) if service_choice and '|' in service_choice else 0 | |
| final_price_val = parse_price(grand_total) | |
| service_desc_text = service_choice.split('|')[1].strip() if service_choice and '|' in service_choice else "" | |
| # --- 2. 准备优惠相关的 LaTeX 字符串 --- | |
| discount_section_str = "" | |
| discount_terms_str = "" | |
| is_discounted = final_price_val < original_price_val | |
| if is_discounted: | |
| # ... (此部分优惠逻辑与您提供的代码相同,保持不变) ... | |
| discount_desc_text = "" | |
| # 根据选择的优惠类型,生成不同的描述和条款 | |
| if "新客优惠" in discount_choice: | |
| discount_desc_text = "新客优惠 (9折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”。" | |
| "此优惠适用于原始总价在 ¥ 50.00 至 ¥ 200.00 之间的订单。" | |
| ) | |
| elif "学生优惠" in discount_choice: | |
| discount_desc_text = "学生优惠 (7折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”," | |
| "请在合作前提供相关有效证明(如学生证等)。" | |
| ) | |
| elif "译者优惠" in discount_choice: | |
| discount_desc_text = "译者优惠 (8折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”," | |
| "请在合作前提供相关有效证明(如翻译资格证书等)。" | |
| ) | |
| # 生成通用的折扣明细 LaTeX 表格 | |
| if discount_desc_text: | |
| discount_amount_val = original_price_val - final_price_val | |
| discount_section_str = ( | |
| "\\begin{tabular}{@{} l r @{}}\n" | |
| f" 原始总计: & ¥ {original_price_val:,.2f} \\\\\n" | |
| f" {escape_latex(discount_desc_text)}: & - ¥ {discount_amount_val:,.2f} \\\\\n" | |
| " \\midrule[0.8pt]\n" | |
| "\\end{tabular}\n" | |
| ) | |
| # --- 3. 【新增】处理最低消费逻辑 --- | |
| minimum_charge_note_str = "" | |
| MINIMUM_CHARGE = 20.00 | |
| if 0 < final_price_val <= MINIMUM_CHARGE: | |
| final_price_val = MINIMUM_CHARGE | |
| minimum_charge_note_str = ( | |
| f"本次订单总额低于我们的最低起步价(¥ {MINIMUM_CHARGE:.2f}),已按最低起步价计费。" | |
| ) | |
| # --- 4. 准备最终的报价数据字典 --- | |
| quote_data = { | |
| # 基本信息 | |
| "customer_name": customer_name, "quote_id": quote_id, "validity_period": validity_period, | |
| "content_type": content_type, "source_language": source_language, "target_language": target_language, | |
| "billing_unit": billing_unit, "quantity": str(quantity), "delivery_time": delivery_time, | |
| # --- 【修复】新增字段以匹配新模板 --- | |
| "layout_choice": escape_latex(layout_choice), | |
| # 价格明细 | |
| "std_fast_unit_price": f"{std_fast_unit} / {billing_unit}", | |
| "std_deep_unit_price": f"{std_deep_unit} / {billing_unit}", | |
| "pro_fast_unit_price": f"{pro_fast_unit} / {billing_unit}", | |
| "pro_deep_unit_price": f"{pro_deep_unit} / {billing_unit}", | |
| "expert_fast_unit_price": f"{expert_fast_unit} / {billing_unit}", | |
| "expert_deep_unit_price": f"{expert_deep_unit} / {billing_unit}", | |
| "std_fast_total_price": std_fast_total, "std_deep_total_price": std_deep_total, | |
| "pro_fast_total_price": pro_fast_total, "pro_deep_total_price": pro_deep_total, | |
| "expert_fast_total_price": expert_fast_total, "expert_deep_total_price": expert_deep_total, | |
| # 总价与优惠部分 (使用更新后的变量) | |
| "discount_section": discount_section_str, | |
| "grand_total_price": f"¥ {final_price_val:,.2f}", # 使用可能已更新的final_price_val | |
| "selected_plan": service_desc_text, | |
| "discount_terms_note": discount_terms_str, | |
| "minimum_charge_note": minimum_charge_note_str, # 新增字段 | |
| } | |
| # 定义输出文件名 | |
| safe_customer_name = re.sub(r'[\\/*?:"<>|]', "", customer_name) | |
| output_filename = f"Quote_{quote_id}_{safe_customer_name}_Complex.pdf" | |
| output_path = os.path.join(tempfile.gettempdir(), output_filename) | |
| # 调用核心函数生成PDF,并指定使用新的模板 | |
| generated_file = generate_quote_pdf(quote_data, output_path, "complex_pdf.tex") | |
| return generated_file | |
| def project_handle_form_submission( | |
| # --- 基本信息 (12个参数) --- | |
| customer_name, quote_id, validity_period, content_type, source_language, | |
| target_language, billing_unit, quantity, delivery_time, grand_total, | |
| service_choice, discount_choice, | |
| # --- 新增的第13个参数 --- | |
| layout_choice, | |
| # --- 价格明细 (12个参数) --- | |
| std_fast_unit, std_deep_unit, pro_fast_unit, pro_deep_unit, expert_fast_unit, expert_deep_unit, | |
| std_fast_total, std_deep_total, pro_fast_total, pro_deep_total, expert_fast_total, expert_deep_total): | |
| """ | |
| 【工程图处理函数 v2】收集表单数据,为LaTex模板准备占位符。 | |
| 新增了对'layout_choice'的处理和最低消费逻辑。 | |
| """ | |
| # --- 1. 解析所需的核心数值 --- | |
| original_price_val = parse_price(service_choice.split('|')[0]) if service_choice and '|' in service_choice else 0 | |
| final_price_val = parse_price(grand_total) | |
| service_desc_text = service_choice.split('|')[1].strip() if service_choice and '|' in service_choice else "" | |
| # --- 2. 准备优惠相关的 LaTeX 字符串 --- | |
| discount_section_str = "" | |
| discount_terms_str = "" | |
| is_discounted = final_price_val < original_price_val | |
| if is_discounted: | |
| # ... (此部分优惠逻辑与您提供的代码相同,保持不变) ... | |
| discount_desc_text = "" | |
| # 根据选择的优惠类型,生成不同的描述和条款 | |
| if "新客优惠" in discount_choice: | |
| discount_desc_text = "新客优惠 (9折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”。" | |
| "此优惠适用于原始总价在 ¥ 50.00 至 ¥ 200.00 之间的订单。" | |
| ) | |
| elif "学生优惠" in discount_choice: | |
| discount_desc_text = "学生优惠 (7折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”," | |
| "请在合作前提供相关有效证明(如学生证等)。" | |
| ) | |
| elif "译者优惠" in discount_choice: | |
| discount_desc_text = "译者优惠 (8折)" | |
| discount_terms_str = ( | |
| f"\\item \\textbf{{优惠说明:}} 您已选择“{escape_latex(discount_desc_text)}”," | |
| "请在合作前提供相关有效证明(如翻译资格证书等)。" | |
| ) | |
| # 生成通用的折扣明细 LaTeX 表格 | |
| if discount_desc_text: | |
| discount_amount_val = original_price_val - final_price_val | |
| discount_section_str = ( | |
| "\\begin{tabular}{@{} l r @{}}\n" | |
| f" 原始总计: & ¥ {original_price_val:,.2f} \\\\\n" | |
| f" {escape_latex(discount_desc_text)}: & - ¥ {discount_amount_val:,.2f} \\\\\n" | |
| " \\midrule[0.8pt]\n" | |
| "\\end{tabular}\n" | |
| ) | |
| # --- 3. 处理最低消费逻辑 --- | |
| minimum_charge_note_str = "" | |
| MINIMUM_CHARGE = 20.00 | |
| if 0 < final_price_val <= MINIMUM_CHARGE: | |
| final_price_val = MINIMUM_CHARGE | |
| minimum_charge_note_str = ( | |
| f"本次订单总额低于我们的最低起步价(¥ {MINIMUM_CHARGE:.2f}),已按最低起步价计费。" | |
| ) | |
| # --- 4. 准备最终的报价数据字典 --- | |
| quote_data = { | |
| # 基本信息 | |
| "customer_name": customer_name, "quote_id": quote_id, "validity_period": validity_period, | |
| "content_type": content_type, "source_language": source_language, "target_language": target_language, | |
| "billing_unit": billing_unit, "quantity": str(quantity), "delivery_time": delivery_time, | |
| # 新增字段以匹配新模板 | |
| "layout_choice": escape_latex(layout_choice), | |
| # 价格明细 | |
| "std_fast_unit_price": f"{std_fast_unit} / {billing_unit}", | |
| "std_deep_unit_price": f"{std_deep_unit} / {billing_unit}", | |
| "pro_fast_unit_price": f"{pro_fast_unit} / {billing_unit}", | |
| "pro_deep_unit_price": f"{pro_deep_unit} / {billing_unit}", | |
| "expert_fast_unit_price": f"{expert_fast_unit} / {billing_unit}", | |
| "expert_deep_unit_price": f"{expert_deep_unit} / {billing_unit}", | |
| "std_fast_total_price": std_fast_total, "std_deep_total_price": std_deep_total, | |
| "pro_fast_total_price": pro_fast_total, "pro_deep_total_price": pro_deep_total, | |
| "expert_fast_total_price": expert_fast_total, "expert_deep_total_price": expert_deep_total, | |
| # 总价与优惠部分 (使用更新后的变量) | |
| "discount_section": discount_section_str, | |
| "grand_total_price": f"¥ {final_price_val:,.2f}", # 使用可能已更新的final_price_val | |
| "selected_plan": service_desc_text, | |
| "discount_terms_note": discount_terms_str, | |
| "minimum_charge_note": minimum_charge_note_str, # 新增字段 | |
| } | |
| # 定义输出文件名 | |
| safe_customer_name = re.sub(r'[\\/*?:"<>|]', "", customer_name) | |
| output_filename = f"Quote_{quote_id}_{safe_customer_name}_Drawing.pdf" | |
| output_path = os.path.join(tempfile.gettempdir(), output_filename) | |
| # 调用核心函数生成PDF,并指定使用新的模板 | |
| generated_file = generate_quote_pdf(quote_data, output_path, "project.tex") | |
| return generated_file | |
| def build_free( | |
| input_path: str, | |
| output_path: Optional[str] = None, | |
| default_status: str = "待处理", | |
| dedup_by_link: bool = True, | |
| ) -> str: | |
| """ | |
| 将“这种 Excel”(含原始收集字段)转换为队列 CSV。 | |
| 输出列为:提交时间、状态、标题、链接、提交者、备注 | |
| 规则: | |
| - 提交时间:直接继承原 Excel 中的时间文本(不更改格式) | |
| - 状态:固定写入 `default_status`(默认:待处理) | |
| - 标题:格式为【机构】作者《中文课程名 - 英文课程名》 | |
| * 若作者为空或为“无”等,则省略作者部分 | |
| * 若中/英文课程名只有其一存在,则只使用存在的那个,不加“ - ” | |
| * 若两者皆空,则省略《》部分 | |
| * 机构为空时用“未填写机构” | |
| - 链接、提交者、备注:直接继承 | |
| - 若 `dedup_by_link=True`,则按“链接”去重,保留首个 | |
| 兼容列名(从左到右优先匹配): | |
| 提交时间: ["提交时间", "提交时间(自动)"] | |
| 机构/学校: ["机构/学校(必填)", "机构/学校", "机构", "学校"] | |
| 作者信息: ["作者信息(必填)", "作者信息", "作者"] | |
| 原语言课程名: ["课程名(原语言课程名)", "课程名(原文课程名)", "原语言课程名", "英文课程名", "课程名(英文)"] | |
| 中文课程名: ["课程名(中文课程名)", "中文课程名", "课程名(中文)", "课程名-中文"] | |
| 课程链接: ["课程链接(必填)", "链接", "URL"] | |
| 备注: ["备注", "说明"] | |
| 提交者: ["提交者(自动)", "提交者", "提交人"] | |
| 返回:生成的 CSV 路径(utf-8-sig 编码) | |
| """ | |
| # --------- 读入表格(优先 Excel,屏蔽 openpyxl 的默认样式告警) --------- | |
| with warnings.catch_warnings(): | |
| warnings.filterwarnings( | |
| "ignore", | |
| category=UserWarning, | |
| module=r"openpyxl\.styles\.stylesheet", | |
| ) | |
| try: | |
| df = pd.read_excel(input_path) # 默认读第一个工作表 | |
| except Exception: | |
| # 兜底尝试 CSV(如果用户误传了 .csv) | |
| for enc in ("utf-8", "utf-8-sig", "gb18030"): | |
| try: | |
| df = pd.read_csv(input_path, encoding=enc) | |
| break | |
| except Exception: | |
| df = None | |
| if df is None: | |
| raise RuntimeError("无法读取为 Excel 或 CSV。") | |
| # --------- 列名解析(兼容多种命名) --------- | |
| candidates: Dict[str, List[str]] = { | |
| "time": ["提交时间", "提交时间(自动)"], | |
| "org": ["机构/学校(必填)", "机构/学校", "机构", "学校"], | |
| "author": ["作者信息(必填)", "作者信息", "作者"], | |
| "title_en": ["课程名(原语言课程名)", "课程名(原文课程名)", "原语言课程名", "英文课程名", "课程名(英文)"], | |
| "title_cn": ["课程名(中文课程名)", "中文课程名", "课程名(中文)", "课程名-中文"], | |
| "link": ["课程链接(必填)", "链接", "URL"], | |
| "remark": ["备注", "说明"], | |
| "submitter": ["提交者(自动)", "提交者", "提交人"], | |
| } | |
| def resolve_col(name_list: Iterable[str]) -> str: | |
| for name in name_list: | |
| if name in df.columns: | |
| return name | |
| # 为方便定位问题,返回更清晰的报错 | |
| raise KeyError(f"找不到列名(任一均可):{list(name_list)};现有列:{list(df.columns)}") | |
| col_time = resolve_col(candidates["time"]) | |
| col_org = resolve_col(candidates["org"]) | |
| col_author = resolve_col(candidates["author"]) | |
| col_title_en = resolve_col(candidates["title_en"]) | |
| col_title_cn = resolve_col(candidates["title_cn"]) | |
| col_link = resolve_col(candidates["link"]) | |
| col_remark = resolve_col(candidates["remark"]) | |
| col_submitter = resolve_col(candidates["submitter"]) | |
| # --------- 清洗 & 构造标题 --------- | |
| def _s(x) -> str: | |
| if pd.isna(x): | |
| return "" | |
| return str(x).strip() | |
| def _author_ok(a: str) -> bool: | |
| aa = _s(a) | |
| return aa not in {"", "无", "None", "none", "N/A", "n/a", "null", "-", "—", "——"} | |
| def build_title(row) -> str: | |
| inst = _s(row[col_org]) or "未填写机构" | |
| author = _s(row[col_author]) | |
| cn = _s(row[col_title_cn]) | |
| en = _s(row[col_title_en]) | |
| inner_parts = [p for p in (cn, en) if p] | |
| inner = " - ".join(inner_parts) | |
| title = f"【{inst}】" | |
| if _author_ok(author): | |
| title += author | |
| if inner: | |
| title += f"《{inner}》" | |
| return title | |
| out = pd.DataFrame( | |
| { | |
| "提交时间": df[col_time].apply(_s), | |
| "状态": default_status, | |
| "标题": df.apply(build_title, axis=1), | |
| "链接": df[col_link].apply(_s), | |
| "提交者": df[col_submitter].apply(_s), | |
| "备注": df[col_remark].apply(_s), | |
| } | |
| ) | |
| # --------- 去重(按链接) --------- | |
| if dedup_by_link: | |
| out["__link_norm__"] = out["链接"].str.strip().str.lower() | |
| out = out.drop_duplicates(subset="__link_norm__", keep="first").drop(columns="__link_norm__") | |
| # --------- 写出 CSV --------- | |
| in_path = Path(input_path) | |
| if output_path is None: | |
| output_path = str(in_path.with_name(in_path.stem + "_队列.csv")) | |
| out.to_csv(output_path, index=False, encoding="utf-8-sig") | |
| return output_path | |
| def build_money(input_excel_path: str) -> str: | |
| """ | |
| 将 Excel 转换为队列 CSV,列为:提交时间、状态、标题、链接、提交者、备注 | |
| """ | |
| # --- 静默 openpyxl 的 “no default style” 告警 --- | |
| with warnings.catch_warnings(): | |
| warnings.filterwarnings( | |
| "ignore", | |
| category=UserWarning, | |
| module=r"openpyxl\.styles\.stylesheet" | |
| ) | |
| df = pd.read_excel(input_excel_path) # 默认读第一个工作表 | |
| COL_SUBMIT_TIME = "提交时间(自动)" | |
| COL_INST = "机构/学校(必填)" | |
| COL_AUTHOR = "作者信息(必填)" | |
| COL_ORIG = "课程名(原语言课程名)" # 作为“英文课程名”来源 | |
| COL_CN = "课程名(中文课程名)" | |
| COL_LINK = "课程链接(必填)" | |
| COL_REMARK = "备注" | |
| COL_SUBMITTER = "提交者(自动)" | |
| required_cols = [COL_SUBMIT_TIME, COL_INST, COL_AUTHOR, COL_ORIG, COL_CN, COL_LINK, COL_REMARK, COL_SUBMITTER] | |
| missing = [c for c in required_cols if c not in df.columns] | |
| if missing: | |
| raise ValueError(f"缺少必要列:{missing}") | |
| def _s(x) -> str: | |
| if pd.isna(x): | |
| return "" | |
| return str(x).strip() | |
| def _author_ok(a: str) -> bool: | |
| aa = _s(a) | |
| return aa not in {"", "无", "None", "none", "N/A", "null", "-"} | |
| def _build_title(row) -> str: | |
| inst = _s(row[COL_INST]) or "未填写机构" | |
| author = _s(row[COL_AUTHOR]) | |
| cn = _s(row[COL_CN]) | |
| en = _s(row[COL_ORIG]) | |
| parts = [p for p in [cn, en] if p] | |
| inner = " - ".join(parts) | |
| title = f"【{inst}】" | |
| if _author_ok(author): | |
| title += author | |
| if inner: | |
| title += f"《{inner}》" | |
| return title | |
| out = pd.DataFrame({ | |
| "提交时间": df[COL_SUBMIT_TIME].apply(_s), | |
| "状态": "待处理", | |
| "标题": df.apply(_build_title, axis=1), | |
| "链接": df[COL_LINK].apply(_s), | |
| "提交者": df[COL_SUBMITTER].apply(_s), | |
| "备注": df[COL_REMARK].apply(_s), | |
| }) | |
| in_path = Path(input_excel_path) | |
| csv_path = in_path.with_name(in_path.stem + "_队列.csv") | |
| out.to_csv(csv_path, index=False, encoding="utf-8-sig") | |
| return str(csv_path) | |
| # ---------- 辅助:转义未被反斜杠保护的 % ---------- | |
| _ESCAPE_PERCENT_RE = re.compile(r'(?<!\\)%') # 前面不是反斜杠的 % | |
| def escape_percent(text: str) -> str: | |
| return _ESCAPE_PERCENT_RE.sub(r'\\%', text) | |
| # ------------------------------------------------- | |
| def translated_json2txt_file(json_path: Union[str, Path]) -> tuple[str, str]: | |
| """ | |
| 读取符合题述结构的 JSON 文件,生成: | |
| 1. 双语 TXT : 原文\\n\\n译文\\n\\n… | |
| 2. 纯译文 TXT: 译文\\n\\n… | |
| 返回值 (仅译文路径, 双语路径) | |
| """ | |
| json_path = Path(json_path).expanduser().resolve() | |
| if not json_path.is_file(): | |
| raise FileNotFoundError(f"找不到 JSON 文件: {json_path}") | |
| # 输出文件路径 | |
| bilingual_path = json_path.with_suffix(".txt") | |
| pure_path = json_path.with_name(f"{json_path.stem}_translated.txt") | |
| # 读取 JSON | |
| with json_path.open("r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| if "chunks" not in data or not isinstance(data["chunks"], list): | |
| raise ValueError("输入 JSON 缺少 'chunks' 列表") | |
| # 逐对拼接 | |
| ordered = sorted(data["chunks"], key=lambda c: c.get("chunk_index", 0)) | |
| bilingual_segments, translation_segments = [], [] | |
| for c in ordered: | |
| original = (c.get("original_chunk") or "").strip() | |
| translated = (c.get("refined_chunk") or c.get("translated_chunk") or "").strip() | |
| translated = escape_percent(translated) # ← 关键:转义 % | |
| bilingual_segments.append(f"{original}\n\n{translated}") | |
| translation_segments.append(translated) | |
| bilingual_segments = [_strip_linebreak_backslash(b) for b in bilingual_segments] | |
| translation_segments = [_strip_linebreak_backslash(b) for b in translation_segments] | |
| # 写入文件 | |
| bilingual_path.write_text("\n\n".join(bilingual_segments), encoding="utf-8") | |
| pure_path.write_text("\n\n".join(translation_segments), encoding="utf-8") | |
| return str(pure_path), str(bilingual_path) | |
| from uuid import uuid4 | |
| from langchain_text_splitters.latex import LatexTextSplitter | |
| from typing import Union | |
| _DOLLAR_RE = re.compile(r'(?<!\\)(?:\\\\)*\$') | |
| def count_dollar_signs(text: str) -> int: | |
| r""" | |
| 统计 LaTeX 文本中 **未被转义** 的 '$' 数量。 | |
| 规则 | |
| ---- | |
| - `\$` ➜ **不计入**(单反斜杠转义) | |
| - `\\$` ➜ 计入(两个反斜杠→实际输出一个 \,$ 未被转义) | |
| """ | |
| return len(_DOLLAR_RE.findall(text)) | |
| def _strip_linebreak_backslash(block: str) -> str: | |
| r""" | |
| 若区块不含 ``\begin``,根据四条规则清理多余的 ``\\``: | |
| 1. “…X\\\n” → “…X” (X 不是换行 / . / 。;后面是换行) | |
| 2. “…X\\Y” → “…XY” (X 同上;后面不是换行) | |
| 3. “\n\\Y” → “Y” (前面是换行;后面不是换行) | |
| 4. “…[.。]\\” → “…[.。]\n”(前面是 . 或 。) | |
| """ | |
| if r"\begin" in block: | |
| return block | |
| block = re.sub(r"([^\n.。])\\\\\n", r"\1", block) | |
| block = re.sub(r"\n\\\\([^\n])", r"\1", block) | |
| block = re.sub(r"([.。])\\\\", r"\1\n\n", block) | |
| block = re.sub(r"([^\n.。])\\\\(?!\n)", r"\1", block) | |
| return block | |
| def split_by_double_newline(text: str) -> list[str]: | |
| """ | |
| 根据两个及两个以上的换行符分段。 | |
| Parameters | |
| ---------- | |
| text : str | |
| 待分段的原始字符串。 | |
| Returns | |
| ------- | |
| List[str] | |
| 按段落顺序组成的列表,空段落被忽略。 | |
| """ | |
| # \r?\n 兼容 Windows/Unix 行尾;{2,} 表示至少两次 | |
| parts = re.split(r'(?:\r?\n){2,}', text.strip()) | |
| # 过滤可能出现的空串 | |
| return [p for p in parts if p] | |
| def count_gemini_tokens(prompt: str, model: str = "gemini-1.5-pro-002") -> int: | |
| from vertexai.preview.tokenization import get_tokenizer_for_model | |
| tokenizer = get_tokenizer_for_model(model) | |
| response = tokenizer.count_tokens(prompt) | |
| return response.total_tokens | |
| def token_mark(blocks:list[str]) -> list[int]: | |
| over_limit_count: list[int] = [] | |
| for i in range(len(blocks)): | |
| block = blocks[i] | |
| if count_gemini_tokens(block) > 1000: | |
| print(f"Block {i} > 1000 tokens.") | |
| over_limit_count.append(i) | |
| return over_limit_count | |
| def mark_split(string) -> list[str]: | |
| splitter = LatexTextSplitter(chunk_size=700, chunk_overlap=0) | |
| chunks = splitter.split_text(string) | |
| return chunks | |
| def token_1000_split(blocks: list[str]) -> list[str]: | |
| """ | |
| 对输入的字符串列表进行分块处理: | |
| • 若某块 token 数 ≥ 1000,则用 LatexTextSplitter(chunk_size=700, chunk_overlap=0) | |
| 将其拆分成若干子块,并把这些子块依次插入到结果列表中。 | |
| • 否则直接保留原块。 | |
| 返回一个新的 list[str],不修改传入的 blocks。 | |
| """ | |
| splitter = LatexTextSplitter(chunk_size=700, chunk_overlap=0) | |
| result: list[str] = [] | |
| for block in blocks: | |
| if count_gemini_tokens(block) >= 1000: | |
| # 拆分并追加所有子块 | |
| result.extend(splitter.split_text(block)) | |
| else: | |
| # 保留原块 | |
| result.append(block) | |
| return result | |
| def latex2txt_blocks( | |
| latex_txt_path: Union[str, Path, None] = None, | |
| *, | |
| output_path: Union[str, Path, None] = None, | |
| chunk_size: int = 20_000, | |
| chunk_overlap: int = 0, | |
| ) -> str: | |
| r""" | |
| 处理流程 | |
| ---------- | |
| 1. 读入文件,仅保留 \begin{document}…\end{document}(若无则全文)。 | |
| 2. 先按 **空行 ``\n\n``** 粗分成若干 `raw_block`。 | |
| 3. 对每个 `raw_block` 再用 ``LatexTextSplitter`` 细分为 `fine_blocks`: | |
| - 校验:细分前后 ``count_dollar_signs`` 值必须一致,否则抛 `ValueError`。 | |
| 4. 遍历 `fine_blocks`: | |
| - 若区块 **不含 ``\begin``**,应用三条“删除多余 \\\\”规则(见 `_strip_linebreak_backslash`)。 | |
| 5. 将处理后的 `fine_blocks` 用 ``\n\n`` 连接为 `txt` 并执行 6.1–6.4 后处理。 | |
| 6. 写入最终 TXT,返回其绝对路径。 | |
| """ | |
| # ---------- 1. 提取 document 主体 ---------- | |
| latex_txt_path = Path(latex_txt_path).expanduser().resolve() | |
| content = latex_txt_path.read_text(encoding="utf-8") | |
| doc_match = re.search( | |
| r"\\begin\{document}(.*?)\\end\{document}", | |
| content, | |
| flags=re.DOTALL | re.IGNORECASE, | |
| ) | |
| content = doc_match.group(1) if doc_match else content | |
| # ---------- 2 & 3. 粗分 + 细分 + 校验 ---------- | |
| splitter = LatexTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap) | |
| fine_blocks: list[str] = [] | |
| for raw_block in split_by_double_newline(content): | |
| raw_block = raw_block.strip() | |
| if not raw_block: | |
| continue | |
| split_blocks = [b.strip() for b in splitter.split_text(raw_block) if b.strip()] | |
| # 校验 $ 数量 | |
| before = count_dollar_signs(raw_block) | |
| after = sum(count_dollar_signs(b) for b in split_blocks) | |
| if before != after: | |
| raise ValueError( | |
| "LatexTextSplitter 改变了 `$` 数量!\n\n" | |
| f"===== 原始块({before} 个 $) =====\n{raw_block}\n\n" | |
| f"===== 细分合并({after} 个 $) =====\n" | |
| + "\n\n==== 分块分隔 ====\n\n".join(split_blocks) | |
| ) | |
| fine_blocks.extend(split_blocks) | |
| # ---------- 4. 删除多余的 \\\\ ---------- | |
| fine_blocks = [_strip_linebreak_backslash(b) for b in fine_blocks] | |
| txt = "\n\n".join(fine_blocks) | |
| # ---------- 6.1–6.4 后处理 ---------- | |
| txt = re.sub( | |
| r"(\\begin\{tabular\}\{[^}]*})", | |
| r"\\resizebox{\\textwidth}{!}{\n\1", | |
| txt, | |
| ) | |
| txt = re.sub(r"(\\end\{tabular})", r"\1\n}", txt) | |
| txt = re.sub(r"max\s*width=\\textwidth", r"width =\\textwidth", txt) | |
| txt = re.sub(r"\n\n(\$\$)", r"\n\1", txt) | |
| txt = re.sub(r"\[0pt]", "", txt) | |
| txt = re.sub(r"`", "", txt) # ← 新增:去掉所有反引号 | |
| blocks = split_by_double_newline(txt) | |
| blocks = token_1000_split(blocks) | |
| txt = "\n\n".join(blocks) | |
| # ---------- 7. 写入最终稿 ---------- | |
| if output_path is None: | |
| output_path = Path.cwd() / f"latex_blocks_{uuid4().hex[:8]}.txt" | |
| output_path = Path(output_path).expanduser().resolve() | |
| output_path.write_text(txt, encoding="utf-8") | |
| return str(output_path) | |
| # ============================================================================== | |
| # 运行应用 | |
| # ============================================================================== | |
| if __name__ == "__main__": | |
| app = create_gradio_app() | |
| app.launch(server_name='0.0.0.0', share=True) | |