from io import BytesIO from typing import List, Dict, Any, Type, Optional from openpyxl import Workbook, load_workbook from openpyxl.styles import Font, Alignment, Border, Side, PatternFill from pydantic import BaseModel class ExcelHandler: """Excel处理工具类""" # 表头样式 HEADER_FONT = Font(bold=True, color="FFFFFF") HEADER_FILL = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") HEADER_ALIGNMENT = Alignment(horizontal="center", vertical="center") # 边框样式 THIN_BORDER = Border( left=Side(style="thin"), right=Side(style="thin"), top=Side(style="thin"), bottom=Side(style="thin") ) @classmethod def export_to_excel( cls, data: List[Dict[str, Any]], columns: Dict[str, str], sheet_name: str = "Sheet1" ) -> BytesIO: """ 导出数据到Excel :param data: 数据列表,每个元素是一个字典 :param columns: 列映射,格式为 {字段名: 显示名} :param sheet_name: 工作表名称 :return: Excel文件的BytesIO对象 """ wb = Workbook() ws = wb.active ws.title = sheet_name # 写入表头 headers = list(columns.values()) field_names = list(columns.keys()) for col_idx, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col_idx, value=header) cell.font = cls.HEADER_FONT cell.fill = cls.HEADER_FILL cell.alignment = cls.HEADER_ALIGNMENT cell.border = cls.THIN_BORDER # 写入数据 for row_idx, row_data in enumerate(data, 2): for col_idx, field in enumerate(field_names, 1): value = row_data.get(field, "") cell = ws.cell(row=row_idx, column=col_idx, value=value) cell.border = cls.THIN_BORDER cell.alignment = Alignment(vertical="center") # 自动调整列宽 for col_idx, header in enumerate(headers, 1): max_length = len(str(header)) for row in ws.iter_rows(min_row=2, min_col=col_idx, max_col=col_idx): for cell in row: if cell.value: max_length = max(max_length, len(str(cell.value))) ws.column_dimensions[ws.cell(row=1, column=col_idx).column_letter].width = min(max_length + 2, 50) # 保存到BytesIO output = BytesIO() wb.save(output) output.seek(0) return output @classmethod def import_from_excel( cls, file_content: bytes, columns: Dict[str, str], schema: Optional[Type[BaseModel]] = None ) -> List[Dict[str, Any]]: """ 从Excel导入数据 :param file_content: Excel文件内容 :param columns: 列映射,格式为 {字段名: 显示名} :param schema: 可选的Pydantic Schema用于数据验证 :return: 数据列表 """ wb = load_workbook(filename=BytesIO(file_content), read_only=True) ws = wb.active # 读取表头,建立显示名到字段名的映射 header_to_field = {v: k for k, v in columns.items()} rows = list(ws.iter_rows(values_only=True)) if not rows: return [] # 第一行是表头 headers = rows[0] field_indices = {} for idx, header in enumerate(headers): if header in header_to_field: field_indices[idx] = header_to_field[header] # 读取数据行 result = [] for row in rows[1:]: if not any(row): # 跳过空行 continue row_data = {} for idx, field_name in field_indices.items(): value = row[idx] if idx < len(row) else None row_data[field_name] = value # 如果提供了schema,进行数据验证 if schema: try: validated = schema(**row_data) row_data = validated.model_dump() except Exception: continue # 跳过验证失败的行 result.append(row_data) wb.close() return result @classmethod def generate_template( cls, columns: Dict[str, str], sheet_name: str = "Sheet1" ) -> BytesIO: """ 生成导入模板 :param columns: 列映射,格式为 {字段名: 显示名} :param sheet_name: 工作表名称 :return: Excel模板文件的BytesIO对象 """ return cls.export_to_excel([], columns, sheet_name)