250 lines
19 KiB
Python
250 lines
19 KiB
Python
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)
|