from pathlib import Path from docx import Document from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Inches, Pt, RGBColor OUT = Path(__file__).resolve().parents[1] / "docs" / "WanderQ官网客户验收与资料确认清单.docx" GREEN = "315A46" DEEP_GREEN = "18382A" LIGHT_GREEN = "EAF2ED" PALE_GREEN = "F5F8F6" GOLD = "A77A3F" INK = "26332C" MUTED = "68736C" LINE = "CCD8D0" WHITE = "FFFFFF" def set_cell_shading(cell, fill): tc_pr = cell._tc.get_or_add_tcPr() shd = tc_pr.find(qn("w:shd")) if shd is None: shd = OxmlElement("w:shd") tc_pr.append(shd) shd.set(qn("w:fill"), fill) def set_cell_margins(cell, top=100, start=120, bottom=100, end=120): tc = cell._tc tc_pr = tc.get_or_add_tcPr() tc_mar = tc_pr.first_child_found_in("w:tcMar") if tc_mar is None: tc_mar = OxmlElement("w:tcMar") tc_pr.append(tc_mar) for margin, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)): node = tc_mar.find(qn(f"w:{margin}")) if node is None: node = OxmlElement(f"w:{margin}") tc_mar.append(node) node.set(qn("w:w"), str(value)) node.set(qn("w:type"), "dxa") def set_cell_width(cell, width_dxa): tc_pr = cell._tc.get_or_add_tcPr() tc_w = tc_pr.find(qn("w:tcW")) if tc_w is None: tc_w = OxmlElement("w:tcW") tc_pr.append(tc_w) tc_w.set(qn("w:w"), str(width_dxa)) tc_w.set(qn("w:type"), "dxa") def set_table_geometry(table, widths, indent=120): table.autofit = False tbl_pr = table._tbl.tblPr tbl_w = tbl_pr.find(qn("w:tblW")) if tbl_w is None: tbl_w = OxmlElement("w:tblW") tbl_pr.append(tbl_w) tbl_w.set(qn("w:w"), str(sum(widths))) tbl_w.set(qn("w:type"), "dxa") tbl_ind = tbl_pr.find(qn("w:tblInd")) if tbl_ind is None: tbl_ind = OxmlElement("w:tblInd") tbl_pr.append(tbl_ind) tbl_ind.set(qn("w:w"), str(indent)) tbl_ind.set(qn("w:type"), "dxa") grid = table._tbl.tblGrid for child in list(grid): grid.remove(child) for width in widths: col = OxmlElement("w:gridCol") col.set(qn("w:w"), str(width)) grid.append(col) for row in table.rows: for index, cell in enumerate(row.cells): set_cell_width(cell, widths[index]) set_cell_margins(cell) cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER def set_repeat_table_header(row): tr_pr = row._tr.get_or_add_trPr() tbl_header = OxmlElement("w:tblHeader") tbl_header.set(qn("w:val"), "true") tr_pr.append(tbl_header) def set_cant_split(row): tr_pr = row._tr.get_or_add_trPr() cant_split = OxmlElement("w:cantSplit") cant_split.set(qn("w:val"), "true") tr_pr.append(cant_split) def set_run(run, size=11, bold=False, color=INK, font="Noto Sans CJK SC"): run.font.name = font run._element.get_or_add_rPr().rFonts.set(qn("w:ascii"), font) run._element.get_or_add_rPr().rFonts.set(qn("w:hAnsi"), font) run._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), font) run.font.size = Pt(size) run.bold = bold run.font.color.rgb = RGBColor.from_string(color) def add_text(paragraph, text, size=11, bold=False, color=INK): run = paragraph.add_run(text) set_run(run, size=size, bold=bold, color=color) return run def add_header_footer(section): header = section.header p = header.paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.RIGHT p.paragraph_format.space_after = Pt(0) add_text(p, "WANDERQ · CLIENT ACCEPTANCE", size=8.5, bold=True, color=MUTED) footer = section.footer p = footer.paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_before = Pt(0) add_text(p, "WanderQ 官网客户验收与资料确认清单 · 2026-08-12", size=8.5, color=MUTED) def add_title_block(doc): p = doc.add_paragraph() p.paragraph_format.space_before = Pt(10) p.paragraph_format.space_after = Pt(2) add_text(p, "WANDERQ / 万趣旅行", size=10.5, bold=True, color=GOLD) p = doc.add_paragraph() p.paragraph_format.space_before = Pt(0) p.paragraph_format.space_after = Pt(6) add_text(p, "官网客户验收与资料确认清单", size=27, bold=True, color=DEEP_GREEN) p = doc.add_paragraph() p.paragraph_format.space_after = Pt(16) add_text(p, "用于整站内容确认、真实资料补齐与正式发布前定版", size=12.5, color=MUTED) table = doc.add_table(rows=2, cols=2) set_table_geometry(table, [4680, 4680], indent=120) info = [ ("当前阶段", "已完成第一版开发与全站技术验收"), ("确认方式", "可在本文勾选、批注,或按编号逐项回复"), ("本轮范围", "贵州私家定制旅行官网(不含企业团建会务)"), ("建议顺序", "先确认整站方向,再分批补齐酒店与供应资料"), ] for index, (label, value) in enumerate(info): cell = table.cell(index // 2, index % 2) set_cell_shading(cell, LIGHT_GREEN if index < 2 else PALE_GREEN) p = cell.paragraphs[0] p.paragraph_format.space_after = Pt(1) add_text(p, label + "\n", size=8.5, bold=True, color=GREEN) add_text(p, value, size=10.5, color=INK) doc.add_paragraph() def add_heading(doc, number, title, intro=None): p = doc.add_paragraph(style="Heading 1") p.paragraph_format.keep_with_next = True add_text(p, f"{number} {title}", size=16, bold=True, color=DEEP_GREEN) if intro: p = doc.add_paragraph() p.paragraph_format.space_after = Pt(8) add_text(p, intro, size=10.5, color=MUTED) def add_subheading(doc, title): p = doc.add_paragraph(style="Heading 2") p.paragraph_format.keep_with_next = True add_text(p, title, size=12.5, bold=True, color=GREEN) def add_checkbox_line(doc, text, indent=0): p = doc.add_paragraph() p.paragraph_format.left_indent = Inches(indent) p.paragraph_format.space_after = Pt(4) p.paragraph_format.keep_together = True add_text(p, "☐ ", size=11.5, color=GREEN) add_text(p, text, size=10.5) def add_response_box(doc, prompt="客户意见 / 补充说明:", lines=2): table = doc.add_table(rows=1, cols=1) set_table_geometry(table, [9360], indent=120) cell = table.cell(0, 0) set_cell_shading(cell, PALE_GREEN) set_cell_margins(cell, top=120, start=160, bottom=120, end=160) p = cell.paragraphs[0] p.paragraph_format.space_after = Pt(3) add_text(p, prompt, size=9, bold=True, color=GREEN) for _ in range(lines): p = cell.add_paragraph() p.paragraph_format.space_after = Pt(3) add_text(p, "________________________________________________________________________________", size=9, color=LINE) doc.add_paragraph().paragraph_format.space_after = Pt(2) def add_status_table(doc): rows = [ ("整站视觉", "浅色、低饱和、欧美极简与编辑式排版", "请确认方向"), ("首屏主视觉", "保留自动轮播,并支持手动切换与暂停", "已按前期确认执行"), ("默认语言", "首次打开默认英文,可切换中文", "已按前期确认执行"), ("业务范围", "暂不加入企业团建会务", "已按前期确认执行"), ("第三方背书", "无正式文件前,不展示政府、媒体、协会等暗示性 Logo", "需资料后再上线"), ("英文评价", "无可核验原始评价和授权前,不编造评价", "需资料后再上线"), ] table = doc.add_table(rows=1, cols=3) set_table_geometry(table, [1900, 5060, 2400], indent=120) for i, text in enumerate(("项目", "当前处理", "本轮确认")): cell = table.rows[0].cells[i] set_cell_shading(cell, GREEN) p = cell.paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER add_text(p, text, size=9.5, bold=True, color=WHITE) set_repeat_table_header(table.rows[0]) for row_index, row_data in enumerate(rows): row = table.add_row() set_cant_split(row) cells = row.cells for i, text in enumerate(row_data): set_cell_shading(cells[i], WHITE if row_index % 2 == 0 else PALE_GREEN) p = cells[i].paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.LEFT if i == 1 else WD_ALIGN_PARAGRAPH.CENTER add_text(p, text, size=9.5, bold=(i == 0), color=INK) set_table_geometry(table, [1900, 5060, 2400], indent=120) def add_hotel_table(doc): hotels = [ "兴义峰兮・洞穴民宿", "朱砂古镇・放下云居", "雷公山树蛙部落", "榕江加宜梯田民宿", "台江拾仓来隐", "云屯・星空野奢树屋", "枕宿・枕山河", "兴义蘑菇野奢酒店", ] table = doc.add_table(rows=1, cols=4) widths = [700, 2800, 3960, 1900] set_table_geometry(table, widths, indent=120) headers = ("序号", "酒店候选", "请提供 / 确认", "状态") for i, text in enumerate(headers): cell = table.rows[0].cells[i] set_cell_shading(cell, GREEN) p = cell.paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER add_text(p, text, size=9.2, bold=True, color=WHITE) set_repeat_table_header(table.rows[0]) detail = "正式中英文名、准确地址/地图点位、经营或预订主体、可接待境外客人方式、4–6 张原图及授权、当前房型与价格渠道" for index, hotel in enumerate(hotels, 1): row = table.add_row() set_cant_split(row) cells = row.cells values = (f"{index:02d}", hotel, detail, "☐ 待提供\n☐ 已确认") for i, text in enumerate(values): set_cell_shading(cells[i], WHITE if index % 2 else PALE_GREEN) p = cells[i].paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER if i in (0, 3) else WD_ALIGN_PARAGRAPH.LEFT add_text(p, text, size=8.8, bold=(i == 1), color=INK) set_table_geometry(table, widths, indent=120) def add_material_block(doc, title, items): table = doc.add_table(rows=1, cols=1) set_table_geometry(table, [9360], indent=120) cell = table.cell(0, 0) set_cell_shading(cell, PALE_GREEN) set_cell_margins(cell, top=130, start=170, bottom=130, end=170) p = cell.paragraphs[0] p.paragraph_format.space_after = Pt(5) add_text(p, title, size=11, bold=True, color=DEEP_GREEN) for item in items: p = cell.add_paragraph() p.paragraph_format.space_after = Pt(3) add_text(p, "☐ ", size=10.5, color=GREEN) add_text(p, item, size=9.7) doc.add_paragraph().paragraph_format.space_after = Pt(2) def build(): doc = Document() section = doc.sections[0] section.page_width = Inches(8.5) section.page_height = Inches(11) section.top_margin = Inches(0.78) section.bottom_margin = Inches(0.75) section.left_margin = Inches(0.9) section.right_margin = Inches(0.9) section.header_distance = Inches(0.35) section.footer_distance = Inches(0.35) add_header_footer(section) normal = doc.styles["Normal"] normal.font.name = "Noto Sans CJK SC" normal._element.rPr.rFonts.set(qn("w:ascii"), "Noto Sans CJK SC") normal._element.rPr.rFonts.set(qn("w:hAnsi"), "Noto Sans CJK SC") normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Noto Sans CJK SC") normal.font.size = Pt(10.5) normal.font.color.rgb = RGBColor.from_string(INK) normal.paragraph_format.space_after = Pt(6) normal.paragraph_format.line_spacing = 1.25 for name, size, before, after in (("Heading 1", 16, 16, 8), ("Heading 2", 12.5, 12, 6)): style = doc.styles[name] style.font.name = "Noto Sans CJK SC" style._element.rPr.rFonts.set(qn("w:ascii"), "Noto Sans CJK SC") style._element.rPr.rFonts.set(qn("w:hAnsi"), "Noto Sans CJK SC") style._element.rPr.rFonts.set(qn("w:eastAsia"), "Noto Sans CJK SC") style.font.size = Pt(size) style.font.bold = True style.font.color.rgb = RGBColor.from_string(DEEP_GREEN if name == "Heading 1" else GREEN) style.paragraph_format.space_before = Pt(before) style.paragraph_format.space_after = Pt(after) add_title_block(doc) add_heading(doc, "01", "客户如何使用本清单", "请先完成整站方向确认;具体酒店、供应商和交易资料可以分批补充。任何暂时无法确认的项目,官网都会继续使用“待确认”或安全边界表达,不会替客户作无依据承诺。") add_checkbox_line(doc, "整站方向无重大问题,可继续进入真实资料补齐和发布前定版。") add_checkbox_line(doc, "整体方向认可,但需要按本文编号进行局部修改。") add_checkbox_line(doc, "需要重新讨论某个栏目结构;请注明栏目和原因。") add_response_box(doc, "请填写本轮总意见:", 3) add_heading(doc, "02", "整站方向确认", "以下项目是此前沟通结果与当前官网的执行方式,请客户确认是否继续沿用。") add_status_table(doc) add_subheading(doc, "请客户确认") add_checkbox_line(doc, "认可当前整站视觉方向,不需要大规模重做。") add_checkbox_line(doc, "认可当前首页到详情页的栏目结构和内容顺序。") add_checkbox_line(doc, "认可英文为默认语言,中文作为切换语言。") add_checkbox_line(doc, "认可暂不加入企业团建会务。") add_checkbox_line(doc, "认可未提供证明的卖点、资质和背书暂不上线。") add_response_box(doc, "如有页面级修改,请写“页面名称 + 位置 + 修改内容”:", 3) add_heading(doc, "03", "8 家特色酒店资料确认", "官网已经按照客户提出的 8 家住宿候选完成栏目结构,但目前不使用无关风景图冒充酒店实景。请按酒店逐家提供下列资料。") add_hotel_table(doc) add_subheading(doc, "每家酒店图片最低要求") add_checkbox_line(doc, "外观或环境 1–2 张、代表房型 2–3 张、公共空间或餐饮 1–2 张,尽量提供原始高清图。") add_checkbox_line(doc, "明确图片对应的酒店、房型、拍摄时间和是否允许官网商业使用。") add_checkbox_line(doc, "如图片来自酒店官方或第三方平台,请提供授权依据,不能仅提供网络截图。") add_response_box(doc, "酒店资料负责人 / 预计提供日期:", 2) add_heading(doc, "04", "体验供应与安全资料", "客户文档中的体验方向已完整进入网站。正式对外销售前,需要把“概念内容”落实到具名运营方、具名人员和可执行条件。") add_material_block(doc, "户外体验", [ "洞穴、瀑降、溯溪、漂流、徒步/骑行、高桥挑战等项目的具名运营方与联系人。", "领队、向导、教练或救援人员的相关资质、经验与可公开介绍。", "适用年龄、体能/健康限制、装备清单、天气/水位取消条件与替代方案。", "保险责任、应急通信、撤离路线、最近医疗资源和事故处理流程。", "真实产品时长、人数上下限、当前价格、可约日期和取消规则。", ]) add_material_block(doc, "文化与村寨体验", [ "苗绣/蜡染、侗族技艺、水族马尾绣、布依技艺等具名老师、工坊和地点。", "村寨拜访、家庭接待、住宿、家宴、节庆参与的主人/社区/运营主体同意。", "课程可完成内容、材料与工具、儿童参与边界、拍摄同意和作品带走/后寄方式。", "英文支持由谁提供,以及费用如何分配给老师、主人或社区。", ]) add_material_block(doc, "特别场景与历史演绎", [ "洞穴咖啡/晚宴、山野咖啡、露营的场地许可、餐饮、消防、电力、天气与撤离资料。", "《战青岩》《娄山关大捷》等现有产品的运营主体、当前场次、票价、英文接待与包团条件。", "客户提出但尚未成为现有产品的二期共创内容,需单独确认开发主体、预算和周期。", ]) add_response_box(doc, "供应商与安全资料负责人 / 预计提供日期:", 2) add_heading(doc, "05", "合作背书与真实评价", "这一部分能够显著增强官网信任感,但必须保证来源真实、关联准确并获得公开使用许可。") add_subheading(doc, "合作、媒体与资质") add_checkbox_line(doc, "合作机构或政府单位:正式名称、合作内容、有效期、证明文件及 Logo 使用授权。") add_checkbox_line(doc, "媒体报道或推荐:原始报道链接/文件、准确标题和允许引用的内容。") add_checkbox_line(doc, "协会会员、行业认证或获奖:证书、有效期、认证主体和允许展示的标识。") add_subheading(doc, "真实英文客户评价") add_checkbox_line(doc, "英文原评(不由开发侧翻译成“原评”)、客户姓名或允许公开的署名方式。") add_checkbox_line(doc, "客户照片、国籍/地区、人数、实际参加线路或体验、出行时间。") add_checkbox_line(doc, "照片和评价公开用于官网及营销材料的授权。") add_response_box(doc, "如暂时没有完整资料,可注明“本次不上线”:", 2) add_heading(doc, "06", "价格、预订与法律信息", "官网进入正式发布和接单阶段前,请确认以下交易信息,避免页面卖点与实际报价、履约条件不一致。") for item in [ "各线路、体验、酒店和用车是展示参考价、起价、询价制,还是暂不公开价格。", "报价包含与不包含:门票、住宿、餐饮、车辆、司导、装备、保险、税费和服务费。", "预订金比例、尾款时间、支付方式、汇率或跨境支付费用。", "取消、退款、日期变更、人数变化、天气取消和供应商取消条款。", "公司/经营主体正式名称、注册地址、联系人、客服邮箱及适用的隐私与服务条款。", "客户咨询信息由谁接收、如何跟进、保存多久,以及是否需要单独的隐私同意。", ]: add_checkbox_line(doc, item) add_response_box(doc, "价格与合同资料负责人 / 预计提供日期:", 2) add_heading(doc, "07", "回复优先级与最终确认", "为了不影响当前设计确认,资料可以按以下顺序分批提供。") priorities = [ ("P0 / 本轮确认", "整站视觉、结构、默认英文、业务范围,以及需要立即修改的页面。"), ("P1 / 发布前必须", "酒店原图与授权、具名供应商与安全资料、价格/取消条款、经营与法律信息。"), ("P2 / 可后补增强", "合作背书、媒体材料、真实英文评价和新增内容。"), ] table = doc.add_table(rows=1, cols=2) set_table_geometry(table, [2200, 7160], indent=120) for index, (level, detail) in enumerate(priorities): row = table.rows[0] if index == 0 else table.add_row() set_cell_shading(row.cells[0], GREEN if index == 0 else LIGHT_GREEN) set_cell_shading(row.cells[1], PALE_GREEN) p = row.cells[0].paragraphs[0] add_text(p, level, size=10, bold=True, color=WHITE if index == 0 else GREEN) p = row.cells[1].paragraphs[0] add_text(p, detail, size=10) set_table_geometry(table, [2200, 7160], indent=120) doc.add_paragraph() add_checkbox_line(doc, "确认当前官网可作为客户整站验收版本继续推进。") add_checkbox_line(doc, "确认按本清单分批补充资料;未提供或未核验内容不作为官网承诺。") add_checkbox_line(doc, "确认客户提供的文字、图片、Logo、评价与证明材料拥有相应公开和商业使用权限。") add_response_box(doc, "客户代表 / 日期 / 其他说明:", 4) OUT.parent.mkdir(parents=True, exist_ok=True) doc.save(OUT) print(OUT) if __name__ == "__main__": build()