Files
subtrack/internal/web/templates/pages/customer_detail.html
T
subtrack a2542e1135
build / build-and-test (push) Successful in 3m36s
feat(v1.7): 计数改已完成/总数、客户月历、套餐标签合并、批量开通、触控模式、按次到期
1) 日历计数口径
- 工作台与大屏的格子统一改为「已完成 / 总数」(如 2/5 = 共 5 单、做完 2 单)

2) 客户详情·当月排单
- 新增客户专属月历:每天显示 已完成/总数,可翻月,点某天跳转到日历对应日期
- 没有排单的日期淡化显示

3) 套餐标签与页签合并
- packages 新增 tag 列(可空),套餐编辑页可填
- 日历/大屏页签按标签合并:同标签套餐合成一个页签;勾了「独立显示」的套餐额外再开一个自己的页签
- 大屏同样支持标签筛选,链接参数为 tag=xxx

4) 批量开通
- 新增套餐详情页 /packages/{id}:显示套餐信息 + 客户列表
- 表头「全选」置顶,已订阅该套餐的客户置灰不可选;开始日期默认今天、天数/次数默认取套餐设置,均可改
- POST /packages/{id}/subscribe 批量开通,重复的自动跳过并汇总提示
- 修复 r.PostForm 未 ParseForm 导致拿不到勾选项的问题(批量打卡同源问题一并修正)

5) 触控 / PC 模式
- 尾号输入框右侧新增切换按钮,触控模式展开 3×4 键盘、输入框只读 + inputmode=none(不唤起系统输入法),按键直接写值
- 偏好记在 localStorage,默认 PC 模式(键盘收起)

6) 按次到期
- 按次订阅的到期日保持选填;填了到期后状态显示「已到期」且不再进日历待排(复用既有 end_date,无新列)

数据库兼容性:
- 只新增 packages.tag 一列,老库启动时自动 ALTER,可重复迁移;原有表结构与接口不变

测试:
- 无头 Chrome 实拍 5 张页面(日历/触控模式/客户月历/套餐批量/大屏)
- 功能验证 18 项全过(计数口径、标签页签、标签筛选、批量开通与重复跳过、月历翻月、触控开关、按次过期不上日历)
- 119 项验收回归全绿;go vet / go test ./... 通过;版本号 1.7.0
2026-10-19 16:23:01 +08:00

