Files
XQKqueue/deliverables/product-document/build_product_doc.py
2026-07-12 15:53:24 +08:00

250 lines
19 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.section import WD_SECTION
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
ROOT = Path('/Users/inmanw/公司/calllinesystem/deliverables/product-document')
SHOTS = ROOT / 'screenshots'
OUT = ROOT / '景区排队叫号系统_产品功能手册_业务熟悉版.docx'
BLUE = '2E74B5'
DARK_BLUE = '1F4D78'
MUTED = '667085'
LIGHT = 'E8EEF5'
GREEN = '1F6B43'
doc = Document()
sec = doc.sections[0]
sec.page_width, sec.page_height = Inches(8.5), Inches(11)
sec.top_margin = sec.bottom_margin = sec.left_margin = sec.right_margin = Inches(1)
sec.header_distance = sec.footer_distance = Inches(.492)
def font(run, size=11, bold=False, color='1F2937', name='Noto Sans CJK SC'):
run.font.name = name
run._element.get_or_add_rPr().rFonts.set(qn('w:eastAsia'), name)
run._element.get_or_add_rPr().rFonts.set(qn('w:ascii'), name)
run._element.get_or_add_rPr().rFonts.set(qn('w:hAnsi'), name)
run.font.size, run.bold, run.font.color.rgb = Pt(size), bold, RGBColor.from_string(color)
return run
styles = doc.styles
normal = styles['Normal']
normal.font.name, normal.font.size = 'Noto Sans CJK SC', Pt(11)
normal._element.rPr.rFonts.set(qn('w:eastAsia'), 'Noto Sans CJK SC')
normal.paragraph_format.space_after = Pt(6)
normal.paragraph_format.line_spacing = 1.25
for name, size, color, before, after in [
('Heading 1',16,BLUE,18,10),('Heading 2',13,BLUE,14,7),('Heading 3',12,DARK_BLUE,10,5)]:
st=styles[name]; st.font.name='Noto Sans CJK SC'; st.font.size=Pt(size); st.font.bold=True; st.font.color.rgb=RGBColor.from_string(color)
st._element.rPr.rFonts.set(qn('w:eastAsia'),'Noto Sans CJK SC')
st.paragraph_format.space_before=Pt(before); st.paragraph_format.space_after=Pt(after); st.paragraph_format.keep_with_next=True
def shade(cell, fill):
tcPr=cell._tc.get_or_add_tcPr(); shd=tcPr.find(qn('w:shd'))
if shd is None: shd=OxmlElement('w:shd'); tcPr.append(shd)
shd.set(qn('w:fill'),fill)
def set_cell_margins(cell, top=80, start=120, bottom=80, end=120):
tc=cell._tc; tcPr=tc.get_or_add_tcPr(); tcMar=tcPr.first_child_found_in('w:tcMar')
if tcMar is None: tcMar=OxmlElement('w:tcMar'); tcPr.append(tcMar)
for m,v in [('top',top),('start',start),('bottom',bottom),('end',end)]:
el=tcMar.find(qn('w:'+m))
if el is None: el=OxmlElement('w:'+m); tcMar.append(el)
el.set(qn('w:w'),str(v)); el.set(qn('w:type'),'dxa')
def set_repeat_table_header(row):
trPr=row._tr.get_or_add_trPr(); el=OxmlElement('w:tblHeader'); el.set(qn('w:val'),'true'); trPr.append(el)
def set_table_widths(table, widths):
table.autofit=False
for row in table.rows:
for i,w in enumerate(widths): row.cells[i].width=Inches(w)
tblPr=table._tbl.tblPr; tblW=tblPr.find(qn('w:tblW'))
tblW.set(qn('w:w'),'9360'); tblW.set(qn('w:type'),'dxa')
tblInd=tblPr.find(qn('w:tblInd'))
if tblInd is None: tblInd=OxmlElement('w:tblInd'); tblPr.append(tblInd)
tblInd.set(qn('w:w'),'120'); tblInd.set(qn('w:type'),'dxa')
def add_table(headers, rows, widths):
t=doc.add_table(rows=1, cols=len(headers)); t.style='Table Grid'; t.alignment=WD_TABLE_ALIGNMENT.LEFT
for i,h in enumerate(headers):
c=t.rows[0].cells[i]; shade(c,LIGHT); c.vertical_alignment=WD_CELL_VERTICAL_ALIGNMENT.CENTER
p=c.paragraphs[0]; p.alignment=WD_ALIGN_PARAGRAPH.CENTER; font(p.add_run(h),10,bold=True,color=DARK_BLUE)
set_repeat_table_header(t.rows[0])
for row in rows:
cells=t.add_row().cells
for i,val in enumerate(row):
cells[i].vertical_alignment=WD_CELL_VERTICAL_ALIGNMENT.CENTER
p=cells[i].paragraphs[0]; font(p.add_run(str(val)),9.5,color='263238')
if i==0: p.runs[0].bold=True
for c in cells: set_cell_margins(c)
for c in t.rows[0].cells: set_cell_margins(c)
set_table_widths(t,widths)
doc.add_paragraph().paragraph_format.space_after=Pt(2)
return t
def add_bullets(items):
for item in items:
p=doc.add_paragraph(style='List Bullet'); p.paragraph_format.left_indent=Inches(.375); p.paragraph_format.first_line_indent=Inches(-.188); p.paragraph_format.space_after=Pt(4); p.paragraph_format.line_spacing=1.25
font(p.add_run(item),10.5)
def add_steps(items):
for item in items:
p=doc.add_paragraph(style='List Number'); p.paragraph_format.left_indent=Inches(.375); p.paragraph_format.first_line_indent=Inches(-.188); p.paragraph_format.space_after=Pt(4); p.paragraph_format.line_spacing=1.25
font(p.add_run(item),10.5)
def add_note(title, text):
t=doc.add_table(rows=1,cols=1); t.autofit=False; t.alignment=WD_TABLE_ALIGNMENT.LEFT
set_table_widths(t,[6.5]); c=t.cell(0,0); shade(c,'F4F6F9'); set_cell_margins(c,140,180,140,180)
p=c.paragraphs[0]; font(p.add_run(title+''),10.5,True,DARK_BLUE); font(p.add_run(text),10.5)
doc.add_paragraph().paragraph_format.space_after=Pt(1)
def add_figure(filename, caption, width=6.35):
p=doc.add_paragraph(); p.alignment=WD_ALIGN_PARAGRAPH.CENTER; p.paragraph_format.keep_with_next=True
shape = p.add_run().add_picture(str(SHOTS/filename),width=Inches(width))
shape._inline.docPr.set('descr', caption)
shape._inline.docPr.set('title', caption)
cp=doc.add_paragraph(); cp.alignment=WD_ALIGN_PARAGRAPH.CENTER; cp.paragraph_format.space_after=Pt(8); cp.paragraph_format.keep_with_next=False
font(cp.add_run(caption),9,color=MUTED)
def page_break(): doc.add_page_break()
# Header/footer
hp=sec.header.paragraphs[0]; hp.alignment=WD_ALIGN_PARAGRAPH.LEFT; font(hp.add_run('景区排队叫号系统|产品功能手册'),9,color=MUTED)
fp=sec.footer.paragraphs[0]; fp.alignment=WD_ALIGN_PARAGRAPH.RIGHT
font(fp.add_run('业务熟悉版 '),9,color=MUTED)
fld=OxmlElement('w:fldSimple'); fld.set(qn('w:instr'),'PAGE'); fp._p.append(fld)
# Cover
for _ in range(4): doc.add_paragraph()
p=doc.add_paragraph(); p.alignment=WD_ALIGN_PARAGRAPH.CENTER; font(p.add_run('产品功能手册'),30,True,GREEN)
p=doc.add_paragraph(); p.alignment=WD_ALIGN_PARAGRAPH.CENTER; font(p.add_run('景区排队叫号系统'),20,True,DARK_BLUE)
p=doc.add_paragraph(); p.alignment=WD_ALIGN_PARAGRAPH.CENTER; p.paragraph_format.space_before=Pt(10); font(p.add_run('面向业务人员的端、功能与页面说明'),13,color=MUTED)
for _ in range(5): doc.add_paragraph()
add_note('阅读对象','景区运营、现场管理、客服、培训及相关业务协作人员。')
add_note('文档范围','基于当前可运行版本整理,覆盖员工端、游客端、现场公示大屏端和管理端。')
p=doc.add_paragraph(); p.alignment=WD_ALIGN_PARAGRAPH.CENTER; p.paragraph_format.space_before=Pt(22); font(p.add_run('版本日期2026 年 7 月 12 日'),10,color=MUTED)
page_break()
doc.add_heading('1. 产品概览', level=1)
doc.add_paragraph('本系统用于景区多项目的排队取号、批量叫号、游客进度查询和现场信息公示。中心服务统一维护队列,员工负责现场操作,游客查看个人状态,管理人员查看全局运营并维护项目与账号。')
add_table(['','主要使用者','核心任务','典型设备'],[
('员工端','项目一线员工','代游客取号、查看队列、单号/批量叫号、切换项目','手机 / H5'),
('游客端','已取号游客','查看本人号码、前方数量、预计等待、叫号入口与官方提示','手机 / H5'),
('公示大屏端','现场游客','查看当前叫号、最近叫号、后续号段预计','电视 / 大屏浏览器'),
('管理端','管理员、运营负责人','看运营概览、维护项目规则、维护账号、监控各项目大屏','电脑浏览器'),
], [1.05,1.25,3.05,1.15])
doc.add_heading('1.1 核心业务流程', level=2)
add_steps([
'员工在“取号”页面录入游客手机号,可补充姓氏和称谓,系统生成连续排队号码及游客状态页。',
'号码进入项目等待队列;员工可在“叫号”页面查看下一号码、剩余数量和已等待时长。',
'员工执行“快速叫下一个号”或输入数量后“叫号段”;新一批按先到先服务顺序叫出。',
'游客状态页自动更新等待进度;被叫号后突出显示前往入口与叫号时间。',
'公示大屏同步展示当前叫号、最近叫号和后续号段预计;管理端汇总各项目运营状态。',
])
doc.add_heading('1.2 角色与数据可见范围', level=2)
add_bullets([
'员工账号仅能操作已分配的项目;多个项目可在“我的”中切换。',
'管理员可查看所有项目,创建/维护项目和账号,并分配员工可操作项目。',
'游客使用私密链接查看本人排队状态,只显示手机号尾号,不展示完整手机号。',
'现场公示大屏只展示公开叫号信息,不展示游客姓名或手机号。',
])
doc.add_heading('1.3 主要状态', level=2)
add_table(['对象','状态','业务含义'],[
('项目','未开放','尚未开始接待,不允许正常取号/叫号。'),('项目','运行中','正常取号、排队与叫号。'),('项目','已暂停','队列号码保留,暂停期间不继续正常叫号,预计时间可能暂停计算。'),('项目','已结束','当日或本轮服务结束。'),
('号码','等待中','已进入队列,等待叫号。'),('号码','已叫号','当前已被叫出,应前往入口。'),('号码','已到场/服务中/已完成','现场已确认并进入后续服务环节。'),('号码','已过号/已取消','本号码不再按正常等待流程继续。'),
], [1.0,1.25,4.25])
page_break()
doc.add_heading('2. 员工端', level=1)
doc.add_paragraph('员工端是现场一线操作入口,采用手机端底部导航,包含“叫号、取号、我的”三项任务。员工与管理端使用独立登录入口及独立会话。')
doc.add_heading('2.1 员工登录', level=2)
add_table(['功能名称','功能描述'], [('账号登录','员工输入账号和密码进入本人已授权项目;管理员账号不能从员工入口登录。'),('退出登录','结束当前员工会话,回到员工登录页。')],[1.45,5.05])
add_figure('01-staff-login.png','图 2-1 员工端登录页',3.0)
doc.add_heading('2.2 叫号', level=2)
add_table(['功能名称','功能描述'],[
('快速叫下一个号','按队列先后顺序叫出下一个等待号码。'),('批量叫号','输入 1—100 的数量后,按先进先出顺序叫出连续号段。'),('当前叫号指标','展示最新叫到、下一个号、最末号数、剩余未叫号和最末号预计时长。'),('待叫队列','逐号展示等待号码和已等待时长,支持上下滑动查看。'),('写入保护','项目未运行、队列为空或数据过期时,系统会禁用不适合执行的写操作并给出原因。'),
], [1.45,5.05])
add_note('业务规则','每次叫下一批时,系统会自动结束上一批,再从等待队列中按顺序叫出指定数量。')
add_figure('02-staff-call.png','图 2-2 员工端叫号页',3.0)
doc.add_heading('2.3 取号', level=2)
add_table(['功能名称','功能描述'],[
('创建排队号码','录入游客手机号后创建号码;姓氏和称谓为补充信息。'),('重复手机号确认','同一手机号已有活动号码时,先提示风险,员工确认后可继续共用手机号取新号。'),('上下号提示','显示上个取号和预计下个号码,便于现场口头核对。'),('游客状态链接','取号成功后提供游客私密状态页,可直接打开或交给游客查看。'),
], [1.45,5.05])
add_figure('03-staff-ticket.png','图 2-3 员工端取号页',3.0)
doc.add_heading('2.4 我的 / 项目切换', level=2)
add_table(['功能名称','功能描述'],[
('账号信息','显示姓名/显示名、登录账号和账号角色。'),('当前项目','显示目前正在操作的项目。'),('切换项目','账号被分配多个项目时,可从授权列表中选择并确认切换。')],[1.45,5.05])
add_figure('04-staff-me.png','图 2-4 员工端“我的”页',3.0)
page_break()
doc.add_heading('3. 游客端', level=1)
doc.add_paragraph('游客端通过每张排队单的私密链接访问,无需账号登录。页面每 3 秒自动刷新,并提供手动刷新;网络中断或数据延迟时会显示提示。')
add_table(['功能名称','功能描述'],[
('个人号码','显示本人排队号码和所属项目。'),('等待进度','等待中显示前方号码数、当前排位和预计等待时间。'),('叫号提醒','号码被叫出后,突出显示应前往的入口及叫号时间。'),('状态说明','对等待、叫号、到场、服务中、完成、过号和取消等状态提供对应文案。'),('隐私展示','仅显示手机号尾号,避免在页面中暴露完整手机号。'),('官方提示','展示管理员为项目配置的游客注意事项。'),('实时状态','自动更新;支持手动刷新,并显示连接中断或更新延迟。'),
], [1.45,5.05])
add_note('暂停场景','项目暂停时,等待号码继续保留,页面提示“项目暂时暂停”;项目恢复后重新估算等待时间。')
add_figure('05-visitor-waiting.png','图 3-1 游客端等待状态页',3.15)
page_break()
doc.add_heading('4. 现场公示大屏端', level=1)
doc.add_paragraph('公示大屏用于现场公开播报单个项目的叫号进度,页面无需登录,通过项目专属展示地址打开,适合全屏展示。')
add_table(['功能名称','功能描述'],[
('项目运行状态','展示运行中、暂停、未开放或结束等状态。'),('当前叫号','以最大视觉层级展示当前号段,并提示前往入口。'),('最近叫号','展示最近 5 个叫号批次及时间,便于游客核对。'),('后续号段预计','基于当前队列与项目等待规则,显示后续号段和预计等待;内容较多时自动轮播。'),('等待汇总','显示当前等待号码总数。'),('时钟日期','展示当前时间、日期和星期。'),('异常提示','连接中断或数据延迟时显示上次更新时间,并支持重新连接。'),
], [1.45,5.05])
add_note('隐私原则','公示屏仅显示号码和项目公开状态,不显示游客姓名、手机号或其他个人信息。')
add_figure('06-public-display.png','图 4-1 单项目现场公示大屏',6.35)
page_break()
doc.add_heading('5. 管理端', level=1)
doc.add_paragraph('管理端面向景区管理员与运营负责人,采用电脑端布局,包含运营概览、项目管理、账号管理和大屏中心四个模块。')
doc.add_heading('5.1 管理端登录', level=2)
add_table(['功能名称','功能描述'], [('管理员登录','管理员使用专属入口登录;员工账号不能从管理入口登录。'),('独立会话','管理端与员工端会话相互独立,可在同一浏览器同时保持两种身份。')],[1.45,5.05])
add_figure('07-admin-login.png','图 5-1 管理端登录页',5.7)
doc.add_heading('5.2 运营概览', level=2)
add_table(['功能名称','功能描述'],[
('运营汇总','显示当前运行项目数和总等待号码数。'),('项目队列总览','逐项目展示等待数量、当前叫号、项目状态、设备状态和更新时间。'),('筛选与搜索','按项目状态筛选,并按项目名称搜索。'),('刷新数据','主动读取最新运营信息。')],[1.45,5.05])
add_figure('08-admin-overview.png','图 5-2 管理端运营概览',6.35)
doc.add_heading('5.3 项目管理', level=2)
add_table(['功能名称','功能描述'],[
('项目列表','查看项目名称、编码、票号格式和运行状态。'),('创建项目','录入项目名称和项目编码,创建后再配置运行规则。'),('基础信息维护','修改项目名称、编码等基础标识。'),('项目状态维护','设置未开放、运行中、已暂停或已结束。'),('叫号与过号规则','维护预计每次叫号数和过号宽限时间。'),('等待时间算法','支持“按平均叫号节奏”或“连续放行”两种方式,并配置间隔/速率和缓冲分钟。'),('游客官方提示','配置游客状态页显示的项目提示,最多 240 字。')],[1.45,5.05])
add_figure('09-admin-projects.png','图 5-3 项目列表',6.35)
add_figure('12-admin-project-maintenance.png','图 5-4 项目维护与等待规则',6.15)
doc.add_heading('5.4 账号管理', level=2)
add_table(['功能名称','功能描述'],[
('账号列表','查看账号、角色、启用状态和所属项目。'),('创建账号','创建新的员工账号并设置初始密码。'),('角色维护','在员工与管理员角色之间调整。'),('启用/停用','控制账号是否可继续登录。'),('项目授权','为员工账号勾选可操作项目;未分配项目的员工无法操作项目队列。')],[1.45,5.05])
add_figure('10-admin-accounts.png','图 5-5 账号管理',6.35)
doc.add_heading('5.5 大屏中心', level=2)
add_table(['功能名称','功能描述'],[
('多项目监控墙','在一个页面同时查看各项目运行状态、当前叫号、后续号段预计和等待数量。'),('自动轮播预计','后续预计超过一页时,每 6 秒切换。'),('单项目全屏','点击某项目“全屏展示”,将对应项目卡片放大为现场展示视图。')],[1.45,5.05])
add_figure('11-admin-display-center.png','图 5-6 管理端大屏中心',6.35)
page_break()
doc.add_heading('6. 业务使用建议', level=1)
doc.add_heading('6.1 开园前', level=2)
add_bullets(['管理员确认当日开放项目状态为“运行中”,未开放项目保持“未开放”。','核对员工账号是否启用、是否分配到正确项目。','检查项目预计等待参数和游客官方提示是否符合当天运营安排。','打开现场公示屏并确认时间、项目状态和数据更新正常。'])
doc.add_heading('6.2 运营中', level=2)
add_bullets(['员工取号前核对手机号,遇重复手机号提示时确认是否确需为同行游客再次取号。','优先使用“快速叫下一个号”;需要集中放行时再使用批量叫号。','项目临时停止服务时,由管理员及时调整为“已暂停”,避免业务人员继续误操作。','管理人员通过运营概览关注等待量、当前叫号和数据更新时间。'])
doc.add_heading('6.3 收园或项目结束', level=2)
add_bullets(['将项目状态调整为“已结束”,避免继续取号。','现场员工确认当前批次与剩余队列处置方案。','关闭或切换公示屏前,确认现场已无需要继续等待的游客。'])
add_note('版本说明','本文档描述当前可运行版本。后续新增功能、页面或业务规则时,应同步更新“端—功能—描述—截图”及版本日期。')
doc.core_properties.title='景区排队叫号系统 产品功能手册(业务熟悉版)'
doc.core_properties.subject='端、功能名称、功能描述与页面截图'
doc.core_properties.author='产品团队'
doc.save(OUT)
print(OUT)