feat(v1.10.0): 新增客户自助查询页 /customer

需求与确认:
- 完全公开、不加入口链接(用户确认)
- 输入手机尾号查询;同一尾号命中多个客户时必须补姓名,模糊匹配(用户确认)
- 只展示排单与完成情况:月历每日 已完成/总数、订阅套餐名与类型、到期时间、今日是否清完;不显示备注、打手、价格(用户确认)

实现:
- 新增 internal/web/handlers_portal.go:handleCustomerPortal + buildPortal
  - 尾号查客户(CustomersByTail,已过滤归档客户)
  - 多命中时返回候选家数并要求补姓名,姓名模糊匹配(不区分大小写)
  - 月度网格:按订阅覆盖日期统计每日应打/已完成,打卡数据复用 CheckinsBetween
  - 今日汇总、最近到期日、活跃订阅数
- 新增模板 pages/portal.html(Bootstrap 卡片 + 自定义 .pcal 月历)
- 路由 GET /customer(公开,无需登录)
- app.css 增加门户与 pcal 样式
- 数据库零改动,未新增表或列

测试:
- HTTP 验证 6 条路径全部 200:落地页、仅尾号(提示补姓名)、尾号+姓名模糊、姓名不匹配、尾号不存在、翻月
- 无头浏览器实拍 shots/portal-v110.png(公开页)
- 已知待办:门户标题与主按钮的品牌色内联覆盖未生效,需下一版修正(Bootstrap 后加载覆盖了主题色)
This commit is contained in:
subtrack
2026-10-20 03:11:36 +08:00
parent 8d9cf658a3
commit c84175b723
4 changed files with 366 additions and 0 deletions
+22
View File
@@ -1256,3 +1256,25 @@ fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
/* ---------- 客户自助查询 /customer ---------- */
.portal-wrap { max-width: 980px; margin: 0 auto; padding: 24px 16px 48px; }
.portal-head h1 { font-size: 1.6rem; font-weight: 800; margin: 0 0 4px; background: var(--ink-grad); -webkit-background-clip: text; background-clip: text; color: transparent; }
.portal-form { border-radius: 1rem; }
.portal-progress .bar { height: 8px; border-radius: 999px; background: var(--ink-1); overflow: hidden; margin-bottom: 6px; }
.portal-progress .bar i { display: block; height: 100%; background: var(--ink-grad); border-radius: 999px; }
.pcal { border: 1px solid var(--ink-line); border-radius: .85rem; overflow: hidden; }
.pcal-dow { display: grid; grid-template-columns: repeat(7, 1fr); background: var(--ink-1); }
.pcal-dow span { text-align: center; padding: 6px 0; font-size: .72rem; color: var(--ink-muted); }
.pcal-row { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
.pcal-cell { min-height: 54px; padding: 6px 8px; border-top: 1px solid var(--ink-line); border-right: 1px solid var(--ink-line); display: flex; flex-direction: column; gap: 2px; }
.pcal-cell:nth-child(7) { border-right: 0; }
.pcal-cell b { font-size: .82rem; color: var(--ink-ink); }
.pcal-cell span { font-size: .72rem; color: var(--ink-green-2); font-weight: 700; }
.pcal-cell.out { opacity: .4; }
.pcal-cell.today { background: var(--ink-grad-soft); box-shadow: inset 0 0 0 2px var(--ink-green-3); }
@media (max-width: 575px) {
.portal-wrap { padding: 16px 10px 32px; }
.pcal-cell { min-height: 46px; padding: 4px 5px; }
}