199 lines
9.0 KiB
HTML
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.
{{define "content"}}
<div class="page">
<header class="cust-head">
<div class="cust-id">
<span class="cust-tail">{{.D.Customer.Tail}}</span>
<div>
<h1>{{.D.Customer.Name}}{{if .D.Customer.Archived}} <span class="chip chip-closed">已停用</span>{{end}}</h1>
<div class="cust-tags">
{{range .D.Customer.TagList}}<span class="tagchip">{{.}}</span>{{end}}
</div>
</div>
</div>
<dl class="cust-meta">
{{if .D.Customer.GameName}}<div><dt>游戏</dt><dd>{{.D.Customer.GameName}}</dd></div>{{end}}
{{if .D.Customer.GameServer}}<div><dt>区服</dt><dd>{{.D.Customer.GameServer}}</dd></div>{{end}}
{{if .D.Customer.Contact}}<div><dt>联系</dt><dd>{{.D.Customer.Contact}}</dd></div>{{end}}
{{if .D.Customer.Source}}<div><dt>来源</dt><dd>{{.D.Customer.Source}}</dd></div>{{end}}
<div><dt>打手</dt><dd>
<span class="asg{{if eq .D.Customer.AssigneeID 0}} none{{end}}"><i style="background:{{if .D.Customer.AssigneeColor}}{{.D.Customer.AssigneeColor}}{{else}}#8A8F98{{end}}"></i>{{if .D.Customer.AssigneeName}}{{.D.Customer.AssigneeName}}{{else}}未指派{{end}}</span>
<form method="post" action="/customers/{{.D.Customer.ID}}/assignee" class="assign-form">
<select name="assignee_id">
<option value="0">未指派</option>
{{range .D.Users}}<option value="{{.ID}}"{{if eq $.D.Customer.AssigneeID .ID}} selected{{end}}>{{if .DisplayName}}{{.DisplayName}}{{else}}{{.Username}}{{end}}</option>{{end}}
</select>
<button class="btn btn-sm" type="submit">保存</button>
</form>
</dd></div>
<div><dt>建档</dt><dd>{{.D.Customer.CreatedAt}}</dd></div>
</dl>
<div class="page-actions">
<a class="btn" href="/customers/{{.D.Customer.ID}}/edit">编辑资料</a>
{{if .D.Customer.Archived}}
<form method="post" action="/customers/{{.D.Customer.ID}}/restore"><button class="btn" type="submit">恢复客户</button></form>
{{else}}
<form method="post" action="/customers/{{.D.Customer.ID}}/archive" data-confirm="停用后该客户不再出现在日历和大屏,订阅会保留。确定停用?"><button class="btn btn-ghost" type="submit">停用客户</button></form>
{{end}}
<form method="post" action="/customers/{{.D.Customer.ID}}/delete" data-confirm="将永久删除该客户及其全部订阅、打卡与追加记录,无法恢复。确定删除?">
<input type="hidden" name="confirm" value="delete">
<button class="btn btn-ghost" type="submit" style="color:var(--danger)">彻底删除</button>
</form>
<a class="btn btn-ghost" href="/customers">返回列表</a>
</div>
</header>
{{if .D.Customer.Note}}<div class="notice note">{{.D.Customer.Note}}</div>{{end}}
{{if .D.CustCal}}
<section class="section">
<div class="section-head">
<h2>当月排单</h2>
<div class="minical-nav">
<a class="btn btn-sm btn-ghost" href="{{.D.CustCal.Prev}}">‹</a>
<b>{{.D.CustCal.Title}}</b>
<a class="btn btn-sm btn-ghost" href="{{.D.CustCal.Next}}">›</a>
<span class="muted small">{{.D.CustCal.TaskDays}} 天有单 · {{.D.CustCal.TotalTasks}} 单</span>
</div>
</div>
<div class="minical">
<div class="minical-dow">{{range .D.CustCal.Weekdays}}<span>{{.}}</span>{{end}}</div>
{{range .D.CustCal.Weeks}}
<div class="minical-row">
{{range .}}
<a class="minical-cell{{if not .InMonth}} out{{end}}{{if .IsToday}} today{{end}}{{if eq .Count 0}} no-task{{end}}{{if .AllDone}} all-done{{end}}" href="/calendar?d={{.Date}}" title="{{.Date}}">
<span class="mc-day">{{.Day}}</span>
{{if gt .Count 0}}<span class="mc-count">{{.Done}}/{{.Count}}</span>{{end}}
</a>
{{end}}
</div>
{{end}}
</div>
</section>
{{end}}
<section class="section">
<div class="section-head">
<h2>订阅</h2>
<span class="muted">同一套餐只能有一条订阅,需要更多时长请用「追加」</span>
</div>
{{if .D.Subs}}
<div class="subgrid">
{{range .D.Subs}}{{template "sub-card" .}}{{end}}
</div>
{{else}}
<p class="muted">还没有订阅。用下面的表单为他开通第一个套餐。</p>
{{end}}
{{if .D.Packages}}
<form method="post" action="/customers/{{.D.Customer.ID}}/subscriptions" class="inline-form" data-sub-form>
<select name="package_id" required data-pkg-select>
<option value="">选择套餐…</option>
{{range .D.Packages}}
<option value="{{.ID}}" data-unit="{{.Unit}}" data-amount="{{.DefaultAmount}}"{{if .Archived}} disabled{{end}}>
{{.Name}} · {{.UnitLabel}} {{.DefaultAmount}}{{if eq .Unit "day"}}天{{else}}次{{end}}{{if .Archived}}(已归档){{end}}
</option>
{{end}}
</select>
<label class="inline-field">开始日
<input type="date" name="start_date" value="{{.TodayStr}}" required>
</label>
<label class="inline-field" data-days-wrap>{{/* 默认显示 */}}
天数 <input type="number" name="days" min="1" max="3650" value="30">
</label>
<label class="inline-field" data-count-wrap hidden>
次数 <input type="number" name="total_count" min="1" max="10000" value="10">
</label>
<label class="inline-field" data-end-wrap>
终止日(可留空)<input type="date" name="end_date">
</label>
<button class="btn btn-primary" type="submit">开通订阅</button>
</form>
{{else}}
<p class="muted">还没有可用套餐,<a href="/packages/new">先新建套餐</a>。</p>
{{end}}
</section>
{{if .D.Extend}}{{template "extend-panel" .D.Extend}}{{end}}
{{if .D.Timeline}}
<section class="section">
<div class="section-head">
<h2>时间轴</h2>
<span class="muted">{{.D.Timeline.Start}} → {{.D.Timeline.End}} · 灰色为暂停段</span>
</div>
<div class="timeline">
<div class="tl-axis">
{{range .D.Timeline.Ticks}}<span class="tl-tick" style="left:{{pct .Left}}%">{{.Label}}</span>{{end}}
</div>
{{range .D.Timeline.Bars}}
<div class="tl-row">
<span class="tl-name">{{.Name}}</span>
<div class="tl-track">
<i class="tl-bar st-{{.State}}" style="--tag:{{.Color}};left:{{pct .Left}}%;width:{{pct .Width}}%"></i>
{{range .Pauses}}<u class="tl-pause" style="left:{{pct .Left}}%;width:{{pct .Width}}%"></u>{{end}}
{{if .HasDue}}<em class="tl-due" style="left:{{pct .DueLeft}}%"></em>{{end}}
</div>
<span class="tl-legend">{{.Label}}</span>
</div>
{{end}}
</div>
</section>
{{end}}
<section class="section">
<div class="section-head">
<h2>服务时长追加记录</h2>
<span class="muted">每次追加都留痕,可作为对账依据</span>
</div>
{{if .D.Adjustments}}
<div class="table-wrap">
<table class="table tight">
<thead>
<tr><th>时间</th><th>套餐</th><th>类型</th><th>计算起点</th><th>结束日变化</th><th>增量</th><th>备注</th><th>操作人</th></tr>
</thead>
<tbody>
{{range .D.Adjustments}}
<tr>
<td class="mono small">{{.CreatedAt}}</td>
<td><span class="dot" style="background:{{index $.D.AdjSubColor .SubID}}"></span>{{index $.D.AdjSubName .SubID}}</td>
<td>{{.KindLabel}}</td>
<td class="mono small">{{if .BaseDate}}{{.BaseDate}}{{else}}—{{end}}</td>
<td class="mono small">{{if .OldEndDate}}{{.OldEndDate}} → {{end}}{{if .NewEndDate}}{{.NewEndDate}}{{else}}—{{end}}</td>
<td class="mono">
{{if gt .DaysAdded 0}}<span class="plus">+{{.DaysAdded}} 天</span>{{end}}
{{if gt .CountAdded 0}}<span class="plus">+{{.CountAdded}} 次</span>{{end}}
{{if and (le .DaysAdded 0) (le .CountAdded 0)}}—{{end}}
</td>
<td class="small">{{if .Note}}{{.Note}}{{else}}—{{end}}</td>
<td class="small">{{if .OperatorName}}{{.OperatorName}}{{else}}—{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}<p class="muted">还没有追加记录。</p>{{end}}
</section>
<section class="section">
<div class="section-head">
<h2>打卡历史</h2>
<span class="muted">最近 30 条</span>
</div>
{{if .D.Checkins}}
<ul class="checkin-list">
{{range .D.Checkins}}
<li>
<span class="mono">{{.ServiceDate}}</span>
<span class="dot" style="background:{{.SubColor}}"></span>
<span>{{.SubName}}</span>
<span class="chip {{if eq .Status "done"}}chip-active{{else}}chip-paused{{end}}">{{if eq .Status "done"}}完成{{else}}跳过{{end}}</span>
{{if .Note}}<span class="muted small">{{.Note}}</span>{{end}}
<span class="muted small">{{if .OperatorName}}{{.OperatorName}}{{end}}</span>
</li>
{{end}}
</ul>
{{else}}<p class="muted">还没有打卡记录。</p>{{end}}
</section>
</div>
{{end}}