复古车票很适合做旅行札记封面站名、日期、座位各自占一小块纸面。真正做成工具时先要回答一个朴素问题——用户改了字段下载的文件会不会跟着变如果页面看起来能编辑导出却仍是默认站名装饰就掩盖了功能缺口。一份数据同时生成预览与文件我把车票当作结构化数据出发站、到达站、日期、座位。校验完成后只生成一份 SVG 字符串预览和下载使用同一份内容。这样至少不会出现“屏幕是成都文件里还是上海”的双份状态。日期应检查真实日历不能只匹配四位数、两位数、两位数2026-02-30也符合那个正则。下面用 UTC 日期回转检查这是没有时刻含义的票面日期不拿用户时区猜测出发时间。站名限制长度并转义 XML既避免撑破版式也防止用户输入变成额外标签。条纹只当条纹底部黑白条纹由固定数组画出明确写着“装饰条纹不可检票”。它不是 Code 39、二维码或真实订单号。一个随意排列的黑条图案即使很像条码也不能宣称被扫码器读取。若产品真的需要机器可读编码应选定标准、编码库和实际扫描测试另作实现。SVG 路径与图形规范为矢量导出提供了明确的几何语义但不替应用完成字段检查。例子在 Node 中生成ticket.svg可用支持 SVG 的查看器打开没有调用订票服务没有产生真实车票或购买凭证。本地验证包含闰日、非法日期、XML 特殊字符以及导出内容确实包含用户输入。浏览器交互、下载手势与字体效果还需要界面测试。对这类小工具我会先保证“你填的就是你拿走的”再给纸边加一圈旧旧的毛刺。可独立运行的核心例子const fsrequire(node:fs); const escss.replace(/[]/g,c({:amp;,:lt;,:gt;,:quot;,:apos;}[c])); function dateOK(s) { if(!/^20\d\d-\d\d-\d\d$/.test(s)) return false; const dnew Date(sT00:00:00Z); return Number.isFinite(d.getTime()) d.toISOString().slice(0,10)s; } function ticket({from,to,date,seat}) { for(const s of [from,to,seat]) if(typeof s!string||!s.trim()||[...s].length12) throw Error(字段需1..12字); if(!dateOK(date)) throw Error(日期不存在或超出2000..2099); const labels[${from} → ${to},date,座位 ${seat}]; let x30; const bars[3,1,2,4,1,3,2,1,4,2,3,1].map(w{ const outrect x${x} y142 width${w*2} height22/;xw*24;return out; }); return svg xmlnshttp://www.w3.org/2000/svg viewBox0 0 500 200 rect width500 height200 rx12 fill#f1e2c8/ ${labels.map((s,i)text x30 y${38i*36} font-size24${esc(s)}/text).join()} ${bars.join()}text x30 y188 font-size12装饰条纹不可检票/text/svg; } if(dateOK(2026-02-30)||!dateOK(2028-02-29)) throw Error(日期校验失败); const sticket({from:成都,to:山间,date:2026-10-24,seat:07A}); if(!s.includes(山间lt;amp;)||!s.includes(07A)) throw Error(字段未进入导出); fs.writeFileSync(ticket.svg,s);console.log(已生成 ticket.svg);