mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 04:46:26 +00:00
1. 迁移存储模块功能到工作流模块,合并冗余代码 2. 调整环境配置加载路径与初始化脚本目录 3. 更新文档与前端组件代码适配重构 4. 新增工作流相关CRUD、模型与迁移文件 5. 移除过期模块与冗余代码,优化日志配置
150 lines
5.6 KiB
Python
150 lines
5.6 KiB
Python
import asyncio
|
|
import io
|
|
from typing import Any
|
|
|
|
from openpyxl import Workbook, load_workbook
|
|
from openpyxl.styles import Alignment, PatternFill
|
|
from openpyxl.utils import get_column_letter
|
|
from openpyxl.worksheet.datavalidation import DataValidation
|
|
|
|
|
|
class ExcelUtil:
|
|
"""Excel 模板生成与列表导出(openpyxl)。"""
|
|
|
|
@staticmethod
|
|
def read_excel_to_dicts(contents: bytes) -> list[dict[str, Any]]:
|
|
"""读取 Excel 文件字节,返回字典列表(首行为列名)。"""
|
|
wb = load_workbook(io.BytesIO(contents), read_only=True, data_only=True)
|
|
ws = wb.active
|
|
if not ws:
|
|
raise ValueError("工作簿没有活动工作表")
|
|
headers = [cell.value for cell in next(ws.iter_rows(min_row=1, max_row=1))]
|
|
result: list[dict[str, Any]] = []
|
|
for row in ws.iter_rows(min_row=2, values_only=True):
|
|
if all(cell is None for cell in row):
|
|
continue
|
|
row_dict: dict[str, Any] = {}
|
|
for i, val in enumerate(row):
|
|
if i < len(headers) and headers[i] is not None:
|
|
row_dict[str(headers[i])] = val
|
|
if row_dict:
|
|
result.append(row_dict)
|
|
wb.close()
|
|
return result
|
|
|
|
@staticmethod
|
|
async def aread_excel_to_dicts(contents: bytes) -> list[dict[str, Any]]:
|
|
"""read_excel_to_dicts 的异步版本:解析在线程池中执行。
|
|
|
|
openpyxl 解析上传文件(含 zip 解压与逐单元格构建)是纯同步 CPU +
|
|
内存操作,大文件会在事件循环里独占数秒,async 调用点必须使用本方法。
|
|
"""
|
|
return await asyncio.to_thread(ExcelUtil.read_excel_to_dicts, contents)
|
|
|
|
@classmethod
|
|
def __mapping_list(cls, list_data: list[dict[str, Any]], mapping_dict: dict) -> list[dict[str, Any]]:
|
|
"""将列表数据中的字段名映射为对应的中文字段名。
|
|
|
|
参数:
|
|
- list_data: 数据列表。
|
|
- mapping_dict: 字段名映射字典 {英文key: 中文表头}。
|
|
|
|
返回:
|
|
- list[dict]: 映射后的数据列表 [{中文表头: value}]。
|
|
"""
|
|
return [{str(mapping_dict.get(key)): item.get(key) for key in mapping_dict} for item in list_data]
|
|
|
|
@classmethod
|
|
def get_excel_template(
|
|
cls,
|
|
header_list: list[str],
|
|
selector_header_list: list[str],
|
|
option_list: list[dict[str, list[str]]],
|
|
) -> bytes:
|
|
"""生成 Excel 模板文件。
|
|
|
|
参数:
|
|
- header_list: 表头列表。
|
|
- selector_header_list: 需要设置下拉选择的表头列表。
|
|
- option_list: 下拉选项配置列表。
|
|
|
|
返回:
|
|
- bytes: Excel 文件的二进制数据。
|
|
"""
|
|
wb = Workbook()
|
|
ws = wb.active
|
|
if not ws:
|
|
raise ValueError("不存在活动工作表")
|
|
|
|
header_fill = PatternFill(start_color="ababab", end_color="ababab", fill_type="solid")
|
|
|
|
for col_num, header in enumerate(header_list, 1):
|
|
cell = ws.cell(row=1, column=col_num)
|
|
cell.value = header # pyright: ignore[reportAttributeAccessIssue]
|
|
cell.fill = header_fill
|
|
cell.alignment = Alignment(horizontal="center")
|
|
ws.column_dimensions[get_column_letter(col_num)].width = 12
|
|
|
|
for selector_header in selector_header_list:
|
|
col_idx = header_list.index(selector_header) + 1
|
|
header_options = next(
|
|
(opt.get(selector_header) for opt in option_list if selector_header in opt),
|
|
[],
|
|
)
|
|
if header_options:
|
|
dv = DataValidation(type="list", formula1=f'"{",".join(header_options)}"')
|
|
dv.add(f"{get_column_letter(col_idx)}2:{get_column_letter(col_idx)}1048576")
|
|
ws.add_data_validation(dv)
|
|
|
|
buffer = io.BytesIO()
|
|
wb.save(buffer)
|
|
buffer.seek(0)
|
|
return buffer.getvalue()
|
|
|
|
@classmethod
|
|
def export_list2excel(cls, list_data: list[dict[str, Any]], mapping_dict: dict) -> bytes:
|
|
"""将列表数据导出为 Excel 文件。
|
|
|
|
参数:
|
|
- list_data: 要导出的数据列表。
|
|
- mapping_dict: 字段名映射字典 {英文key: 中文表头}。
|
|
|
|
返回:
|
|
- bytes: Excel 文件的二进制数据。
|
|
|
|
限制:
|
|
- 最多导出 100000 条记录,超出部分截断。
|
|
"""
|
|
max_rows = 100000
|
|
if len(list_data) > max_rows:
|
|
list_data = list_data[:max_rows]
|
|
|
|
mapping_data = cls.__mapping_list(list_data, mapping_dict)
|
|
headers = list(mapping_dict.values())
|
|
|
|
wb = Workbook()
|
|
ws = wb.active
|
|
if not ws:
|
|
raise ValueError("不存在活动工作表")
|
|
|
|
for col_num, header in enumerate(headers, 1):
|
|
ws.cell(row=1, column=col_num, value=header)
|
|
|
|
for row_num, row_data in enumerate(mapping_data, 2):
|
|
for col_num, header in enumerate(headers, 1):
|
|
ws.cell(row=row_num, column=col_num, value=row_data.get(header))
|
|
|
|
buffer = io.BytesIO()
|
|
wb.save(buffer)
|
|
buffer.seek(0)
|
|
return buffer.getvalue()
|
|
|
|
@classmethod
|
|
async def aexport_list2excel(cls, list_data: list[dict[str, Any]], mapping_dict: dict) -> bytes:
|
|
"""export_list2excel 的异步版本:构建与序列化在线程池中执行。
|
|
|
|
导出最多 10 万行,逐单元格写入 + ZIP 压缩为纯同步 CPU 操作,
|
|
千行以上即会明显卡住事件循环,async 调用点必须使用本方法。
|
|
"""
|
|
return await asyncio.to_thread(cls.export_list2excel, list_data, mapping_dict)
|