Files
WanderQ-Web2.0/scripts/build_client_acceptance_doc.py

424 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = "正式中英文名、准确地址/地图点位、经营或预订主体、可接待境外客人方式、46 张原图及授权、当前房型与价格渠道"
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, "外观或环境 12 张、代表房型 23 张、公共空间或餐饮 12 张,尽量提供原始高清图。")
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()