首次推送
build / build-and-test (push) Failing after 1m24s

This commit is contained in:
2026-10-18 19:44:38 +08:00
commit f047662129
64 changed files with 10454 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
{{define "content"}}
<div class="cal-page">
<header class="cal-head">
<div class="cal-title">
<a class="btn btn-ghost btn-square" href="/calendar?y={{.D.PrevY}}&m={{.D.PrevM}}" aria-label="上个月">‹</a>
<h1>{{.D.Title}}</h1>
<a class="btn btn-ghost btn-square" href="/calendar?y={{.D.NextY}}&m={{.D.NextM}}" aria-label="下个月">›</a>
<a class="btn btn-sm" href="/calendar">回到今天</a>
</div>
<div class="cal-stats">
<span><em>{{.D.MonthDue}}</em>本月到期</span>
<span><em>{{.D.MonthTasks}}</em>本月待打</span>
<span><em>{{.D.MonthDone}}</em>已完成</span>
<a class="btn btn-primary btn-sm" href="/customers/new">+ 新建客户</a>
</div>
</header>
{{if .D.NoPackages}}
<div class="notice">还没有服务套餐,先去 <a href="/packages/new">新建套餐</a>,再给客户开通。</div>
{{else if .D.NoCustomers}}
<div class="notice">还没有客户,<a href="/customers/new">新建第一位客户</a>。</div>
{{end}}
<div class="cal-body">
<div class="cal-grid" role="grid">
<div class="cal-dow" role="row">
{{range .D.Weekdays}}<span>{{.}}</span>{{end}}
</div>
{{range .D.Weeks}}
<div class="cal-week" role="row">
{{range .}}
<a class="cal-cell heat-{{.Heat}}{{if not .InMonth}} out{{end}}{{if .IsToday}} today{{end}}{{if .IsSelected}} sel{{end}}{{if .IsWeekend}} weekend{{end}}{{if .DueCount}} has-due{{end}}"
href="/calendar?y={{$.D.Year}}&m={{$.D.Month}}&d={{.Date}}" role="gridcell">
<div class="cell-top">
<span class="dnum">{{.Day}}</span>
{{if .DueCount}}<span class="due-badge"><em>到期</em>{{.DueCount}}</span>{{end}}
</div>
{{if .TaskCount}}
<div class="cell-load">{{.DoneCount}}/{{.TaskCount}} 单</div>
{{end}}
<div class="cell-tickets">
{{range .Tickets}}
<span class="ticket{{if .Done}} done{{end}}{{if .Skipped}} skipped{{end}}" style="--tag:{{.Color}}"><b>{{.Tail}}</b><span class="tk-name">{{.Name}}</span><i>{{.Pkg}}</i></span>
{{end}}
{{if gt .More 0}}<span class="ticket more">+{{.More}}</span>{{end}}
</div>
</a>
{{end}}
</div>
{{end}}
</div>
<aside class="drawer">
<header class="drawer-head">
<h2>{{.D.SelectedLabel}}</h2>
<div class="drawer-tags">
{{if .D.SelectedIsToday}}<span class="chip chip-active">今天</span>{{end}}
{{if .D.SelectedIsPast}}<span class="chip chip-closed">已过去</span>{{end}}
<span class="chip chip-plain">{{len .D.Tasks}} 待打</span>
</div>
</header>
<section class="drawer-block">
<h3>这天到期 <em>{{len .D.Due}}</em></h3>
{{if .D.Due}}
<ul class="due-list">
{{range .D.Due}}
<li>
<a class="due-main" href="/customers/{{.CustomerID}}">
<span class="ticket" style="--tag:{{.PackageColor}}"><b>{{.CustomerTail}}</b>{{.CustomerName}}</span>
<span class="pkg">{{.PackageName}}</span>
<span class="rem">{{.RemainShort}}</span>
</a>
<a class="btn btn-sm" href="/calendar?y={{$.D.Year}}&m={{$.D.Month}}&d={{$.D.Selected}}&extend={{.ID}}#extend">追加</a>
</li>
{{end}}
</ul>
{{else}}<p class="muted">没有到期订阅。</p>{{end}}
</section>
<section class="drawer-block">
<h3>当日待打 <em>{{len .D.Tasks}}</em></h3>
{{if .D.Tasks}}
<ul class="task-list">
{{range .D.Tasks}}
<li class="task{{if .Done}} done{{end}}{{if .Skipped}} skipped{{end}}">
<div class="task-main">
<a class="task-title" href="/customers/{{.CustomerID}}">
<span class="ticket" style="--tag:{{.PackageColor}}"><b>{{.CustomerTail}}</b>{{.CustomerName}}</span>
<span class="pkg">{{.PackageName}}</span>
</a>
<div class="task-meta">
<span>{{.RemainText}}</span>
{{if .DueText}}<span class="due due-{{.Eval.Level}}">{{.DueText}}</span>{{end}}
</div>
{{if .Note}}<div class="task-note">{{.Note}}{{if .Operator}} · {{.Operator}}{{end}}</div>{{end}}
</div>
<div class="task-actions">
{{if or .Done .Skipped}}
<span class="chip {{if .Done}}chip-active{{else}}chip-paused{{end}}">{{if .Done}}已完成{{else}}已跳过{{end}}</span>
<form method="post" action="/checkins/{{.CheckinID}}/delete">
<input type="hidden" name="back" value="{{$.D.Back}}">
<button class="btn btn-sm btn-ghost">撤销</button>
</form>
{{else}}
<form method="post" action="/subscriptions/{{.ID}}/checkin">
<input type="hidden" name="date" value="{{$.D.Selected}}">
<input type="hidden" name="status" value="done">
<input type="hidden" name="back" value="{{$.D.Back}}">
<button class="btn btn-sm btn-primary">完成</button>
</form>
<form method="post" action="/subscriptions/{{.ID}}/checkin">
<input type="hidden" name="date" value="{{$.D.Selected}}">
<input type="hidden" name="status" value="skipped">
<input type="hidden" name="back" value="{{$.D.Back}}">
<button class="btn btn-sm btn-ghost">跳过</button>
</form>
{{end}}
<a class="btn btn-sm" href="/calendar?y={{$.D.Year}}&m={{$.D.Month}}&d={{$.D.Selected}}&extend={{.ID}}#extend">追加</a>
</div>
</li>
{{end}}
</ul>
{{else}}<p class="muted">这天没有待打任务。</p>{{end}}
</section>
{{if .D.Extend}}{{template "extend-panel" .D.Extend}}{{end}}
<section class="drawer-block">
<h3>近 7 天到期</h3>
{{if .D.OtherDue}}
<ul class="mini-due">
{{range .D.OtherDue}}
<li>
<a href="/customers/{{.CustomerID}}"><span class="ticket" style="--tag:{{.PackageColor}}"><b>{{.CustomerTail}}</b>{{.CustomerName}}</span></a>
<span class="muted">{{.PackageName}}</span>
<span class="sm-date">{{.EndDate}}</span>
</li>
{{end}}
</ul>
{{else}}<p class="muted">7 天内没有其它到期。</p>{{end}}
</section>
</aside>
</div>
</div>
{{end}}
@@ -0,0 +1,152 @@
{{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}}</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>{{.D.Customer.CreatedAt}}</dd></div>
</dl>
<div class="page-actions">
<a class="btn" href="/customers/{{.D.Customer.ID}}/edit">编辑资料</a>
<a class="btn btn-ghost" href="/customers">返回列表</a>
</div>
</header>
{{if .D.Customer.Note}}<div class="notice note">{{.D.Customer.Note}}</div>{{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}}
@@ -0,0 +1,48 @@
{{define "content"}}
<div class="page narrow">
<header class="page-head">
<div>
<h1>{{if .D.IsNew}}新建客户{{else}}编辑客户{{end}}</h1>
<p class="muted">手机尾号 + 客户名 唯一;尾号是日常查询的主键。</p>
</div>
</header>
<form method="post" class="form card"
action="{{if .D.IsNew}}/customers{{else}}/customers/{{.D.Customer.ID}}/edit{{end}}">
<div class="grid-2">
<label class="field"><span>手机尾号 <i>*</i></span>
<input name="tail" value="{{.D.Customer.Tail}}" required maxlength="8" placeholder="例如 1234" autofocus>
</label>
<label class="field"><span>客户名 <i>*</i></span>
<input name="name" value="{{.D.Customer.Name}}" required maxlength="32" placeholder="例如 老张">
</label>
</div>
<div class="grid-2">
<label class="field"><span>游戏名</span>
<input name="game_name" value="{{.D.Customer.GameName}}" maxlength="32" placeholder="例如 王者荣耀">
</label>
<label class="field"><span>区服 / UID</span>
<input name="game_server" value="{{.D.Customer.GameServer}}" maxlength="32" placeholder="例如 微信区 123456">
</label>
</div>
<div class="grid-2">
<label class="field"><span>联系方式</span>
<input name="contact" value="{{.D.Customer.Contact}}" maxlength="64" placeholder="微信 / QQ">
</label>
<label class="field"><span>来源渠道</span>
<input name="source" value="{{.D.Customer.Source}}" maxlength="32" placeholder="抖音 / 朋友介绍">
</label>
</div>
<label class="field"><span>标签</span>
<input name="tags" value="{{.D.Customer.Tags}}" maxlength="120" placeholder="老客户,包月,难缠(逗号分隔)">
</label>
<label class="field"><span>备注</span>
<textarea name="note" rows="3" maxlength="500" placeholder="打法偏好、沟通记录等">{{.D.Customer.Note}}</textarea>
</label>
<div class="actions">
<button class="btn btn-primary" type="submit">保存</button>
<a class="btn" href="{{if .D.IsNew}}/customers{{else}}/customers/{{.D.Customer.ID}}{{end}}">取消</a>
</div>
</form>
</div>
{{end}}
@@ -0,0 +1,58 @@
{{define "content"}}
<div class="page">
<header class="page-head">
<div>
<h1>客户</h1>
<p class="muted">共 {{.D.Total}} 位 · 输入手机尾号可快速定位</p>
</div>
<div class="page-actions">
<a class="btn btn-primary" href="/customers/new">+ 新建客户</a>
</div>
</header>
<form class="filterbar" method="get" action="/customers">
<input type="search" name="q" value="{{.D.Q}}" placeholder="手机尾号 / 姓名 / 游戏名 / 联系方式" autocomplete="off">
<button class="btn" type="submit">搜索</button>
{{if .D.Q}}<a class="btn btn-ghost" href="/customers">清除</a>{{end}}
</form>
{{if .D.List}}
<div class="table-wrap">
<table class="table">
<thead>
<tr><th>尾号</th><th>客户</th><th>联系方式</th><th>订阅与剩余</th><th>标签</th><th></th></tr>
</thead>
<tbody>
{{range .D.List}}
<tr>
<td><a class="tail" href="/customers/{{.ID}}">{{.Tail}}</a></td>
<td>
<a class="name" href="/customers/{{.ID}}">{{.Name}}</a>
{{if .GameName}}<div class="muted small">{{.GameName}}{{if .GameServer}} · {{.GameServer}}{{end}}</div>{{end}}
</td>
<td class="muted small">{{if .Contact}}{{.Contact}}{{else}}—{{end}}</td>
<td>
<span class="summary due-{{.Level}}">{{.Summary}}</span>
</td>
<td>{{range .TagList}}<span class="tagchip">{{.}}</span>{{end}}</td>
<td class="right"><a class="btn btn-sm" href="/customers/{{.ID}}">详情</a></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{template "pager" .D.Pager}}
{{else}}
<div class="empty">
<div class="empty-mark">票</div>
{{if .D.Q}}
<p>没有匹配「{{.D.Q}}」的客户。</p>
<a class="btn" href="/customers">看全部客户</a>
{{else}}
<p>还没有客户。先建一位,再给他开套餐。</p>
<a class="btn btn-primary" href="/customers/new">+ 新建客户</a>
{{end}}
</div>
{{end}}
</div>
{{end}}
+20
View File
@@ -0,0 +1,20 @@
{{define "content"}}
<div class="auth">
<div class="auth-card">
<div class="auth-brand"><span class="brand-mark">代</span><span>代打台</span></div>
<h1>登录</h1>
<p class="muted">所有账户均为管理员,任何操作都会留痕。</p>
<form method="post" action="/login" class="form">
<input type="hidden" name="next" value="{{.Next}}">
<label class="field"><span>用户名</span>
<input type="text" name="username" required autofocus autocomplete="username">
</label>
<label class="field"><span>密码</span>
<input type="password" name="password" required autocomplete="current-password">
</label>
<button type="submit" class="btn btn-primary btn-block">登录</button>
</form>
<p class="auth-foot muted">代打台 · 单文件部署 · 数据只在本机</p>
</div>
</div>
{{end}}
+39
View File
@@ -0,0 +1,39 @@
{{define "content"}}
<div class="page">
<header class="page-head">
<div>
<h1>操作日志</h1>
<p class="muted">共 {{.D.Total}} 条 · 谁在什么时候改了什么,都在这里</p>
</div>
</header>
<form class="filterbar" method="get" action="/logs">
<input type="search" name="q" value="{{.D.Q}}" placeholder="搜索摘要 / 动作 / 操作人" autocomplete="off">
<button class="btn" type="submit">搜索</button>
{{if .D.Q}}<a class="btn btn-ghost" href="/logs">清除</a>{{end}}
</form>
{{if .D.List}}
<div class="table-wrap">
<table class="table tight">
<thead><tr><th>时间</th><th>操作人</th><th>动作</th><th>对象</th><th>摘要</th><th>IP</th></tr></thead>
<tbody>
{{range .D.List}}
<tr>
<td class="mono small">{{.CreatedAt}}</td>
<td>{{if .OperatorName}}{{.OperatorName}}{{else}}系统{{end}}</td>
<td><span class="chip chip-plain">{{.Action}}</span></td>
<td class="small muted">{{if .Entity}}{{.Entity}}{{if .EntityID}} #{{.EntityID}}{{end}}{{else}}—{{end}}</td>
<td class="small">{{.Summary}}</td>
<td class="mono small muted">{{.IP}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{template "pager" .D.Pager}}
{{else}}
<div class="empty"><div class="empty-mark">票</div><p>没有符合条件的日志。</p></div>
{{end}}
</div>
{{end}}
@@ -0,0 +1,57 @@
{{define "content"}}
<div class="page narrow">
<header class="page-head">
<div>
<h1>{{if .D.IsNew}}新增套餐{{else}}编辑套餐{{end}}</h1>
<p class="muted">按日套餐用起止日期计算剩余天数;按次套餐记录剩余次数。</p>
</div>
</header>
<form method="post" class="form card" data-package-form
action="{{if .D.IsNew}}/packages{{else}}/packages/{{.D.Package.ID}}/edit{{end}}">
<label class="field"><span>套餐名称 <i>*</i></span>
<input name="name" required maxlength="32" value="{{.D.Package.Name}}" placeholder="例如 包月日常 / 赛季深渊 / 10 次副本">
</label>
<fieldset class="field">
<span>计费方式 <i>*</i></span>
<div class="radios">
<label><input type="radio" name="unit" value="day"{{if eq .D.Package.Unit "day"}} checked{{end}}>
<b>按日</b><small>有开始日与结束日,每天一条待打任务,可打卡</small></label>
<label><input type="radio" name="unit" value="count"{{if eq .D.Package.Unit "count"}} checked{{end}}>
<b>按次</b><small>记录剩余次数,排期完成时自动扣 1 次,有效期可选</small></label>
</div>
</fieldset>
<div class="grid-2">
<label class="field"><span>默认量 <i>*</i></span>
<div class="input-suffix">
<input type="number" name="default_amount" min="1" max="10000" value="{{.D.Package.DefaultAmount}}" required>
<b data-unit-label>{{if eq .D.Package.Unit "count"}}次{{else}}天{{end}}</b>
</div>
</label>
<label class="field"><span>参考价(元,可空)</span>
<input type="number" name="price" min="0" step="1" value="{{if .D.Package.HasPrice}}{{money .D.Package.PriceCents}}{{end}}" placeholder="例如 300">
</label>
</div>
<div class="grid-2">
<label class="field"><span>日历色签</span>
<input type="color" name="color" value="{{.D.Package.Color}}">
</label>
<label class="field"><span>排序(小的靠前)</span>
<input type="number" name="sort_order" value="{{.D.Package.SortOrder}}">
</label>
</div>
<label class="field"><span>说明</span>
<textarea name="note" rows="2" maxlength="200" placeholder="套餐包含什么、怎么个打法">{{.D.Package.Note}}</textarea>
</label>
<div class="actions">
<button class="btn btn-primary" type="submit">保存</button>
<a class="btn" href="/packages">取消</a>
</div>
</form>
</div>
{{end}}
@@ -0,0 +1,49 @@
{{define "content"}}
<div class="page">
<header class="page-head">
<div>
<h1>套餐</h1>
<p class="muted">新增套餐只需加一行配置,不会影响任何已有客户与订阅</p>
</div>
<div class="page-actions"><a class="btn btn-primary" href="/packages/new">+ 新增套餐</a></div>
</header>
{{if .D.List}}
<div class="table-wrap">
<table class="table">
<thead>
<tr><th class="col-dot"></th><th>名称</th><th>类型</th><th>默认量</th><th>参考价</th><th>订阅中 / 总数</th><th>状态</th><th></th></tr>
</thead>
<tbody>
{{range .D.List}}
<tr{{if .Archived}} class="archived"{{end}}>
<td><span class="dot big" style="background:{{.Color}}"></span></td>
<td>
<b>{{.Name}}</b>
{{if .Note}}<div class="muted small">{{.Note}}</div>{{end}}
</td>
<td>{{.UnitLabel}}</td>
<td class="mono">{{.DefaultAmount}}{{if eq .Unit "day"}} 天{{else}} 次{{end}}</td>
<td class="mono">{{if .HasPrice}}¥{{money .PriceCents}}{{else}}—{{end}}</td>
<td class="mono">{{.ActiveCount}} / {{.SubCount}}</td>
<td>{{if .Archived}}<span class="chip chip-closed">已归档</span>{{else}}<span class="chip chip-active">启用中</span>{{end}}</td>
<td class="actions-cell">
<a class="btn btn-sm" href="/packages/{{.ID}}/edit">编辑</a>
<form method="post" action="/packages/{{.ID}}/archive"{{if not .Archived}} data-confirm="归档后不能再新建该套餐的订阅,已有订阅不受影响。继续?"{{end}}>
<button class="btn btn-sm btn-ghost">{{if .Archived}}恢复{{else}}归档{{end}}</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="empty">
<div class="empty-mark">票</div>
<p>还没有套餐。先建「包月日常」这类常用套餐,之后客户开通就一键完成。</p>
<a class="btn btn-primary" href="/packages/new">+ 新增套餐</a>
</div>
{{end}}
</div>
{{end}}
+113
View File
@@ -0,0 +1,113 @@
{{define "content"}}
<div class="scr">
<header class="scr-head">
<div class="scr-brand"><span class="mark">代</span>代打台<em>控制台</em></div>
<div class="scr-clock"><b data-clock>--:--:--</b><span data-date></span></div>
<div class="scr-links">每 {{.D.Refresh}} 秒刷新 · <a href="/calendar">进入工作台</a></div>
</header>
<section class="scr-kpis">
<div class="kpi">
<span>客户</span><b>{{.D.Stats.Customers}}</b><i>位</i>
</div>
<div class="kpi">
<span>活跃订阅</span><b>{{.D.Stats.ActiveSubs}}</b><i>条</i>
</div>
<div class="kpi kpi-hot">
<span>今日到期</span><b>{{.D.Stats.DueToday}}</b><i>条</i>
</div>
<div class="kpi kpi-warm">
<span>7 天内到期</span><b>{{.D.Stats.Due7}}</b><i>条</i>
</div>
</section>
<section class="scr-main">
<div class="panel panel-cal">
<div class="panel-head">
<h2>到期热力</h2>
<span>{{.D.MonthTitle}} · 本月 {{.D.Stats.MonthDue}} 条到期</span>
</div>
<div class="heat-grid">
<div class="heat-dow">{{range .D.Weekdays}}<span>{{.}}</span>{{end}}</div>
{{range .D.Weeks}}
<div class="heat-row">
{{range .}}
<div class="heat-cell h{{.Heat}}{{if not .InMonth}} out{{end}}{{if .IsToday}} today{{end}}">
<span>{{.Day}}</span>
{{if .Due}}<b>{{.Due}}</b>{{end}}
</div>
{{end}}
</div>
{{end}}
</div>
<div class="heat-legend">
<span>到期数</span><i class="h0"></i><i class="h1"></i><i class="h2"></i><i class="h3"></i><i class="h4"></i>
</div>
</div>
<div class="panel panel-top">
<div class="panel-head"><h2>即将到期 TOP 10</h2><span>按剩余量排序</span></div>
{{if .D.Top}}
<ol class="top-list">
{{range $i, $s := .D.Top}}
<li class="top-item lv-{{$s.Eval.Level}}">
<span class="rank">{{add $i 1}}</span>
<span class="top-tail">{{$s.CustomerTail}}</span>
<span class="top-name">{{$s.CustomerName}}</span>
<span class="top-pkg" style="--tag:{{$s.PackageColor}}">{{$s.PackageName}}</span>
<span class="top-remain">{{$s.RemainShort}}</span>
<span class="top-bar"><i style="width:{{$s.Progress}}%"></i></span>
</li>
{{end}}
</ol>
{{else}}
<p class="scr-empty">暂无订阅数据</p>
{{end}}
</div>
</section>
<section class="scr-bottom">
<div class="panel">
<div class="panel-head">
<h2>今日待打</h2>
<span>{{.D.TodayDone}} / {{.D.TodayTasks}} 已完成</span>
</div>
{{if .D.TodayList}}
<ul class="today-list">
{{range .D.TodayList}}
<li class="lv-{{.Eval.Level}}">
<span class="tchip" style="--tag:{{.PackageColor}}"><b>{{.CustomerTail}}</b>{{.CustomerName}}</span>
<span class="pkg">{{.PackageName}}</span>
<span class="rem">{{.RemainShort}}</span>
</li>
{{end}}
</ul>
{{else}}
<p class="scr-empty">今天没有待打任务</p>
{{end}}
</div>
<div class="panel">
<div class="panel-head"><h2>最近追加</h2><span>服务时长变更流水</span></div>
{{if .D.Recent}}
<ul class="recent-list">
{{range .D.Recent}}
<li>
<span class="time mono">{{.CreatedAt}}</span>
<span class="who"><b>{{.CustomerTail}}</b>{{.CustomerName}}</span>
<span class="pkg">{{.PackageName}}</span>
{{if gt .DaysAdded 0}}<em class="plus">+{{.DaysAdded}} 天</em>{{end}}
{{if gt .CountAdded 0}}<em class="plus">+{{.CountAdded}} 次</em>{{end}}
<span class="op">{{.OperatorName}}</span>
</li>
{{end}}
</ul>
{{else}}
<p class="scr-empty">还没有追加记录</p>
{{end}}
</div>
</section>
<footer class="scr-foot">代打台 v{{.D.Version}} · 数据每 {{.D.Refresh}} 秒自动刷新 · 按 F 进入全屏</footer>
</div>
{{end}}
+102
View File
@@ -0,0 +1,102 @@
{{define "content"}}
<div class="page narrow">
<header class="page-head">
<div>
<h1>设置</h1>
<p class="muted">预警天数、备份、账户与数据导出</p>
</div>
</header>
<section class="section card">
<h2>基础设置</h2>
<form method="post" action="/settings" class="form">
<div class="grid-2">
<label class="field"><span>到期预警(天)</span>
<input type="number" name="warn_days" min="0" max="60" value="{{.D.WarnDays}}">
</label>
<label class="field"><span>大屏自动刷新(秒)</span>
<input type="number" name="screen_refresh" min="5" max="600" value="{{.D.ScreenRefresh}}">
</label>
</div>
<p class="muted small">监听地址、端口、数据库、备份、日志、时区等部署参数在 <code>daili.ini</code> 里配置,改完重启生效。</p>
<div class="actions"><button class="btn btn-primary" type="submit">保存设置</button></div>
</form>
</section>
<section class="section card">
<h2>管理员</h2>
<p class="muted small">所有账户权限相同,任何操作都会记在日志里。</p>
<div class="table-wrap">
<table class="table tight">
<thead><tr><th>用户名</th><th>显示名称</th><th>状态</th><th>创建时间</th><th>最近登录</th><th></th></tr></thead>
<tbody>
{{$me := .User}}
{{range .D.Users}}
<tr>
<td class="mono">{{.Username}}</td>
<td>{{.DisplayName}}</td>
<td>{{if .IsActive}}<span class="chip chip-active">启用</span>{{else}}<span class="chip chip-closed">停用</span>{{end}}</td>
<td class="mono small">{{.CreatedAt}}</td>
<td class="mono small">{{if .LastLoginAt}}{{.LastLoginAt}}{{else}}—{{end}}</td>
<td class="actions-cell">
{{if eq .ID $me.ID}}
<span class="muted small">当前账户</span>
{{else}}
<form method="post" action="/settings/users/{{.ID}}/toggle">
<button class="btn btn-sm btn-ghost">{{if .IsActive}}停用{{else}}启用{{end}}</button>
</form>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<form method="post" action="/settings/users" class="inline-form">
<label class="inline-field">用户名 <input name="username" required minlength="3" maxlength="32" autocomplete="off"></label>
<label class="inline-field">显示名称 <input name="display_name" maxlength="32"></label>
<label class="inline-field">初始密码 <input type="password" name="password" required minlength="6" autocomplete="new-password"></label>
<button class="btn btn-primary" type="submit">新增管理员</button>
</form>
</section>
<section class="section card">
<h2>修改我的密码</h2>
<form method="post" action="/settings/password" class="form">
<div class="grid-3">
<label class="field"><span>原密码</span><input type="password" name="old_password" required autocomplete="current-password"></label>
<label class="field"><span>新密码(≥6 位)</span><input type="password" name="password" required minlength="6" autocomplete="new-password"></label>
<label class="field"><span>再输一次</span><input type="password" name="password2" required minlength="6" autocomplete="new-password"></label>
</div>
<div class="actions"><button class="btn btn-primary" type="submit">更新密码</button></div>
</form>
</section>
<section class="section card">
<h2>数据</h2>
<dl class="kv">
<div><dt>配置文件</dt><dd class="mono small">{{.D.ConfigPath}} <span class="muted">(修改后重启生效)</span></dd></div>
<div><dt>数据库</dt><dd class="mono small">{{.D.DBPath}}</dd></div>
<div><dt>备份</dt><dd class="mono small">{{.D.BackupDir}} · 保留 {{.D.BackupKeep}} 份 · {{if .D.BackupEnabled}}自动备份已开启{{else}}自动备份已关闭{{end}}</dd></div>
<div><dt>大屏</dt><dd class="mono small">/screen {{if .D.ScreenPublic}}免登录{{else}}需登录{{end}}</dd></div>
<div><dt>局域网地址</dt><dd class="mono small">http://{{if .D.Addrs}}{{index .D.Addrs 0}}{{else}}127.0.0.1{{end}}:{{.D.Port}}/screen</dd></div>
<div><dt>已运行</dt><dd class="mono">{{.D.Uptime}}</dd></div>
<div><dt>数据量</dt><dd>{{.D.CustomerCount}} 位客户 · {{.D.PackageCount}} 个套餐 · {{.D.LogCount}} 条日志</dd></div>
</dl>
<div class="actions wrap">
<form method="post" action="/settings/backup"><button class="btn" type="submit">立即备份</button></form>
<a class="btn" href="/export/customers.csv">导出客户 CSV</a>
<a class="btn" href="/export/subscriptions.csv">导出订阅 CSV</a>
<a class="btn" href="/export/logs.csv">导出日志 CSV</a>
</div>
{{if .D.Backups}}
<details class="backups">
<summary>最近备份({{len .D.Backups}} 份)</summary>
<ul class="mono small">
{{range .D.Backups}}<li>{{.}}</li>{{end}}
</ul>
</details>
{{end}}
</section>
</div>
{{end}}
+25
View File
@@ -0,0 +1,25 @@
{{define "content"}}
<div class="auth">
<div class="auth-card">
<div class="auth-brand"><span class="brand-mark">代</span><span>代打台</span></div>
<h1>创建第一个管理员</h1>
<p class="muted">数据库里还没有账户。设置好账号后即可开始录入套餐与客户。</p>
<form method="post" action="/setup" class="form">
<label class="field"><span>用户名</span>
<input type="text" name="username" required minlength="3" maxlength="32" autocomplete="username" placeholder="建议用拼音或英文名">
</label>
<label class="field"><span>显示名称</span>
<input type="text" name="display_name" maxlength="32" placeholder="例如:老王(可留空)">
</label>
<label class="field"><span>密码(至少 6 位)</span>
<input type="password" name="password" required minlength="6" autocomplete="new-password">
</label>
<label class="field"><span>再输一次密码</span>
<input type="password" name="password2" required minlength="6" autocomplete="new-password">
</label>
<button type="submit" class="btn btn-primary btn-block">创建并登录</button>
</form>
<p class="auth-foot muted">忘记密码只能手动改数据库,请妥善保管。</p>
</div>
</div>
{{end}}
@@ -0,0 +1,76 @@
{{define "content"}}
<div class="page">
<header class="page-head">
<div>
<h1>订阅</h1>
<p class="muted">共 {{.D.Total}} 条 · 剩余时长各自独立计算</p>
</div>
</header>
<div class="tabs">
<a href="/subscriptions?state=active" class="{{if eq .D.State "active"}}on{{end}}">进行中</a>
<a href="/subscriptions?state=due7" class="{{if eq .D.State "due7"}}on{{end}}">7 天内到期</a>
<a href="/subscriptions?state=due_today" class="{{if eq .D.State "due_today"}}on{{end}}">今天到期</a>
<a href="/subscriptions?state=expired" class="{{if eq .D.State "expired"}}on{{end}}">已到期</a>
<a href="/subscriptions?state=paused" class="{{if eq .D.State "paused"}}on{{end}}">暂停中</a>
<a href="/subscriptions?state=closed" class="{{if eq .D.State "closed"}}on{{end}}">已结束</a>
<a href="/subscriptions?state=all" class="{{if eq .D.State "all"}}on{{end}}">全部</a>
</div>
<form class="filterbar" method="get" action="/subscriptions">
<input type="hidden" name="state" value="{{.D.State}}">
<input type="search" name="q" value="{{.D.Q}}" placeholder="尾号 / 姓名 / 套餐" autocomplete="off">
<select name="package_id">
<option value="0">全部套餐</option>
{{range .D.Packages}}
<option value="{{.ID}}"{{if eq $.D.PackageID .ID}} selected{{end}}>{{.Name}}</option>
{{end}}
</select>
<button class="btn" type="submit">筛选</button>
</form>
{{if .D.List}}
<div class="table-wrap">
<table class="table">
<thead>
<tr><th>客户</th><th>套餐</th><th>状态</th><th>剩余</th><th>时间</th><th>操作</th></tr>
</thead>
<tbody>
{{range .D.List}}
<tr>
<td>
<a class="tail" href="/customers/{{.CustomerID}}">{{.CustomerTail}}</a>
<a class="name" href="/customers/{{.CustomerID}}">{{.CustomerName}}</a>
</td>
<td><span class="dot" style="background:{{.PackageColor}}"></span>{{.PackageName}}
<span class="muted small">{{.UnitLabel}}</span></td>
<td>{{template "state-chip" .}}</td>
<td class="mono">{{.RemainText}}{{if .DueText}}<div class="due due-{{.Eval.Level}} small">{{.DueText}}</div>{{end}}</td>
<td class="mono small">{{.PeriodText}}</td>
<td class="actions-cell">
{{if ne .Status "closed"}}
<a class="btn btn-sm" href="/customers/{{.CustomerID}}?extend={{.ID}}#extend">追加</a>
{{if eq .Status "active"}}
<form method="post" action="/subscriptions/{{.ID}}/pause"><button class="btn btn-sm btn-ghost">暂停</button></form>
{{else if eq .Status "paused"}}
<form method="post" action="/subscriptions/{{.ID}}/resume"><button class="btn btn-sm">恢复</button></form>
{{end}}
{{else}}
<form method="post" action="/subscriptions/{{.ID}}/reopen"><button class="btn btn-sm">重新启用</button></form>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{template "pager" .D.Pager}}
{{else}}
<div class="empty">
<div class="empty-mark">票</div>
<p>这个筛选下没有订阅。</p>
<a class="btn" href="/subscriptions?state=all">看全部</a>
</div>
{{end}}
</div>
{{end}}