# -*- coding: utf-8 -*- # Author : Charley # Python : 3.12.10 # Date : 2026/08/19 """集物星球报表 Excel 样式工具库(openpyxl):供在售/已售日报复用统一排版。 提供分区标题条、表头样式、明细表(斑马纹/新品高亮/金额百分比格式/冻结首行/列宽自适应)、 键值竖排(概览/汇总)等写入函数。金额一律以「元」浮点写入单元格并套 #,##0.00 格式, 调用方负责把原始金额值 ÷1000000 后传入(1 元 = 1000000)。 """ from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter # 配色(沿用参考项目 deca 报表风格) FILL_SECTION = PatternFill("solid", fgColor="4472C4") # 分区标题条:蓝底 FILL_HEADER = PatternFill("solid", fgColor="D9E1F2") # 表头:浅蓝底 FILL_ZEBRA = PatternFill("solid", fgColor="F5F8FC") # 明细隔行斑马 FILL_SUBTOTAL = PatternFill("solid", fgColor="FCE4D6") # 汇总数值:浅橙底 FILL_NEWROW = PatternFill("solid", fgColor="FEF2F2") # 今日新上架高亮:淡红底 FONT_SECTION = Font(color="FFFFFF", bold=True, size=12) # 分区标题:白字加粗 FONT_HEADER = Font(color="1F3864", bold=True) # 表头:深蓝字加粗 FONT_LABEL = Font(color="1F3864", bold=True) # 键值标签 _SIDE = Side(style="thin", color="D6DCE5") BORDER = Border(left=_SIDE, right=_SIDE, top=_SIDE, bottom=_SIDE) MONEY_FMT = "#,##0.00" # 金额(元) INT_FMT = "#,##0" # 计数 PCT_FMT = "0.0%" # 进度(传入 0~1 小数) _ALIGN_L = Alignment(horizontal="left", vertical="center", wrap_text=True) _ALIGN_L_NOWRAP = Alignment(horizontal="left", vertical="center") # 标题/分区条:不换行,长文本自然向右溢出 _ALIGN_R = Alignment(horizontal="right", vertical="center") _ALIGN_C = Alignment(horizontal="center", vertical="center", wrap_text=True) def new_workbook() -> Workbook: """新建一个不含默认 sheet 的工作簿。 Returns: Workbook: 已删掉默认 Sheet 的空工作簿,后续用 add_sheet 逐个加。 """ wb = Workbook() wb.remove(wb.active) return wb def add_sheet(wb: Workbook, title: str): """新增一个工作表(表名超长自动截断到 31 字符)。 Args: wb (Workbook): 目标工作簿。 title (str): 工作表名。 Returns: Worksheet: 新建的工作表。 """ return wb.create_sheet(title[:31]) def _disp_width(text: str) -> int: """按中文占 2、其余占 1 估算字符串显示宽度。 Args: text (str): 文本。 Returns: int: 估算显示宽度。 """ return sum(2 if ord(c) > 127 else 1 for c in str(text)) def _autosize(ws, headers: list, rows: list, base_col: int = 1) -> None: """按表头与数据估算并设置各列列宽(clamp 到 [8,60])。 Args: ws: 工作表。 headers (list): 表头列表。 rows (list): 数据行(元组列表)。 base_col (int): 首列列号(1 起)。Defaults to 1。 """ for i, h in enumerate(headers): w = _disp_width(h) for r in rows: if i < len(r) and r[i] is not None: w = max(w, _disp_width(r[i])) letter = get_column_letter(base_col + i) cur = ws.column_dimensions[letter].width or 0 # 只增不减:不覆盖调用方预设的更宽列宽 ws.column_dimensions[letter].width = max(cur, min(60, max(8, w + 2))) def _apply_fmt(cell, col_type: str) -> None: """按列类型给单元格设数字格式与对齐。 Args: cell: 单元格。 col_type (str): 列类型 money / int / pct / text。 """ if col_type == "money": cell.number_format = MONEY_FMT cell.alignment = _ALIGN_R elif col_type == "int": cell.number_format = INT_FMT cell.alignment = _ALIGN_R elif col_type == "pct": cell.number_format = PCT_FMT cell.alignment = _ALIGN_R else: cell.alignment = _ALIGN_L def write_section_title(ws, row: int, ncols: int, text: str) -> int: """写一条横跨 ncols 列的分区标题条。 Args: ws: 工作表。 row (int): 起始行(1 起)。 ncols (int): 跨列数。 text (str): 标题文字。 Returns: int: 下一可写行号。 """ # 不用合并单元格:合并区内文字无法溢出、列窄时长标题会被切。改为「给若干列铺底色的空单元格 + # 文字写在 A 列不换行」,让文字自然溢出到后面这些有色空单元格上(永远落在色条内、不被截断)。 span = max(1, ncols) # 铺色跨度=调用方给定列数(同 sheet 内各标题条传同一 ncols→长度统一、严谨) for col in range(1, span + 1): ws.cell(row=row, column=col).fill = FILL_SECTION c = ws.cell(row=row, column=1, value=text) c.font = FONT_SECTION c.alignment = _ALIGN_L_NOWRAP # 不换行,长文本自然向右溢出到有色空单元格 ws.row_dimensions[row].height = 22 return row + 1 def write_title(ws, row: int, text: str) -> int: """写一条大标题(加粗大字,不填色不合并,自然向右溢出到空列,风格贴近 deca)。 Args: ws: 工作表。 row (int): 行号。 text (str): 标题文字。 Returns: int: 下一可写行号。 """ c = ws.cell(row=row, column=1, value=text) c.font = Font(color="1F3864", bold=True, size=14) c.alignment = _ALIGN_L_NOWRAP return row + 1 def write_kv(ws, row: int, items: list, title: str = None) -> int: """竖排写键值区(指标/数值两列),用于概览、单商家汇总。 Args: ws: 工作表。 row (int): 起始行。 items (list): 元素为 (标签, 值, 类型) 的列表;类型取 text/money/int/pct。 title (str, optional): 若给出,先写一条分区标题条。Defaults to None。 Returns: int: 下一可写行号。 """ if title: row = write_section_title(ws, row, 2, title) for label, value, col_type in items: lc = ws.cell(row=row, column=1, value=label) lc.font = FONT_LABEL # 标签:深蓝加粗 + 浅蓝底(对齐 deca 汇总块配色) lc.fill = FILL_HEADER lc.alignment = _ALIGN_L_NOWRAP lc.border = BORDER vc = ws.cell(row=row, column=2, value=value) vc.fill = FILL_SUBTOTAL # 数值:浅橙底(对齐 deca) vc.border = BORDER _apply_fmt(vc, col_type) row += 1 ws.column_dimensions["A"].width = max(ws.column_dimensions["A"].width or 8, min(40, max(_disp_width(i[0]) for i in items) + 2)) ws.column_dimensions["B"].width = max(ws.column_dimensions["B"].width or 8, 20) return row + 1 # 区块后空一行 def write_table(ws, row: int, headers: list, rows: list, col_types: list, new_flags: list = None, freeze: bool = True) -> int: """写一张明细表(表头样式 + 斑马纹 + 可选新品高亮 + 数字格式 + 冻结表头 + 列宽自适应)。 Args: ws: 工作表。 row (int): 表头起始行。 headers (list): 列标题。 rows (list): 数据行,每行元组,元素已是展示单位(金额传元、进度传 0~1 小数)。 col_types (list): 与 headers 等长的列类型 text/money/int/pct。 new_flags (list, optional): 与 rows 等长的布尔列表,True 的行套新品高亮底色。Defaults to None。 freeze (bool, optional): 是否冻结表头行。Defaults to True。 Returns: int: 下一可写行号。 """ header_row = row for i, h in enumerate(headers): c = ws.cell(row=header_row, column=1 + i, value=h) c.fill = FILL_HEADER c.font = FONT_HEADER c.alignment = _ALIGN_C c.border = BORDER ws.row_dimensions[header_row].height = 26 for ri, data in enumerate(rows): r = header_row + 1 + ri is_new = bool(new_flags[ri]) if new_flags else False for i, val in enumerate(data): c = ws.cell(row=r, column=1 + i, value=val) c.border = BORDER _apply_fmt(c, col_types[i] if i < len(col_types) else "text") if is_new: c.fill = FILL_NEWROW elif ri % 2 == 1: c.fill = FILL_ZEBRA _autosize(ws, headers, rows) if freeze: ws.freeze_panes = f"A{header_row + 1}" return header_row + 1 + len(rows) + 1 # 表后空一行 def save(wb: Workbook, path: str) -> None: """保存工作簿到磁盘。 Args: wb (Workbook): 工作簿。 path (str): 输出文件路径。 """ wb.save(path)