| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- # -*- 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)
|