| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- # -*- coding: utf-8 -*-
- from __future__ import annotations
- import re
- from pathlib import Path
- from docx import Document
- from docx.enum.text import WD_ALIGN_PARAGRAPH
- from docx.oxml import OxmlElement
- from docx.shared import Inches, Pt
- from docx.text.paragraph import Paragraph
- from PIL import Image, ImageDraw, ImageFont
- BASE = Path(r"D:\Projects\Ai-DOP\项目\项目管理\产互联项目管理\交付文档")
- MARKER = "【原型页面】"
- W, H = 1200, 680
- REMOVE_SECTION_STARTS = (
- "补充:原型设计",
- "补充:系统数据流",
- "六、核心业务流程",
- "七、业务数据流",
- "补充:核心业务流程",
- "补充:业务数据流",
- )
- HEADING_RE = re.compile(r"^(FUNC-S(\d+)-(\d+(?:~\d+)?))(?:\s+(.*))?$")
- DOMAIN_MENU = {
- "manufacturing": "制造建模",
- "sales": "产销建模",
- "supply": "供应建模",
- "warehouse": "仓储建模",
- "quality": "质量建模",
- "kanban": "运营指标建模",
- }
- def load_fonts():
- try:
- return (
- ImageFont.truetype("msyh.ttc", 11),
- ImageFont.truetype("msyh.ttc", 12),
- ImageFont.truetype("msyh.ttc", 14),
- ImageFont.truetype("msyh.ttc", 18),
- )
- except OSError:
- d = ImageFont.load_default()
- return d, d, d, d
- def delete_paragraph(paragraph: Paragraph):
- el = paragraph._element
- el.getparent().remove(el)
- def paragraph_has_image(paragraph: Paragraph) -> bool:
- xml = paragraph._element.xml
- return "w:drawing" in xml or "w:pict" in xml or "pic:pic" in xml
- def is_func_heading(text: str) -> bool:
- text = text.strip()
- if re.fullmatch(r"FUNC-S\d+-\d{3}", text):
- return False
- return bool(HEADING_RE.match(text))
- def func_code(text: str) -> str | None:
- m = HEADING_RE.match(text.strip())
- return m.group(1) if m else None
- def clean_document(path: Path) -> int:
- doc = Document(path)
- removed = 0
- i = 0
- while i < len(doc.paragraphs):
- p = doc.paragraphs[i]
- text = p.text.strip()
- if any(text.startswith(s) for s in REMOVE_SECTION_STARTS):
- delete_paragraph(p)
- removed += 1
- while i < len(doc.paragraphs):
- t = doc.paragraphs[i].text.strip()
- if not t or re.match(r"^\d+\.", t):
- delete_paragraph(doc.paragraphs[i])
- removed += 1
- continue
- break
- continue
- if text == MARKER:
- delete_paragraph(p)
- removed += 1
- if i < len(doc.paragraphs) and paragraph_has_image(doc.paragraphs[i]):
- delete_paragraph(doc.paragraphs[i])
- removed += 1
- if i < len(doc.paragraphs):
- t = doc.paragraphs[i].text.strip()
- if t.startswith("图:"):
- delete_paragraph(doc.paragraphs[i])
- removed += 1
- continue
- if text.startswith("图:") and "页面原型" in text:
- delete_paragraph(p)
- removed += 1
- continue
- i += 1
- doc.save(path)
- return removed
- def insert_after(paragraph: Paragraph, text: str = "") -> Paragraph:
- new_p = OxmlElement("w:p")
- paragraph._p.addnext(new_p)
- new_para = Paragraph(new_p, paragraph._parent)
- if text:
- new_para.add_run(text)
- return new_para
- def rr(draw, xy, radius=6, **kw):
- draw.rounded_rectangle(xy, radius=radius, **kw)
- def draw_text(draw, xy, value, font, fill="#444444"):
- draw.text(xy, value, fill=fill, font=font)
- def infer_domain(page_path: str) -> str:
- page_path = page_path.lower()
- if "quality" in page_path:
- return "quality"
- if "kanban" in page_path:
- return "kanban"
- for key in ("manufacturing", "sales", "supply", "warehouse"):
- if key in page_path:
- return key
- return "manufacturing"
- def infer_headers(desc: str) -> list[str]:
- patterns = [
- r"列表字段包括([^。]+)",
- r"字段包括([^。]+)",
- r"字段含([^。,]+)",
- r"列表字段为:([^。]+)",
- r"指标字段:([^。]+)",
- ]
- for pat in patterns:
- m = re.search(pat, desc)
- if not m:
- continue
- parts = re.split(r"[、,,/]", re.sub(r"\([^)]*\)", "", m.group(1)))
- headers = [p.strip() for p in parts if p.strip() and len(p.strip()) <= 12][:6]
- if headers:
- if "操作" not in headers:
- headers.append("操作")
- return headers
- if "路线" in desc and "工序" in desc:
- return ["路线编码", "名称", "适用物料", "版本", "状态", "操作"]
- return ["编码", "名称", "状态", "更新时间", "操作"]
- def infer_filters(desc: str) -> list[str]:
- m = re.search(r"支持按([^筛选]+)筛选", desc)
- if m:
- out = [p.strip() for p in re.split(r"[、/]", m.group(1)) if p.strip()][:4]
- if out:
- return out
- if "产线" in desc and "员工" in desc:
- return ["产线", "员工"]
- return ["关键字", "公司", "工厂"]
- def infer_add_label(title: str) -> str:
- return "新增" + title.replace("管理", "") if "管理" in title else "新增"
- def infer_form_fields(title: str) -> list[str]:
- if "工单控制参数" in title:
- return ["默认批次规则", "下达前置时间", "其他控制参数"]
- return ["参数项", "参数值", "说明"]
- def render_crud_png(path, *, module, menu, title, subtitle, filters, headers, add_label, row):
- xs, s, m, b = load_fonts()
- img = Image.new("RGB", (W, H), "#fafafa")
- draw = ImageDraw.Draw(img)
- draw.rectangle([0, 0, W, 44], fill="#ffffff", outline="#d9d9d9")
- draw_text(draw, (190, 14), f"Ai-DOP / {module}", s, "#444444")
- draw.rectangle([0, 44, 170, H], fill="#1f2937")
- draw_text(draw, (16, 72), module, s, "#ffffff")
- draw_text(draw, (32, 98), menu, xs, "#cbd5e1")
- draw.rectangle([170, 44, W, H], fill="#f3f4f6")
- rr(draw, [190, 58, W - 20, 118], radius=8, fill="#111827")
- draw_text(draw, (210, 72), title[:18], b, "#ffffff")
- draw_text(draw, (210, 98), subtitle[:70], xs, "#cbd5e1")
- x = 210
- for label in filters[:4]:
- draw_text(draw, (x, 132), label[:8], xs, "#444444")
- rr(draw, [x, 148, x + 110, 174], radius=4, outline="#d9d9d9", fill="#ffffff")
- x += 130
- bx = 210
- for label, color in [("查询", "#1677ff"), ("重置", "#ffffff"), (add_label[:8], "#52c41a")]:
- w = max(64, len(label) * 14 + 24)
- rr(draw, [bx, 188, bx + w, 216], radius=4, fill=color, outline=color if color != "#ffffff" else "#d9d9d9")
- draw_text(draw, (bx + 10, 194), label, s, "#ffffff" if color != "#ffffff" else "#444444")
- bx += w + 10
- rr(draw, [190, 232, W - 20, H - 20], radius=8, fill="#ffffff", outline="#d9d9d9")
- draw.rectangle([190, 232, W - 20, 266], fill="#fafafa", outline="#d9d9d9")
- col_w = max(80, (W - 230) // max(len(headers), 1))
- for i, h in enumerate(headers):
- draw_text(draw, (200 + i * col_w, 242), h[:8], xs, "#333333")
- if row:
- draw.line([190, 276, W - 20, 276], fill="#f0f0f0", width=1)
- for i, cell in enumerate(row[: len(headers)]):
- draw_text(draw, (200 + i * col_w, 286), str(cell)[:10], xs, "#666666")
- path.parent.mkdir(parents=True, exist_ok=True)
- img.save(path, format="PNG")
- def render_form_png(path, *, module, menu, title, subtitle, fields):
- xs, s, m, b = load_fonts()
- img = Image.new("RGB", (W, H), "#fafafa")
- draw = ImageDraw.Draw(img)
- draw.rectangle([170, 44, W, H], fill="#f3f4f6")
- rr(draw, [190, 58, W - 20, 118], radius=8, fill="#111827")
- draw_text(draw, (210, 72), title, b, "#ffffff")
- draw_text(draw, (210, 98), subtitle[:70], xs, "#cbd5e1")
- rr(draw, [190, 132, W - 20, H - 30], radius=8, fill="#ffffff", outline="#d9d9d9")
- y = 150
- for label in fields:
- draw_text(draw, (210, y), label, xs, "#444444")
- rr(draw, [210, y + 16, 520, y + 42], radius=4, outline="#d9d9d9", fill="#ffffff")
- y += 56
- rr(draw, [210, H - 58, 274, H - 30], radius=4, fill="#1677ff")
- draw_text(draw, (226, H - 52), "保存", s, "#ffffff")
- path.parent.mkdir(parents=True, exist_ok=True)
- img.save(path, format="PNG")
- def render_kpi_graph_png(path, title, subtitle):
- xs, s, m, b = load_fonts()
- img = Image.new("RGB", (W, H), "#fafafa")
- draw = ImageDraw.Draw(img)
- rr(draw, [190, 58, W - 20, 118], radius=8, fill="#111827")
- draw_text(draw, (210, 72), title, b, "#ffffff")
- draw_text(draw, (210, 98), subtitle[:70], xs, "#cbd5e1")
- rr(draw, [190, 132, W - 340, H - 20], radius=8, fill="#ffffff", outline="#d9d9d9")
- draw_text(draw, (210, 148), "L1/L2 指标关系图谱", s, "#333333")
- rr(draw, [W - 310, 132, W - 20, H - 20], radius=8, fill="#ffffff", outline="#d9d9d9")
- draw_text(draw, (W - 290, 148), "节点编辑抽屉", s, "#333333")
- img.save(path, format="PNG")
- def render_kpi_module_png(path, title, subtitle):
- xs, s, m, b = load_fonts()
- img = Image.new("RGB", (W, H), "#fafafa")
- draw = ImageDraw.Draw(img)
- rr(draw, [190, 58, W - 20, 118], radius=8, fill="#111827")
- draw_text(draw, (210, 72), title, b, "#ffffff")
- draw_text(draw, (210, 98), subtitle[:70], xs, "#cbd5e1")
- rr(draw, [190, 132, 520, H - 20], radius=8, fill="#ffffff", outline="#d9d9d9")
- draw_text(draw, (210, 148), "L1/L2 指标勾选", s, "#333333")
- rr(draw, [540, 132, W - 20, H - 20], radius=8, fill="#ffffff", outline="#d9d9d9")
- draw_text(draw, (560, 148), "布局预览", s, "#333333")
- img.save(path, format="PNG")
- def parse_s0_funcs(doc: Document) -> dict[str, dict]:
- funcs: dict[str, dict] = {}
- current = None
- desc_parts: list[str] = []
- for p in doc.paragraphs:
- text = p.text.strip()
- if is_func_heading(text):
- if current:
- funcs[current]["desc"] = "".join(desc_parts)
- m = HEADING_RE.match(text)
- current = m.group(1)
- funcs[current] = {
- "title": (m.group(4) or "").strip() or current,
- "desc": "",
- "req": "",
- "page": "",
- }
- desc_parts = []
- continue
- if not current:
- continue
- if text.startswith("**对应需求**:"):
- funcs[current]["req"] = text.split(":", 1)[1].strip()
- elif text.startswith("**前端页面**:") or text.startswith("**前端实现**:"):
- funcs[current]["page"] = text.split(":", 1)[1].strip(" `")
- elif text.startswith("**功能描述**:"):
- continue
- elif text.startswith("FUNC-") or text.startswith("补充:") or text.startswith("【") or text.startswith("图:"):
- continue
- elif text.startswith("**") or text in {"功能编号", "功能名称", "页面文件"}:
- continue
- elif text:
- desc_parts.append(text)
- if current:
- funcs[current]["desc"] = "".join(desc_parts)
- return funcs
- def build_s0_png(code: str, info: dict) -> Path:
- title = info["title"]
- desc = info["desc"]
- req = info.get("req") or code.replace("FUNC", "REQ")
- page = info.get("page", "")
- domain = infer_domain(page)
- menu = DOMAIN_MENU.get(domain, "数据建模")
- subtitle = f"{req} · {page}" if page else req
- out = BASE / "S0" / "prototype" / f"{code}.png"
- if "表单形式" in desc or code == "FUNC-S0-018":
- render_form_png(out, module="S0 运营建模", menu=menu, title=title, subtitle=subtitle, fields=infer_form_fields(title))
- return out
- if code in ("FUNC-S0-055", "FUNC-S0-056"):
- render_kpi_graph_png(out, title, subtitle)
- return out
- if code == "FUNC-S0-058":
- render_crud_png(
- out,
- module="S0 运营建模",
- menu=menu,
- title=title,
- subtitle=subtitle,
- filters=["模块", "层级", "关键字"],
- headers=["指标编码", "显示名称", "模块", "层级", "启用", "操作"],
- add_label="新增指标",
- row=["S1_L1_001", "订单评审周期", "S1", "L1", "是", "编辑"],
- )
- return out
- if code in ("FUNC-S0-057", "FUNC-S0-059"):
- render_kpi_module_png(out, title, subtitle)
- return out
- headers = infer_headers(desc)
- filters = infer_filters(desc)
- row = []
- for h in headers:
- if h == "操作":
- row.append("编辑")
- elif "编码" in h or h.endswith("号"):
- row.append("001")
- elif "名称" in h or "描述" in h:
- row.append("示例")
- elif "状态" in h or "启用" in h or "确认" in h:
- row.append("是")
- else:
- row.append("-")
- render_crud_png(
- out,
- module="S0 运营建模",
- menu=menu,
- title=title,
- subtitle=subtitle,
- filters=filters,
- headers=headers,
- add_label=infer_add_label(title),
- row=row,
- )
- return out
- def insert_prototypes(doc_path: Path, mapping: dict[str, Path]):
- doc = Document(doc_path)
- headings = [(i, func_code(p.text)) for i, p in enumerate(doc.paragraphs) if is_func_heading(p.text)]
- inserts = []
- for idx, (start, code) in enumerate(headings):
- if not code or code not in mapping:
- continue
- end = headings[idx + 1][0] if idx + 1 < len(headings) else len(doc.paragraphs)
- inserts.append((end - 1, code, mapping[code]))
- for insert_at, code, png in reversed(inserts):
- anchor = doc.paragraphs[insert_at]
- cap = insert_after(anchor, MARKER)
- cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
- pic_p = insert_after(cap)
- pic_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
- pic_p.add_run().add_picture(str(png), width=Inches(6.2))
- note = insert_after(pic_p, f"图:{code}")
- note.alignment = WD_ALIGN_PARAGRAPH.CENTER
- for r in note.runs:
- r.font.size = Pt(9)
- doc.save(doc_path)
- def main():
- for mod in ["S0", "S1", "S2"]:
- for kind in ["功能设计说明", "需求说明书"]:
- n = clean_document(BASE / mod / f"{mod}-{kind}.docx")
- print(f"cleaned {mod}-{kind}: {n}")
- s0_doc = Document(BASE / "S0" / "S0-功能设计说明.docx")
- funcs = parse_s0_funcs(s0_doc)
- mapping = {code: build_s0_png(code, info) for code, info in funcs.items()}
- insert_prototypes(BASE / "S0" / "S0-功能设计说明.docx", mapping)
- print(f"S0 prototypes: {len(mapping)}")
- from _insert_func_prototypes import build_templates, process_doc
- templates = build_templates(BASE / "_prototype_templates")
- for mod in ["S1", "S2"]:
- process_doc(mod, templates)
- if __name__ == "__main__":
- main()
|