import json
import pandas as pd
from pandas import ExcelWriter
import openpyxl
from html import unescape
import re
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.utils import get_column_letter
import requests
import os
import time
import sys
from datetime import datetime
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox, QStatusBar
import logging
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.chart import XL_CHART_TYPE
from pptx.dml.color import RGBColor
from pptx.chart.data import ChartData
import io
import warnings
from typing import List, Dict, Any, Optional
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("ptl_tool.log", mode='w', encoding='utf-8'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger("PTL_Tool")
# 加载配置文件
def load_config():
"""加载配置文件"""
config_path = "config.json"
default_config = {
"API": {
"PROD_URL": "https://lims.se.com/STARLIMS11.prod/RESTServices.REST_API.Api_clab_testplan_v1.lims",
"QUAL_URL": "https://qual.lims.se.com/STARLIMS11.qual/RESTServices.REST_API.Api_clab_testplan_v1.lims",
"USER": "API_CN",
"PASSWORD": "YLWT",
"COOKIE": "ASP.NET_SessionId=nn30yrczlmvf31be5kvynwpq"
},
"TEMPLATE_PATHS": [
os.path.join("api_responses", "Validation Plan Preface.xlsx"),
"Validation Plan Preface.xlsx",
os.path.join(os.path.dirname(__file__), "Validation Plan Preface.xlsx")
],
"MAX_PROCESS_ROWS": 10000,
"PPT_MAX_DISPLAY_ITEMS": 10
}
if os.path.exists(config_path):
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
for key, value in default_config.items():
if key not in config:
config[key] = value
return config
except Exception as e:
logger.warning(f"加载配置文件失败,使用默认配置: {e}")
return default_config
# 全局配置
CONFIG = load_config()
MAX_PROCESS_ROWS = CONFIG["MAX_PROCESS_ROWS"]
def extract_text(html_content: Optional[str]) -> str:
"""从HTML内容中提取纯文本信息,保留换行符"""
if not isinstance(html_content, str) or html_content.lower() == "null" or html_content.strip() == "":
return "N/A"
def process_line(line: str) -> str:
line = line.replace('<br>', '\n').replace('<br />', '\n').replace('<br/>', '\n')
line = re.sub(r'<[^>]*>', '', line)
line = unescape(line)
return re.sub(r'[ \t]+', ' ', line).strip()
if len(html_content) > 10000:
result = []
for chunk in [html_content[i:i + 1000] for i in range(0, len(html_content), 1000)]:
result.append(process_line(chunk))
return ' '.join(result)
else:
return process_line(html_content)
def process_comments(html_comments: str) -> dict:
"""处理特定格式的试验申请注释信息"""
clean_text = extract_text(html_comments)
lines = [line.strip() for line in clean_text.split('\n') if line.strip()]
result = {}
current_section = None
max_lines = min(len(lines), 1000)
for i in range(max_lines):
line = lines[i]
if ':' in line:
parts = line.split(':', 1)
if len(parts) == 2:
key, value = parts
key = key.strip()
value = value.strip()
if '(' in value and ')' in value:
try:
main_value, explanation = value.split('(', 1)
result[key] = main_value.strip()
result[f"{key}_说明"] = explanation.replace(')', '').strip()
except ValueError:
result[key] = value
else:
result[key] = value
else:
if current_section and line:
result[current_section] += ' ' + line
elif line:
current_section = f"备注_{len([k for k in result if k.startswith('备注_')]) + 1}"
result[current_section] = line
return result
def create_template_based_file(df: pd.DataFrame, output_file: str, test_plan_id: str) -> bool:
"""保留模板所有内容,在后面新增第三个工作表写入数据"""
logger.info(f"开始基于模板创建文件:{output_file}")
# 查找模板文件
template_path = None
for path in CONFIG["TEMPLATE_PATHS"]:
if os.path.exists(path):
template_path = path
logger.info(f"找到模板文件:{template_path}")
break
# 加载模板或创建新工作簿
if template_path and os.path.exists(template_path):
try:
workbook = openpyxl.load_workbook(template_path)
logger.info(f"模板包含工作表:{workbook.sheetnames}")
except Exception as e:
logger.error(f"加载模板失败,创建新工作簿:{e}")
workbook = openpyxl.Workbook()
else:
logger.warning("未找到Validation Plan模板,创建新工作簿")
workbook = openpyxl.Workbook()
try:
sheet_name = "Validation Plan"
# 删除已存在的同名工作表
if sheet_name in workbook.sheetnames:
logger.warning(f"工作表 {sheet_name} 已存在,删除后重新创建")
workbook.remove(workbook[sheet_name])
# 创建新工作表
worksheet = workbook.create_sheet(title=sheet_name)
# 确保新工作表是第3个
while len(workbook.sheetnames) < 3:
temp_sheet_name = f"Temp_Sheet_{len(workbook.sheetnames) + 1}"
workbook.create_sheet(title=temp_sheet_name)
logger.info(f"补充空工作表 {temp_sheet_name}")
# 重新排序
sheet_names = workbook.sheetnames
if sheet_name in sheet_names:
sheet_names.remove(sheet_name)
sheet_names.insert(2, sheet_name)
workbook._sheets = [workbook[sheet] for sheet in sheet_names]
logger.info(f"最终工作表顺序:{workbook.sheetnames}")
# 写入表头
for col_num, header in enumerate(df.columns, 1):
cell = worksheet.cell(row=1, column=col_num, value=header)
cell.font = Font(name='Arial', size=10, bold=True)
cell.fill = PatternFill(start_color='D7E4BC', end_color='D7E4BC', fill_type='solid')
cell.alignment = Alignment(horizontal='left', vertical='center', wrap_text=True)
# 分批写入数据,优化内存
batch_size = 1000
total_rows = len(df)
for start_idx in range(0, total_rows, batch_size):
end_idx = min(start_idx + batch_size, total_rows)
batch_df = df.iloc[start_idx:end_idx]
for row_num, (_, row) in enumerate(batch_df.iterrows(), start_idx + 2):
for col_num, value in enumerate(row, 1):
# 处理长文本限制
if isinstance(value, str) and len(value) > 32767:
value = value[:32767]
cell = worksheet.cell(row=row_num, column=col_num, value=value)
cell.font = Font(name='Arial', size=10)
cell.alignment = Alignment(horizontal='left', vertical='center', wrap_text=True)
logger.info(f"已写入 {end_idx}/{total_rows} 行数据")
# 设置列宽和边框
thin_border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
for col_num in range(1, len(df.columns) + 1):
column_letter = get_column_letter(col_num)
col_name = df.columns[col_num - 1]
# 计算列宽
max_length = max(df[col_name].astype(str).map(len).max(), len(col_name)) + 2
if col_name in ['Test Details', 'Sample Description', 'Referential']:
max_length = max(max_length, 30)
elif col_name == 'Comment':
max_length = max(max_length, 25)
elif col_name == 'Sample ID':
max_length = max(max_length, 20)
elif col_name == 'Title':
max_length = max(max_length, 25)
max_length = min(max_length, 50)
worksheet.column_dimensions[column_letter].width = max_length
# 设置边框
for row_num in range(1, total_rows + 2):
worksheet.cell(row=row_num, column=col_num).border = thin_border
# 调整行高
for row_num in range(2, total_rows + 2):
worksheet.row_dimensions[row_num].height = 30
# 保存文件
workbook.save(output_file)
logger.info(f"成功生成文件:{output_file}")
return True
except Exception as e:
logger.error(f"生成文件失败:{str(e)}", exc_info=True)
raise
def api_to_excel(api_response: Any, output_file: Optional[str] = None, test_plan_id: Optional[str] = None) -> bool:
"""将API响应数据转换为Excel文件(修复批量JSON解析和TESTSPECS字段查找)"""
logger.info("开始转换数据到Excel...")
# 解析API响应
if isinstance(api_response, str):
try:
data = json.loads(api_response)
except json.JSONDecodeError as e:
logger.error(f"JSON解析错误: {e}")
QMessageBox.critical(None, "错误", f"无法解析API数据:{str(e)}\n请确保数据是有效的JSON格式。")
return False
else:
data = api_response
# 修复:重新定义批量数据判断逻辑
# 批量数据格式:[{test_plan_id, data, timestamp}, ...]
is_batch_format = isinstance(data, list) and len(data) > 0 and all(
isinstance(item, dict) and 'data' in item and 'test_plan_id' in item
for item in data
)
merged_data = []
used_test_plan_id = "Combined"
if is_batch_format:
logger.info(f"检测到批量合并数据,共 {len(data)} 个Test Plan")
for item_idx, item in enumerate(data):
try:
plan_id = item.get('test_plan_id', f'Unknown_{item_idx + 1}')
plan_data = item.get('data', {})
# 查找TESTSPECS的可能位置
specs = None
if 'TESTSPECS' in plan_data:
specs = plan_data['TESTSPECS']
elif 'TestSpecs' in plan_data: # 兼容大小写
specs = plan_data['TestSpecs']
elif 'testspecs' in plan_data:
specs = plan_data['testspecs']
elif isinstance(plan_data, dict) and len(plan_data) == 1:
# 处理可能的嵌套结构
nested_key = next(iter(plan_data.keys()))
nested_data = plan_data[nested_key]
if isinstance(nested_data, dict) and 'TESTSPECS' in nested_data:
specs = nested_data['TESTSPECS']
if not specs or not isinstance(specs, list):
logger.warning(f"Test Plan {plan_id} 中未找到有效的TESTSPECS列表,跳过该数据")
continue
max_specs = min(len(specs), MAX_PROCESS_ROWS)
logger.info(f"处理Test Plan: {plan_id}, 测试规格数: {max_specs}")
for spec_idx, spec in enumerate(specs[:max_specs]):
if not isinstance(spec, dict):
logger.warning(f"跳过无效的测试规格数据(索引{spec_idx})")
continue
test_spec_id = spec.get('TESTSPEC_ID', 'N/A')
samples = spec.get('SAMPLES', [])
standards = [std.get('REFERENCE', 'N/A') for std in spec.get('STANDARDS', [])]
referential = "\n".join(standards) if standards else "N/A"
sample_count = spec.get('SAMPLE_COUNT', len(samples))
sample_ids = []
sample_descriptions = []
sample_maturities = []
if samples:
for sample in samples:
sample_ids.append(sample.get('SAMPLEID', 'N/A'))
sample_descriptions.append(extract_text(sample.get('DESCRIPTION', 'N/A')))
sample_maturities.append(sample.get('MATURITY', 'N/A'))
sample_id_text = "\n".join(sample_ids) if sample_ids else "N/A"
sample_description_text = "\n".join(sample_descriptions) if sample_descriptions else "N/A"
sample_maturity_text = "\n".join(sample_maturities) if sample_maturities else "N/A"
test_details = spec.get('TEST_DETAILS', 'N/A')
test_details = test_details.replace('<br>', '\n').replace('-->', '->')
test_details = extract_text(test_details)
comments = spec.get('COMMENTS', 'N/A')
if comments and comments.lower() != "null":
comments = extract_text(comments)
row = {
'Test Plan': plan_id,
'Test Spec ID': test_spec_id,
'Status': spec.get('STATUS', 'N/A'),
'Global Status': spec.get('GLOBAL_STATUS', 'N/A'),
'Title': spec.get('TITLE', 'N/A'),
'Referential': referential,
'Acceptance Criteria': spec.get('ACCEPTANCE_CRITERIA', 'N/A'),
'Test Details': test_details,
'Comment': comments,
'Sample Number': sample_count,
'Sample ID': sample_id_text,
'Sample Description': sample_description_text,
'Sample Maturity': sample_maturity_text,
'Workload': spec.get('WORKLOAD', 'N/A')
}
merged_data.append(row)
except Exception as e:
logger.error(f"处理Test Plan {item.get('test_plan_id', 'Unknown')} 时出错: {e}", exc_info=True)
continue
else:
# 单个Test Plan数据处理
logger.info("检测到单个Test Plan数据")
# 查找TESTSPECS的可能位置
specs = None
if 'TESTSPECS' in data:
specs = data['TESTSPECS']
elif 'TestSpecs' in data:
specs = data['TestSpecs']
elif 'testspecs' in data:
specs = data['testspecs']
elif isinstance(data, dict) and len(data) == 1:
nested_key = next(iter(data.keys()))
nested_data = data[nested_key]
if isinstance(nested_data, dict) and 'TESTSPECS' in nested_data:
specs = nested_data['TESTSPECS']
if not specs or not isinstance(specs, list):
logger.error(f"API数据格式错误,在以下位置均未找到TESTSPECS字段:")
logger.error(f"- 顶层数据: {list(data.keys())[:10]}")
if isinstance(data, dict) and len(data) == 1:
nested_key = next(iter(data.keys()))
logger.error(
f"- 嵌套数据[{nested_key}]: {list(data[nested_key].keys())[:10] if isinstance(data[nested_key], dict) else type(data[nested_key])}")
QMessageBox.critical(None, "错误",
"API数据格式错误,未找到TESTSPECS字段。\n"
"请检查JSON文件结构,确保包含TESTSPECS列表。\n"
"批量数据格式应为:[{test_plan_id, data: {TESTSPECS: [...]}, timestamp}, ...]")
return False
# 获取Test Plan ID
if test_plan_id:
used_test_plan_id = test_plan_id
else:
used_test_plan_id = data.get('TESTPLAN_ID', data.get('TestPlanId', 'Unknown_Test_Plan'))
used_test_plan_id = re.sub(r'[\\/:*?"<>|]', '_', used_test_plan_id)
max_specs = min(len(specs), MAX_PROCESS_ROWS)
logger.info(f"共发现 {len(specs)} 个测试规格,将处理前 {max_specs} 个")
for i in range(max_specs):
spec = specs[i]
if not isinstance(spec, dict):
logger.warning(f"跳过无效的测试规格数据(索引{i})")
continue
if i % 100 == 0:
logger.info(f"处理测试规格 {i + 1}/{max_specs}")
test_spec_id = spec.get('TESTSPEC_ID', 'N/A')
samples = spec.get('SAMPLES', [])
standards = [std.get('REFERENCE', 'N/A') for std in spec.get('STANDARDS', [])]
referential = "\n".join(standards) if standards else "N/A"
sample_count = spec.get('SAMPLE_COUNT', len(samples))
sample_ids = []
sample_descriptions = []
sample_maturities = []
if samples:
for sample in samples:
sample_ids.append(sample.get('SAMPLEID', 'N/A'))
sample_descriptions.append(extract_text(sample.get('DESCRIPTION', 'N/A')))
sample_maturities.append(sample.get('MATURITY', 'N/A'))
sample_id_text = "\n".join(sample_ids) if sample_ids else "N/A"
sample_description_text = "\n".join(sample_descriptions) if sample_descriptions else "N/A"
sample_maturity_text = "\n".join(sample_maturities) if sample_maturities else "N/A"
test_details = spec.get('TEST_DETAILS', 'N/A')
test_details = test_details.replace('<br>', '\n').replace('-->', '->')
test_details = extract_text(test_details)
comments = spec.get('COMMENTS', 'N/A')
if comments and comments.lower() != "null":
comments = extract_text(comments)
row = {
'Test Plan': used_test_plan_id,
'Test Spec ID': test_spec_id,
'Status': spec.get('STATUS', 'N/A'),
'Global Status': spec.get('GLOBAL_STATUS', 'N/A'),
'Title': spec.get('TITLE', 'N/A'),
'Referential': referential,
'Acceptance Criteria': spec.get('ACCEPTANCE_CRITERIA', 'N/A'),
'Test Details': test_details,
'Comment': comments,
'Sample Number': sample_count,
'Sample ID': sample_id_text,
'Sample Description': sample_description_text,
'Sample Maturity': sample_maturity_text,
'Workload': spec.get('WORKLOAD', 'N/A')
}
merged_data.append(row)
# 检查是否有有效数据
if not merged_data:
logger.error("未找到任何有效的测试规格数据")
QMessageBox.warning(None, "警告", "JSON文件中未找到任何有效的测试规格数据,请检查数据质量。")
return False
# 创建DataFrame
df = pd.DataFrame(merged_data)
# 定义列顺序
column_order = [
'Test Plan', 'Test Spec ID', 'Status', 'Global Status', 'Title',
'Referential', 'Acceptance Criteria', 'Test Details', 'Comment',
'Sample Number', 'Sample ID', 'Sample Description', 'Sample Maturity', 'Workload'
]
# 确保所有列都存在
for col in column_order:
if col not in df.columns:
df[col] = 'N/A'
df = df[column_order]
# 生成输出文件名
if not output_file:
output_file = f"{used_test_plan_id}_PTL03.xlsx"
# 基于模板创建文件
try:
if create_template_based_file(df, output_file, used_test_plan_id):
logger.info(f"成功生成文件:{output_file}")
QMessageBox.information(None, "成功",
f"文件已保存:{os.path.basename(output_file)}\n\n"
f"已保留模板所有内容,数据已写入第3个工作表\n"
f"共处理 {len(df)} 条测试规格数据")
return True
else:
return False
except Exception as e:
logger.error(f"写入Excel文件时发生异常:{str(e)}", exc_info=True)
QMessageBox.critical(None, "错误", f"写入Excel文件时发生异常:{str(e)}")
return False
def load_api_data_from_file(file_path: str) -> Optional[Any]:
"""从JSON文件加载API数据(增强日志输出)"""
try:
logger.info(f"从文件加载数据: {file_path}")
with open(file_path, 'r', encoding='utf-8') as file:
# 读取文件内容
file_content = file.read().strip()
if not file_content:
logger.error("文件为空")
QMessageBox.warning(None, "错误", "选择的JSON文件为空")
return None
# 尝试解析JSON
data = json.loads(file_content)
# 增强日志:输出数据结构详情
if isinstance(data, list):
logger.info(f"加载到批量数据,共 {len(data)} 项")
if len(data) > 0:
first_item = data[0]
logger.info(f"第一项数据包含字段: {list(first_item.keys())}")
if 'data' in first_item:
data_fields = list(first_item['data'].keys())[:10] # 只显示前10个字段
logger.info(f"第一项data字段包含: {data_fields}...")
if 'TESTSPECS' in first_item['data']:
logger.info(f"找到TESTSPECS字段,共 {len(first_item['data']['TESTSPECS'])} 个测试规格")
else:
logger.warning(f"第一项data字段中未找到TESTSPECS")
elif isinstance(data, dict):
logger.info(f"加载到单个数据,包含字段: {list(data.keys())[:10]}...")
if 'TESTSPECS' in data:
logger.info(f"找到TESTSPECS字段,共 {len(data['TESTSPECS'])} 个测试规格")
else:
logger.warning(f"未找到TESTSPECS字段")
else:
logger.warning(f"加载到非预期数据类型: {type(data)}")
return data
except FileNotFoundError as e:
logger.error(f"文件未找到: {file_path}")
QMessageBox.warning(None, "错误", f"找不到文件 '{os.path.basename(file_path)}'")
return None
except json.JSONDecodeError as e:
logger.error(f"JSON解析错误: {file_path}, 位置: {e.pos}, 原因: {e.msg}")
QMessageBox.warning(None, "错误",
f"文件 '{os.path.basename(file_path)}' 不是有效的JSON格式\n错误位置: {e.pos}\n原因: {e.msg}")
return None
except Exception as e:
logger.error(f"读取文件时发生异常:{str(e)}", exc_info=True)
QMessageBox.warning(None, "错误", f"读取文件时发生异常:{str(e)}")
return None
def fetch_testplan_metadata(test_plan_id: str, version: int = 1, use_prod: bool = True) -> dict:
"""从STARLIMS API获取测试计划的元数据(移除tenacity重试,改用简单重试)"""
logger.info(f"获取测试计划元数据: {test_plan_id}, 版本: {version}")
base_url = CONFIG["API"]["PROD_URL"] if use_prod else CONFIG["API"]["QUAL_URL"]
url = f"{base_url}?TestPlanId={test_plan_id}&Version={version}"
headers = {
'Content-Type': 'application/json',
'STARLIMSUser': CONFIG["API"]["USER"],
'STARLIMSPass': CONFIG["API"]["PASSWORD"],
'Cookie': CONFIG["API"]["COOKIE"]
}
max_attempts = 3
attempt = 0
while attempt < max_attempts:
try:
warnings.filterwarnings('ignore', message='Unverified HTTPS request')
response = requests.get(url, headers=headers, verify=False, timeout=30)
response.raise_for_status()
if len(response.content) > 10 * 1024 * 1024:
logger.info(f"处理大型响应数据({len(response.content) / 1024 / 1024:.1f}MB)")
return response.json()
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
attempt += 1
logger.warning(f"请求失败(尝试 {attempt}/{max_attempts}): {e}")
if attempt < max_attempts:
time.sleep(2 * attempt) # 指数退避等待
else:
logger.error(f"多次请求失败: {e}", exc_info=True)
raise
except requests.exceptions.RequestException as e:
logger.error(f"请求错误: {e}", exc_info=True)
error_msg = f"获取 {test_plan_id} 数据失败:{str(e)}"
if "401" in str(e) or "Unauthorized" in str(e):
QMessageBox.critical(None, "认证错误", "API认证失败,请检查配置")
raise
QMessageBox.warning(None, "请求错误", error_msg)
raise
except ValueError as e:
logger.error(f"JSON解析错误: {e}", exc_info=True)
QMessageBox.warning(None, "解析错误", f"解析 {test_plan_id} 数据失败:{str(e)}")
raise
def create_json_from_test_plan_id():
"""从三个下拉菜单+手动输入框获取Test Plan ID,创建合并JSON和Excel文件"""
logger.info("开始从Test Plan ID创建合并JSON和Excel文件")
try:
test_plan_ids = []
# 从下拉菜单获取
for combo in [ui.comboBox_1, ui.comboBox_2, ui.comboBox_3]:
plan_text = combo.currentText().strip()
if plan_text:
plan_id = plan_text.split('(')[0].strip()
test_plan_ids.append(plan_id)
# 从手动输入框获取
manual_plan_text = ui.textEdit_1.toPlainText().strip()
if manual_plan_text:
manual_ids = [id.strip() for id in manual_plan_text.split(',') if id.strip()]
test_plan_ids.extend(manual_ids)
# 去重和过滤
test_plan_ids = list(set([id for id in test_plan_ids if id]))
if not test_plan_ids:
QMessageBox.critical(None, "输入错误", "请至少选择一个Test Plan ID或手动输入有效ID")
return
# 禁用按钮
ui.pushButton_JSON.setEnabled(False)
ui.statusbar.showMessage(f"正在获取 {len(test_plan_ids)} 个测试计划的数据...")
all_test_plan_data = []
success_count = 0
error_count = 0
# 生成输出文件名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
combined_base_name = f"Combined_{'_'.join(test_plan_ids[:3])}_{timestamp}" # 限制文件名长度
output_dir = "api_responses"
os.makedirs(output_dir, exist_ok=True)
combined_json_file = os.path.join(output_dir, f"{combined_base_name}.json")
combined_excel_file = os.path.join(output_dir, f"{combined_base_name}_PTL_03.xlsx")
combined_df = pd.DataFrame()
# 逐个处理
for idx, test_plan_id in enumerate(test_plan_ids):
try:
ui.statusbar.showMessage(f"正在获取 {test_plan_id} 的数据... ({idx + 1}/{len(test_plan_ids)})")
data = fetch_testplan_metadata(test_plan_id)
all_test_plan_data.append({
"test_plan_id": test_plan_id,
"data": data,
"timestamp": datetime.now().isoformat()
})
success_count += 1
# 处理数据用于Excel
specs = data.get('TESTSPECS', [])
if not specs:
logger.warning(f"Test Plan {test_plan_id} 中未找到TESTSPECS字段")
continue
max_specs = min(len(specs), MAX_PROCESS_ROWS)
merged_data = []
for i in range(max_specs):
spec = specs[i]
test_spec_id = spec.get('TESTSPEC_ID', 'N/A')
samples = spec.get('SAMPLES', [])
standards = [std.get('REFERENCE', 'N/A') for std in spec.get('STANDARDS', [])]
referential = "\n".join(standards) if standards else "N/A"
sample_count = spec.get('SAMPLE_COUNT', len(samples))
sample_ids = []
sample_descriptions = []
sample_maturities = []
if samples:
for sample in samples:
sample_ids.append(sample.get('SAMPLEID', 'N/A'))
sample_descriptions.append(extract_text(sample.get('DESCRIPTION', 'N/A')))
sample_maturities.append(sample.get('MATURITY', 'N/A'))
sample_id_text = "\n".join(sample_ids) if sample_ids else "N/A"
sample_description_text = "\n".join(sample_descriptions) if sample_descriptions else "N/A"
sample_maturity_text = "\n".join(sample_maturities) if sample_maturities else "N/A"
test_details = spec.get('TEST_DETAILS', 'N/A')
test_details = test_details.replace('<br>', '\n').replace('-->', '->')
test_details = extract_text(test_details)
comments = spec.get('COMMENTS', 'N/A')
if comments and comments.lower() != "null":
comments = extract_text(comments)
row = {
'Test Plan': test_plan_id,
'Test Spec ID': test_spec_id,
'Status': spec.get('STATUS', 'N/A'),
'Global Status': spec.get('GLOBAL_STATUS', 'N/A'),
'Title': spec.get('TITLE', 'N/A'),
'Referential': referential,
'Acceptance Criteria': spec.get('ACCEPTANCE_CRITERIA', 'N/A'),
'Test Details': test_details,
'Comment': comments,
'Sample Number': sample_count,
'Sample ID': sample_id_text,
'Sample Description': sample_description_text,
'Sample Maturity': sample_maturity_text,
'Workload': spec.get('WORKLOAD', 'N/A')
}
merged_data.append(row)
single_df = pd.DataFrame(merged_data)
combined_df = pd.concat([combined_df, single_df], ignore_index=True)
except Exception as e:
logger.error(f"获取 {test_plan_id} 数据时发生异常: {str(e)}", exc_info=True)
error_count += 1
QMessageBox.warning(
None,
"获取数据失败",
f"获取 {test_plan_id} 数据时发生错误: {str(e)}\n将继续处理其他Test Plan ID。"
)
# 保存合并JSON
try:
with open(combined_json_file, 'w', encoding='utf-8') as f:
json.dump(all_test_plan_data, f, ensure_ascii=False, indent=2)
logger.info(f"合并的JSON文件已保存: {combined_json_file}")
except Exception as e:
logger.error(f"创建合并JSON文件时出错: {str(e)}", exc_info=True)
QMessageBox.warning(None, "合并JSON失败", f"创建合并JSON文件时发生错误: {str(e)}")
# 保存合并Excel
try:
if not combined_df.empty:
combined_test_plan_id = f"Combined_{'_'.join(test_plan_ids[:3])}"
if create_template_based_file(combined_df, combined_excel_file, combined_test_plan_id):
logger.info(f"合并的Excel文件已保存: {combined_excel_file}")
else:
logger.warning("没有可保存的Excel数据")
QMessageBox.warning(None, "警告", "没有找到有效的测试规格数据,无法生成Excel文件")
except Exception as e:
logger.error(f"创建合并Excel文件时出错: {str(e)}", exc_info=True)
QMessageBox.warning(None, "合并Excel失败", f"创建合并Excel文件时发生错误: {str(e)}")
# 显示结果
result_msg = (f"处理完成!\n"
f"成功获取 {success_count} 个Test Plan ID的数据\n"
f"获取失败 {error_count} 个Test Plan ID\n\n"
f"合并的JSON文件:{os.path.basename(combined_json_file)}\n"
f"合并的Excel文件:{os.path.basename(combined_excel_file) if os.path.exists(combined_excel_file) else '未生成'}\n"
f"文件保存路径:{output_dir}")
ui.statusbar.showMessage("处理完成")
QMessageBox.information(None, "处理结果", result_msg)
except Exception as e:
logger.error(f"创建合并文件时发生异常: {str(e)}", exc_info=True)
QMessageBox.critical(None, "错误", f"获取数据时发生异常: {str(e)}")
ui.statusbar.showMessage("获取数据失败")
finally:
ui.pushButton_JSON.setEnabled(True)
def create_excel_from_json(ptl_type="PTL_03"):
"""修复后的从JSON创建Excel函数"""
logger.info(f"开始从JSON文件创建{ptl_type}格式Excel")
try:
# 选择文件
file_path, _ = QFileDialog.getOpenFileName(
None,
f"选择JSON文件 - {ptl_type}",
"",
"JSON文件 (*.json);;所有文件 (*)"
)
if not file_path:
logger.info("用户取消了文件选择")
return
# 禁用按钮
ui.pushButton_EXCEL03.setEnabled(False)
ui.statusbar.showMessage(f"正在加载并处理{ptl_type}文件...")
# 加载JSON数据(增强日志)
data = load_api_data_from_file(file_path)
if data is None:
ui.statusbar.showMessage("加载JSON文件失败")
return
# 生成Excel文件名
excel_file = f"{os.path.splitext(file_path)[0]}_{ptl_type}.xlsx"
# 转换为Excel(修复后的解析逻辑)
if api_to_excel(data, excel_file):
ui.statusbar.showMessage(f"{ptl_type}文件处理完成")
logger.info(f"{ptl_type}文件已保存: {excel_file}")
else:
ui.statusbar.showMessage(f"{ptl_type}文件转换失败")
logger.error(f"{ptl_type}文件转换失败")
except Exception as e:
logger.error(f"创建{ptl_type}文件时发生异常: {str(e)}", exc_info=True)
QMessageBox.showerror("错误", f"处理{ptl_type}文件时发生异常: {str(e)}")
ui.statusbar.showMessage(f"{ptl_type}文件处理失败")
finally:
ui.pushButton_EXCEL03.setEnabled(True)
def create_slides_from_ptl03():
"""生成包含验证结果的幻灯片"""
logger.info("开始创建幻灯片")
try:
file_path, _ = QFileDialog.getOpenFileName(
None,
"选择PTL_03文件",
"",
"Excel文件 (*.xlsx);;所有文件 (*)"
)
if not file_path:
logger.info("用户取消了文件选择")
return
ui.pushButton_Slides.setEnabled(False)
ui.statusbar.showMessage("正在创建幻灯片...")
try:
excel_file = pd.ExcelFile(file_path)
sheet_names = excel_file.sheet_names
logger.info(f"Excel文件包含工作表:{sheet_names}")
if len(sheet_names) < 3:
df = pd.read_excel(file_path, sheet_name=-1)
logger.warning(f"工作表不足3个,读取最后一个工作表:{sheet_names[-1]}")
QMessageBox.warning(None, "工作表兼容处理",
f"Excel文件只有 {len(sheet_names)} 个工作表\n已自动读取最后一个工作表的数据")
else:
df = pd.read_excel(file_path, sheet_name=2)
logger.info(f"成功读取第3个工作表:{sheet_names[2]},共 {len(df)} 行数据")
except Exception as e:
logger.error(f"读取Excel文件失败: {str(e)}", exc_info=True)
QMessageBox.critical(None, "错误", f"读取Excel文件失败:{str(e)}")
return
# 数据验证
required_cols = ['Global Status', 'Title']
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
raise ValueError(f"Excel文件缺少必要列:{', '.join(missing_cols)}")
# 数据预处理
df['Global Status'] = df['Global Status'].fillna('Unfinished').astype(str).str.strip()
total_test = len(df)
passed_count = len(df[df['Global Status'] == 'Passed'])
failed_count = len(df[df['Global Status'] == 'Non-Compliant'])
ongoing_count = len(df[df['Global Status'] == 'Unfinished'])
failed_titles = df[df['Global Status'] == 'Non-Compliant']['Title'].dropna().tolist()
ongoing_titles = df[df['Global Status'] == 'Unfinished']['Title'].dropna().tolist()
# 创建演示文稿
prs = Presentation()
# 标题页
title_slide_layout = prs.slide_layouts[0]
slide = prs.slides.add_slide(title_slide_layout)
title = slide.shapes.title
title.text = "Summary of Validation"
title_para = title.text_frame.paragraphs[0]
title_para.font.size = Pt(32)
title_para.font.name = 'Arial'
title_para.font.bold = True
subtitle = slide.placeholders[1]
subtitle.text = f"Based on {os.path.basename(file_path)}\n{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
for para in subtitle.text_frame.paragraphs:
para.font.size = Pt(14)
para.font.name = 'Arial'
# 统计+饼图页
slide_layout = prs.slide_layouts[5]
slide = prs.slides.add_slide(slide_layout)
slide_title = slide.shapes.title
slide_title.text = "Validation Results"
slide_title_para = slide_title.text_frame.paragraphs[0]
slide_title_para.font.size = Pt(24)
slide_title_para.font.name = 'Arial'
slide_title_para.font.bold = True
# 饼图
left = Inches(0.5)
top = Inches(2)
width = Inches(3.5)
height = Inches(4.5)
status_counts = df['Global Status'].value_counts().reset_index()
status_counts.columns = ['Status', 'Count']
chart_data = ChartData()
chart_data.categories = list(status_counts['Status'])
chart_data.add_series('Test Status', tuple(status_counts['Count']))
chart = slide.shapes.add_chart(
XL_CHART_TYPE.PIE, left, top, width, height, chart_data
).chart
# 饼图样式
color_map = {
'Passed': RGBColor(155, 187, 89),
'Non-Compliant': RGBColor(192, 80, 77),
'Unfinished': RGBColor(255, 192, 0)
}
for i, status in enumerate(status_counts['Status']):
point = chart.series[0].points[i]
point.format.fill.solid()
point.format.fill.fore_color.rgb = color_map.get(status, RGBColor(128, 100, 162))
label_text = f"{status}: {status_counts.iloc[i]['Count']} ({(status_counts.iloc[i]['Count'] / total_test) * 100:.1f}%)"
font_size = max(4, min(8, 60 // len(label_text)))
label = point.data_label.text_frame.paragraphs[0]
label.text = label_text
label.font.size = Pt(font_size)
label.font.name = 'Arial'
# 统计文字框
text_left = Inches(4.5)
text_top = Inches(1.5)
text_width = Inches(5.5)
text_height = Inches(6.5)
stats_text = f"""Total test item: {total_test}
Passed test item: {passed_count}
Failed test item: {failed_count}
On-going test item: {ongoing_count}
Failed test please see below:
"""
if failed_titles:
for title in failed_titles[:CONFIG["PPT_MAX_DISPLAY_ITEMS"]]:
if len(title) > 80:
title = title[:80] + "..."
stats_text += f"- {title}\n"
if len(failed_titles) > CONFIG["PPT_MAX_DISPLAY_ITEMS"]:
stats_text += f"- ... and {len(failed_titles) - CONFIG['PPT_MAX_DISPLAY_ITEMS']} more\n"
else:
stats_text += "No failed test\n"
stats_text += "\nOn-going test please see below:\n"
if ongoing_titles:
for title in ongoing_titles[:CONFIG["PPT_MAX_DISPLAY_ITEMS"]]:
if len(title) > 80:
title = title[:80] + "..."
stats_text += f"- {title}\n"
if len(ongoing_titles) > CONFIG["PPT_MAX_DISPLAY_ITEMS"]:
stats_text += f"- ... and {len(ongoing_titles) - CONFIG['PPT_MAX_DISPLAY_ITEMS']} more\n"
else:
stats_text += "No on-going test\n"
# 添加文字框
text_box = slide.shapes.add_textbox(text_left, text_top, text_width, text_height)
text_frame = text_box.text_frame
text_frame.word_wrap = True
paragraph = text_frame.add_paragraph()
paragraph.text = stats_text
paragraph.font.size = Pt(11)
paragraph.font.name = 'Arial'
paragraph.space_after = Pt(2)
# 保存文件
base_path = os.path.splitext(file_path)[0]
slides_file = f"{base_path}_Slides.pptx"
if os.path.exists(slides_file):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
slides_file = f"{base_path}_Slides_{timestamp}.pptx"
try:
prs.save(slides_file)
logger.info(f"幻灯片已保存到: {slides_file}")
QMessageBox.information(None, "成功", f"幻灯片已保存:\n{slides_file}")
ui.statusbar.showMessage(f"幻灯片已保存: {os.path.basename(slides_file)}")
except PermissionError:
temp_dir = os.path.join(os.environ.get('TEMP', '/tmp'), 'ptl_slides')
os.makedirs(temp_dir, exist_ok=True)
slides_file = os.path.join(temp_dir, os.path.basename(slides_file))
prs.save(slides_file)
logger.warning(f"原路径无权限,已保存到临时目录: {slides_file}")
QMessageBox.information(None, "保存成功", f"幻灯片已保存到:\n{slides_file}\n(原路径无写入权限)")
except Exception as e:
logger.error(f"创建幻灯片时出错: {str(e)}", exc_info=True)
QMessageBox.critical(None, "错误", f"创建幻灯片失败: {str(e)}")
ui.statusbar.showMessage("创建幻灯片失败")
finally:
ui.pushButton_Slides.setEnabled(True)
# 全局异常处理
def except_hook(cls, exception, traceback):
logger.error(f"未处理的异常: {exception}", exc_info=(cls, exception, traceback))
sys.__excepthook__(cls, exception, traceback)
QMessageBox.critical(None, "程序错误", f"发生未处理的异常:\n{str(exception)}")
sys.excepthook = except_hook
# 创建主窗口
app = QApplication(sys.argv)
MainWindow = QMainWindow()
# UI类定义
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(1500, 950)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.pushButton_JSON = QtWidgets.QPushButton(self.centralwidget)
self.pushButton_JSON.setGeometry(QtCore.QRect(950, 90, 400, 50))
font = QtGui.QFont()
font.setPointSize(16)
self.pushButton_JSON.setFont(font)
self.pushButton_JSON.setObjectName("pushButton_JSON")
self.label_JSON = QtWidgets.QLabel(self.centralwidget)
self.label_JSON.setGeometry(QtCore.QRect(150, 90, 290, 50))
font = QtGui.QFont()
font.setPointSize(16)
self.label_JSON.setFont(font)
self.label_JSON.setObjectName("label_JSON")
self.pushButton_EXCEL03 = QtWidgets.QPushButton(self.centralwidget)
self.pushButton_EXCEL03.setGeometry(QtCore.QRect(950, 450, 400, 50))
font = QtGui.QFont()
font.setPointSize(16)
self.pushButton_EXCEL03.setFont(font)
self.pushButton_EXCEL03.setObjectName("pushButton_EXCEL03")
self.label_EXCEL03 = QtWidgets.QLabel(self.centralwidget)
self.label_EXCEL03.setGeometry(QtCore.QRect(150, 450, 400, 50))
font = QtGui.QFont()
font.setPointSize(16)
self.label_EXCEL03.setFont(font)
self.label_EXCEL03.setObjectName("label_EXCEL03")
self.label_Slides = QtWidgets.QLabel(self.centralwidget)
self.label_Slides.setGeometry(QtCore.QRect(150, 550, 400, 50))
font = QtGui.QFont()
font.setPointSize(16)
self.label_Slides.setFont(font)
self.label_Slides.setToolTipDuration(-1)
self.label_Slides.setObjectName("label_Slides")
self.pushButton_Slides = QtWidgets.QPushButton(self.centralwidget)
self.pushButton_Slides.setGeometry(QtCore.QRect(950, 550, 400, 50))
font = QtGui.QFont()
font.setPointSize(16)
self.pushButton_Slides.setFont(font)
self.pushButton_Slides.setObjectName("pushButton_Slides")
self.label_Instruction = QtWidgets.QLabel(self.centralwidget)
self.label_Instruction.setGeometry(QtCore.QRect(140, 710, 1200, 150))
font = QtGui.QFont()
font.setPointSize(16)
self.label_Instruction.setFont(font)
self.label_Instruction.setToolTipDuration(-1)
self.label_Instruction.setObjectName("label_Instruction")
self.comboBox_1 = QtWidgets.QComboBox(self.centralwidget)
self.comboBox_1.setGeometry(QtCore.QRect(500, 150, 400, 50))
font = QtGui.QFont()
font.setPointSize(12)
self.comboBox_1.setFont(font)
self.comboBox_1.setObjectName("comboBox_1")
self.comboBox_1.addItem("")
self.comboBox_1.setItemText(0, "")
self.comboBox_1.addItem("")
self.comboBox_1.addItem("")
self.comboBox_1.addItem("")
self.comboBox_1.addItem("")
self.comboBox_1.addItem("")
self.comboBox_1.addItem("")
self.comboBox_1.addItem("")
self.comboBox_1.addItem("")
self.comboBox_1.addItem("")
self.comboBox_1.addItem("")
self.comboBox_1.addItem("")
self.comboBox_1.addItem("")
self.comboBox_2 = QtWidgets.QComboBox(self.centralwidget)
self.comboBox_2.setGeometry(QtCore.QRect(500, 210, 400, 50))
font = QtGui.QFont()
font.setPointSize(12)
self.comboBox_2.setFont(font)
self.comboBox_2.setObjectName("comboBox_2")
self.comboBox_2.addItem("")
self.comboBox_2.setItemText(0, "")
self.comboBox_2.addItem("")
self.comboBox_2.addItem("")
self.comboBox_2.addItem("")
self.comboBox_2.addItem("")
self.comboBox_2.addItem("")
self.comboBox_2.addItem("")
self.comboBox_2.addItem("")
self.comboBox_2.addItem("")
self.comboBox_2.addItem("")
self.comboBox_2.addItem("")
self.comboBox_2.addItem("")
self.comboBox_2.addItem("")
self.comboBox_3 = QtWidgets.QComboBox(self.centralwidget)
self.comboBox_3.setGeometry(QtCore.QRect(500, 270, 400, 50))
font = QtGui.QFont()
font.setPointSize(12)
self.comboBox_3.setFont(font)
self.comboBox_3.setObjectName("comboBox_3")
self.comboBox_3.addItem("")
self.comboBox_3.setItemText(0, "")
self.comboBox_3.addItem("")
self.comboBox_3.addItem("")
self.comboBox_3.addItem("")
self.comboBox_3.addItem("")
self.comboBox_3.addItem("")
self.comboBox_3.addItem("")
self.comboBox_3.addItem("")
self.comboBox_3.addItem("")
self.comboBox_3.addItem("")
self.comboBox_3.addItem("")
self.comboBox_3.addItem("")
self.comboBox_3.addItem("")
self.textEdit_1 = QtWidgets.QTextEdit(self.centralwidget)
self.textEdit_1.setGeometry(QtCore.QRect(500, 90, 400, 50))
font = QtGui.QFont()
font.setPointSize(12)
self.textEdit_1.setFont(font)
self.textEdit_1.setObjectName("textEdit_1")
# 添加进度条
self.progress_bar = QtWidgets.QProgressBar(self.centralwidget)
self.progress_bar.setGeometry(QtCore.QRect(150, 870, 1200, 30))
self.progress_bar.setObjectName("progress_bar")
self.progress_bar.setRange(0, 100)
self.progress_bar.setVisible(False)
self.progress_bar.setStyleSheet("""
QProgressBar {
border: 2px solid #bbb;
border-radius: 5px;
text-align: center;
}
QProgressBar::chunk {
background-color: #87CEEB;
width: 20px;
}
""")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1500, 22))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "PTL_Tool"))
self.pushButton_JSON.setText(_translate("MainWindow", "Create a JSON File"))
self.label_JSON.setText(_translate("MainWindow", "Test Plan ID:"))
self.pushButton_EXCEL03.setText(_translate("MainWindow", "Create Validation Plan"))
self.label_EXCEL03.setText(_translate("MainWindow", "Choose a JSON File:"))
self.label_Slides.setText(_translate("MainWindow", "Choose Validation Plan:"))
self.pushButton_Slides.setText(_translate("MainWindow", "Create Slides"))
self.label_Instruction.setText(_translate("MainWindow", "<html><head/><body><p>Instruction: </p><p>1. All functions have been launched.</p></body></html>"))
# 下拉菜单选项
self.comboBox_1.setItemText(1, _translate("MainWindow", "PLAN25AA0120 (ACB)"))
self.comboBox_1.setItemText(2, _translate("MainWindow", "PLAN25AA0029 (MCCB)"))
self.comboBox_1.setItemText(3, _translate("MainWindow", "PLAN25AA0226 (MCB)"))
self.comboBox_1.setItemText(4, _translate("MainWindow", "PLAN25AAxxxx (Switch)"))
self.comboBox_1.setItemText(5, _translate("MainWindow", "PLAN25AA0103 (Contactor)"))
self.comboBox_1.setItemText(6, _translate("MainWindow", "PLAN25AAxxxx (Relay)"))
self.comboBox_1.setItemText(7, _translate("MainWindow", "PLAN25AAxxxx (GV)"))
self.comboBox_1.setItemText(8, _translate("MainWindow", "PLAN25AAxxxx (ATS)"))
self.comboBox_1.setItemText(9, _translate("MainWindow", "PLAN25AA0167 (RCBO IEC)"))
self.comboBox_1.setItemText(10, _translate("MainWindow", "PLAN25AA0062 (SPD)"))
self.comboBox_1.setItemText(11, _translate("MainWindow", "PLAN25AAxxxx (OUPA)"))
self.comboBox_1.setItemText(12, _translate("MainWindow", "PLAN25AAxxxx (AFDD)"))
self.comboBox_2.setItemText(1, _translate("MainWindow", "PLAN25AA0120 (ACB)"))
self.comboBox_2.setItemText(2, _translate("MainWindow", "PLAN25AA0029 (MCCB)"))
self.comboBox_2.setItemText(3, _translate("MainWindow", "PLAN25AA0226 (MCB)"))
self.comboBox_2.setItemText(4, _translate("MainWindow", "PLAN25AAxxxx (Switch)"))
self.comboBox_2.setItemText(5, _translate("MainWindow", "PLAN25AA0103 (Contactor)"))
self.comboBox_2.setItemText(6, _translate("MainWindow", "PLAN25AAxxxx (Relay)"))
self.comboBox_2.setItemText(7, _translate("MainWindow", "PLAN25AAxxxx (GV)"))
self.comboBox_2.setItemText(8, _translate("MainWindow", "PLAN25AAxxxx (ATS)"))
self.comboBox_2.setItemText(9, _translate("MainWindow", "PLAN25AA0167 (RCBO IEC)"))
self.comboBox_2.setItemText(10, _translate("MainWindow", "PLAN25AA0062 (SPD)"))
self.comboBox_2.setItemText(11, _translate("MainWindow", "PLAN25AAxxxx (OUPA)"))
self.comboBox_2.setItemText(12, _translate("MainWindow", "PLAN25AAxxxx (AFDD)"))
self.comboBox_3.setItemText(1, _translate("MainWindow", "PLAN25AA0120 (ACB)"))
self.comboBox_3.setItemText(2, _translate("MainWindow", "PLAN25AA0029 (MCCB)"))
self.comboBox_3.setItemText(3, _translate("MainWindow", "PLAN25AA0226 (MCB)"))
self.comboBox_3.setItemText(4, _translate("MainWindow", "PLAN25AAxxxx (Switch)"))
self.comboBox_3.setItemText(5, _translate("MainWindow", "PLAN25AA0103 (Contactor)"))
self.comboBox_3.setItemText(6, _translate("MainWindow", "PLAN25AAxxxx (Relay)"))
self.comboBox_3.setItemText(7, _translate("MainWindow", "PLAN25AAxxxx (GV)"))
self.comboBox_3.setItemText(8, _translate("MainWindow", "PLAN25AAxxxx (ATS)"))
self.comboBox_3.setItemText(9, _translate("MainWindow", "PLAN25AA0167 (RCBO IEC)"))
self.comboBox_3.setItemText(10, _translate("MainWindow", "PLAN25AA0062 (SPD)"))
self.comboBox_3.setItemText(11, _translate("MainWindow", "PLAN25AAxxxx (OUPA)"))
self.comboBox_3.setItemText(12, _translate("MainWindow", "PLAN25AAxxxx (AFDD)"))
self.textEdit_1.setPlaceholderText(_translate("MainWindow", "PLAN25AA****(多个ID用逗号分隔)"))
# 初始化UI
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
# 连接信号和槽
ui.pushButton_JSON.clicked.connect(create_json_from_test_plan_id)
ui.pushButton_EXCEL03.clicked.connect(lambda: create_excel_from_json("PTL_03"))
ui.pushButton_Slides.clicked.connect(create_slides_from_ptl03)
# 显示主窗口
MainWindow.show()
# 运行主循环
if __name__ == "__main__":
if len(sys.argv) == 1:
sys.exit(app.exec_())
else:
if len(sys.argv) < 2:
print("错误:请提供JSON文件路径作为参数")
sys.exit(1)
json_file = sys.argv[1]
if not json_file.lower().endswith('.json'):
print(f"错误:文件 '{json_file}' 不是有效的JSON文件")
sys.exit(1)
api_data = load_api_data_from_file(json_file)
if api_data is None:
print("程序退出:无法加载数据")
sys.exit(1)
result = api_to_excel(api_data)
if result:
print("操作成功完成")
sys.exit(0)
else:
print("程序退出:处理数据时发生错误")
sys.exit(1)