diff --git a/Code/src/template_reader.py b/Code/src/template_reader.py
index 637fc9b..cd2ec6d 100644
--- a/Code/src/template_reader.py
+++ b/Code/src/template_reader.py
@@ -171,15 +171,18 @@ class TemplateReader:
'fontId': xf.get('fontId', '0'),
'fillId': xf.get('fillId', '0'),
'borderId': xf.get('borderId', '0'),
+ 'xfId': xf.get('xfId', '0'),
'applyFont': xf.get('applyFont', ''),
'applyFill': xf.get('applyFill', ''),
'applyBorder': xf.get('applyBorder', ''),
+ 'applyAlignment': xf.get('applyAlignment', ''),
}
# 对齐方式
align = xf.find(_tag('alignment'))
if align is not None:
xf_info['hAlign'] = align.get('horizontal', '')
xf_info['vAlign'] = align.get('vertical', '')
+ xf_info['wrapText'] = align.get('wrapText', '')
style.cell_xfs.append(xf_info)
def _parse_sheet_dims(self, data: bytes, style: TemplateStyle):
diff --git a/Code/src/xlsx_writer.py b/Code/src/xlsx_writer.py
index 2391246..181cf87 100644
--- a/Code/src/xlsx_writer.py
+++ b/Code/src/xlsx_writer.py
@@ -258,93 +258,35 @@ class StyledXLSXWriter:
return '\n'.join(parts)
def _styles_xml(self) -> str:
- """Build xl/styles.xml with fonts, fills, borders, and cellXfs."""
+ """Build xl/styles.xml with fonts, fills, borders, and cellXfs.
+
+ F011: 有模板时从模板的 fonts / fills / borders / cellXfs 中
+ 提取实际样式定义;无模板时回退到硬编码默认样式。
+
+ 保留现有 4 个样式槽位(xf 0~3):
+ xf 0 = default (no style)
+ xf 1 = centered + thin border (pin cells)
+ xf 2 = bold + centered (A1 package info)
+ xf 3 = centered + border + light fill (header-like)
+
+ 有模板时,四个 xf 的字体/边框/填充/对齐从模板读取。
+ """
s = self._style
- # ── Fonts ──────────────────────────────────────────────────
- fonts_xml = ''
- # Font 0: default (no bold)
- font_name = "Calibri"
- font_size = "11"
- font_color = "FF000000"
if s and s.fonts:
- f0 = s.fonts[0]
- font_name = f0.name
- font_size = str(f0.size)
- font_color = "FF" + f0.color if f0.color and not f0.color.startswith("FF") else f0.color
- fonts_xml += (
- f''
- f''
- f''
- f''
- )
- # Font 1: bold (for package info in A1)
- fonts_xml += (
- f''
- f''
- f''
- f''
- f''
- )
- fonts_xml += ''
-
- # ── Fills ──────────────────────────────────────────────────
- fills_xml = ''
- fills_xml += ''
- # Fill 1: light gray for header-like cells
- fills_xml += (
- ''
- ''
- ''
- )
- fills_xml += ''
-
- # ── Borders ────────────────────────────────────────────────
- borders_xml = ''
- # Border 0: none
- borders_xml += (
- ''
- ''
- ''
- )
- # Border 1: thin all sides
- borders_xml += (
- ''
- ''
- ''
- ''
- ''
- )
- borders_xml += ''
-
- # ── Cell XFs ───────────────────────────────────────────────
- # xf 0: default (no style)
- # xf 1: centered with thin border (for pin cells)
- # xf 2: bold + centered (for A1 package info)
- # xf 3: centered + border + light fill (for header-like)
- cell_xfs_xml = ''
- cell_xfs_xml += (
- ''
- )
- cell_xfs_xml += (
- ''
- ''
- ''
- )
- cell_xfs_xml += (
- ''
- ''
- ''
- )
- cell_xfs_xml += (
- ''
- ''
- ''
- )
- cell_xfs_xml += ''
+ # ── 有模板:从模板提取实际值构建样式 ──────────────────
+ fonts_xml = self._build_fonts_xml_from_template(s.fonts)
+ fills_xml = self._build_fills_xml_from_template(s.fills)
+ borders_xml = self._build_borders_xml_from_template(s.borders)
+ cell_xfs_xml = self._build_cell_xfs_xml_from_template(
+ s.cell_xfs, s.fonts, s.fills, s.borders
+ )
+ else:
+ # ── 无模板:回退到硬编码默认样式(F011 fallback)──────
+ fonts_xml = self._default_fonts_xml()
+ fills_xml = self._default_fills_xml()
+ borders_xml = self._default_borders_xml()
+ cell_xfs_xml = self._default_cell_xfs_xml()
parts = ['']
parts.append(
@@ -357,6 +299,250 @@ class StyledXLSXWriter:
parts.append('')
return '\n'.join(parts)
+ # ── 模板样式构建(F011)─────────────────────────────────────────
+
+ @staticmethod
+ def _build_fonts_xml_from_template(fonts: list) -> str:
+ """从模板字体列表构建 XML。"""
+ parts = [f'']
+ for f in fonts:
+ parts.append('')
+ parts.append(f'')
+ if f.bold:
+ parts.append('')
+ if f.italic:
+ parts.append('')
+ parts.append(f'')
+ color_val = f.color
+ if color_val and not color_val.startswith('FF'):
+ color_val = 'FF' + color_val
+ if color_val:
+ parts.append(f'')
+ parts.append('')
+ parts.append('')
+ return ''.join(parts)
+
+ @staticmethod
+ def _build_fills_xml_from_template(fills: list) -> str:
+ """从模板填充列表构建 XML。"""
+ parts = [f'']
+ for fl in fills:
+ parts.append('')
+ parts.append(f'')
+ if fl.fg_color:
+ color_val = fl.fg_color
+ if not color_val.startswith('FF'):
+ color_val = 'FF' + color_val
+ parts.append(f'')
+ parts.append('')
+ parts.append('')
+ parts.append('')
+ return ''.join(parts)
+
+ @staticmethod
+ def _build_borders_xml_from_template(borders: list) -> str:
+ """从模板边框列表构建 XML。"""
+ parts = [f'']
+ for b in borders:
+ parts.append('')
+ for side_name in ('left', 'right', 'top', 'bottom'):
+ style_val = getattr(b, side_name, 'none')
+ if style_val and style_val != 'none':
+ parts.append(f'<{side_name} style="{style_val}"/>')
+ else:
+ parts.append(f'<{side_name}/>')
+ parts.append('')
+ parts.append('')
+ parts.append('')
+ return ''.join(parts)
+
+ @staticmethod
+ def _build_cell_xfs_xml_from_template(
+ cell_xfs: list, fonts: list, fills: list, borders: list
+ ) -> str:
+ """从模板 cellXfs 列表构建 XML。
+
+ 保留 4 个样式槽位(xf 0~3),每个从模板对应位置的 xf 提取:
+ - xf 0: default (模板的 xf 0)
+ - xf 1: pin cells — 使用模板中带边框的 xf(优先),否则 fallback
+ - xf 2: A1 bold — 使用模板中带 bold 字体的 xf(优先),否则 fallback
+ - xf 3: header — 使用模板中带填充的 xf(优先),否则 fallback
+ """
+ def _xf_to_attrs(xf: dict) -> str:
+ """将 xf 信息字典转换为属性字符串。"""
+ attrs = [
+ f'numFmtId="{xf.get("numFmtId", "0")}"',
+ f'fontId="{xf.get("fontId", "0")}"',
+ f'fillId="{xf.get("fillId", "0")}"',
+ f'borderId="{xf.get("borderId", "0")}"',
+ f'xfId="{xf.get("xfId", "0")}"',
+ ]
+ for attr_key in ('applyFont', 'applyFill', 'applyBorder', 'applyAlignment',
+ 'applyNumberFormat', 'applyProtection'):
+ val = xf.get(attr_key, '')
+ if val:
+ attrs.append(f'{attr_key}="{val}"')
+ return ' '.join(attrs)
+
+ def _xf_to_alignment(xf: dict) -> str:
+ """从 xf 信息字典生成对齐 XML片段。"""
+ h_align = xf.get('hAlign', '')
+ v_align = xf.get('vAlign', '')
+ wrap_text = xf.get('wrapText', '')
+ if h_align or v_align or wrap_text:
+ align_attrs = []
+ if h_align:
+ align_attrs.append(f'horizontal="{h_align}"')
+ if v_align:
+ align_attrs.append(f'vertical="{v_align}"')
+ if wrap_text:
+ align_attrs.append(f'wrapText="{wrap_text}"')
+ return f''
+ return ''
+
+ # 确保有足够的模板样式元素
+ def _get_or_default(lst, idx, default_name):
+ if idx < len(lst):
+ return idx
+ return 0
+
+ # ── 确定 4 个 xf 的映射 ──────────────────────────────────
+ # xf 0: 模板的 xf 0(default)
+ xf0 = cell_xfs[0] if len(cell_xfs) > 0 else {}
+ fi0 = _get_or_default(fonts, int(xf0.get('fontId', '0')), 'font')
+ fl0 = _get_or_default(fills, int(xf0.get('fillId', '0')), 'fill')
+ bd0 = _get_or_default(borders, int(xf0.get('borderId', '0')), 'border')
+
+ # xf 1: 寻找模板中有边框的 xf(borderId > 0 或 applyBorder 非空)
+ xf1 = xf0
+ fi1, fl1, bd1 = fi0, fl0, bd0
+ for xf in cell_xfs:
+ bid = int(xf.get('borderId', '0'))
+ ab = xf.get('applyBorder', '')
+ if bid > 0 or ab:
+ xf1 = xf
+ fi1 = _get_or_default(fonts, int(xf1.get('fontId', '0')), 'font')
+ fl1 = _get_or_default(fills, int(xf1.get('fillId', '0')), 'fill')
+ bd1 = _get_or_default(borders, int(xf1.get('borderId', '0')), 'border')
+ break
+
+ # xf 2: 寻找模板中有 bold 字体的 xf
+ xf2 = xf0
+ fi2, fl2, bd2 = fi0, fl0, bd0
+ bold_font_id = None
+ for i, f in enumerate(fonts):
+ if f.bold:
+ bold_font_id = i
+ break
+ if bold_font_id is not None:
+ for xf in cell_xfs:
+ fid = int(xf.get('fontId', '0'))
+ if xf.get('applyFont', '') or fid == bold_font_id:
+ xf2 = xf
+ break
+ fi2 = bold_font_id
+ fl2 = _get_or_default(fills, int(xf2.get('fillId', '0')), 'fill')
+ bd2 = _get_or_default(borders, int(xf2.get('borderId', '0')), 'border')
+
+ # xf 3: 寻找模板中有填充的 xf(fillId > 0 或 applyFill 非空)
+ xf3 = xf0
+ fi3, fl3, bd3 = fi0, fl0, bd0
+ for xf in cell_xfs:
+ fid = int(xf.get('fillId', '0'))
+ af = xf.get('applyFill', '')
+ if fid > 0 or af:
+ xf3 = xf
+ fi3 = _get_or_default(fonts, int(xf3.get('fontId', '0')), 'font')
+ fl3 = _get_or_default(fills, int(xf3.get('fillId', '0')), 'fill')
+ bd3 = _get_or_default(borders, int(xf3.get('borderId', '0')), 'border')
+ break
+
+ # ── 构建 4 个 xf ──────────────────────────────────────────
+ parts = ['']
+
+ # xf 0: default (no style)
+ parts.append(f'')
+
+ # xf 1: pin cells (border + center align)
+ align1 = _xf_to_alignment(xf1)
+ parts.append(
+ f''
+ f'{align1}'
+ f''
+ )
+
+ # xf 2: A1 package info (bold + center align)
+ align2 = _xf_to_alignment(xf2) or ''
+ parts.append(
+ f''
+ f'{align2}'
+ f''
+ )
+
+ # xf 3: header-like (fill + border + center align)
+ align3 = _xf_to_alignment(xf3) or ''
+ parts.append(
+ f''
+ f'{align3}'
+ f''
+ )
+
+ parts.append('')
+ return ''.join(parts)
+
+ # ── 默认硬编码样式(无模板时回退)────────────────────────────
+
+ @staticmethod
+ def _default_fonts_xml() -> str:
+ """生成默认硬编码字体:font 0 = Calibri 11pt, font 1 = Calibri 11pt bold。"""
+ return (
+ ''
+ ''
+ ''
+ ''
+ )
+
+ @staticmethod
+ def _default_fills_xml() -> str:
+ """生成默认硬编码填充:fill 0 = none, fill 1 = light gray。"""
+ return (
+ ''
+ ''
+ ''
+ ''
+ )
+
+ @staticmethod
+ def _default_borders_xml() -> str:
+ """生成默认硬编码边框:border 0 = none, border 1 = thin all sides。"""
+ return (
+ ''
+ ''
+ ''
+ ''
+ ''
+ )
+
+ @staticmethod
+ def _default_cell_xfs_xml() -> str:
+ """生成默认硬编码 cellXfs:
+ xf 0 = default, xf 1 = centered+border, xf 2 = bold+centered, xf 3 = centered+border+fill。
+ """
+ return (
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ )
+
def _sheet_xml(self, data: dict[str, str]) -> str:
"""Build sheet1.xml with style indices applied."""
max_row = 0