feat(v1.3): 管理员(打手)颜色,客户归属打手,日历/待办按颜色标识

1) 管理员颜色
- users 表新增 color;设置 → 管理员 每行一个颜色选择器,保存即生效
- 新建管理员自动从 8 色调色板分配,尽量不重复;支持 #RRGGBB 校验,非法值拒绝

2) 客户归属打手
- customers 表新增 assignee_id(可空,指向 users)
- 新建/编辑客户时可选择「归属打手」,客户详情页也能直接改派
- 语义:一个客户一个打手,该客户之后的所有单子都归这个打手
- 停用的账户不出现在打手候选里;未指派显示灰色「未指派」

3) 日历
- 每个日期格左侧新增 5px 竖条,按当天涉及的打手颜色等分显示(几种颜色几段)
- 竖条按“当天全部任务”去重计算,不受格内色签 3 条上限影响
- 未指派的任务用中性灰段落

4) 待办与大屏
- 「当日待完成」每条任务左侧 3px 打手色条 + 圆点色标 + 打手名字,未指派灰字
- 大屏:今日待打列表、热力格竖条、悬停浮层(新增 who / who_color 字段)统一使用同一套颜色
- 加急样式改为红底,避免与打手色左边框冲突

数据库兼容性:
- 只做加法:users.color、customers.assignee_id 两列;老库打开时自动 ALTER,可重复迁移
- 原有表结构、字段、页面接口不变;未指派的存量数据照常工作

测试:
- 新增单测:颜色自动分配/规范化/非法值拒绝、客户指派与取消、订阅联表带出打手、停用账户不进候选
- 旧库迁移测试扩展:老 users / customers 表自动补列并可写
- 功能验证 16 项(颜色保存、指派、详情展示、日历多色竖条分段、大屏列表与浮层接口)全部通过
- 原有 119 项验收回归全绿;go vet / go test ./... 通过;版本号 1.3.0
This commit is contained in:
subtrack
2026-10-19 00:16:34 +08:00
parent 85f98dff5c
commit 8cfc9f5891
21 changed files with 542 additions and 43 deletions
+37 -9
View File
@@ -27,6 +27,8 @@ type DayCell struct {
Heat int
Tickets []Ticket
More int
// Strip 是左侧的打手色竖条(按出现顺序去重,未指派为灰)
Strip []string
}
// Ticket 是日历格里的色签。
@@ -40,6 +42,8 @@ type Ticket struct {
Skipped bool
IsDue bool
Urgent bool
// AssigneeColor 打手颜色(未指派为空,模板用 spanAssignee 兜底)
AssigneeColor string
}
// TaskRow 是当日待打清单的一行。
@@ -271,15 +275,16 @@ func (s *Server) handleCalendar(w http.ResponseWriter, r *http.Request) {
cell.UrgentCnt++
}
t := Ticket{
SubID: cs.sub.ID,
Tail: cs.sub.CustomerTail,
Name: cs.sub.CustomerName,
Pkg: shortName(cs.sub.PackageName),
Color: cs.sub.PackageColor,
Done: ci.Status == "done",
Skipped: ci.Status == "skipped",
IsDue: cs.sub.EndDate == ds,
Urgent: isUrgent,
SubID: cs.sub.ID,
Tail: cs.sub.CustomerTail,
Name: cs.sub.CustomerName,
Pkg: shortName(cs.sub.PackageName),
Color: cs.sub.PackageColor,
Done: ci.Status == "done",
Skipped: ci.Status == "skipped",
IsDue: cs.sub.EndDate == ds,
Urgent: isUrgent,
AssigneeColor: cs.sub.AssigneeColor,
}
if t.Done || t.Skipped {
cell.DoneCount++
@@ -295,6 +300,8 @@ func (s *Server) handleCalendar(w http.ResponseWriter, r *http.Request) {
}
return tickets[i].SubID < tickets[j].SubID
})
// 竖条按“当天全部任务”算,不能被色签的 3 条上限截断
cell.Strip = assigneeStrip(tickets)
if len(tickets) > 3 {
cell.More = len(tickets) - 3
tickets = tickets[:3]
@@ -426,6 +433,27 @@ func sortTasks(tasks []TaskRow) {
})
}
// assigneeStrip 把一格里所有任务的打手颜色去重后返回,未指派用灰色占位。
func assigneeStrip(tickets []Ticket) []string {
if len(tickets) == 0 {
return nil
}
seen := map[string]bool{}
out := make([]string, 0, 4)
for _, t := range tickets {
c := t.AssigneeColor
if c == "" {
c = "#8A8F98"
}
if seen[c] {
continue
}
seen[c] = true
out = append(out, c)
}
return out
}
func heatLevel(n int) int {
switch {
case n <= 0:
+39 -6
View File
@@ -51,15 +51,17 @@ func (s *Server) handleCustomers(w http.ResponseWriter, r *http.Request) {
type customerFormData struct {
Customer *store.Customer
IsNew bool
Users []store.User
}
func (s *Server) handleCustomerNewPage(w http.ResponseWriter, r *http.Request) {
p := s.newPage(r, "新建客户", "customers")
p.D = &customerFormData{Customer: &store.Customer{}, IsNew: true}
users, _ := s.db.ActiveUsers()
p.D = &customerFormData{Customer: &store.Customer{}, IsNew: true, Users: users}
s.render(w, r, "customer_form", p)
}
func readCustomerForm(r *http.Request) *store.Customer {
func (s *Server) readCustomerForm(r *http.Request) *store.Customer {
return &store.Customer{
Tail: strings.TrimSpace(r.PostFormValue("tail")),
Name: strings.TrimSpace(r.PostFormValue("name")),
@@ -69,6 +71,7 @@ func readCustomerForm(r *http.Request) *store.Customer {
Tags: normalizeTags(r.PostFormValue("tags")),
Source: strings.TrimSpace(r.PostFormValue("source")),
Note: strings.TrimSpace(r.PostFormValue("note")),
AssigneeID: int64(s.formInt(r, "assignee_id", 0)),
}
}
@@ -104,10 +107,11 @@ func validateCustomer(c *store.Customer) string {
}
func (s *Server) handleCustomerCreate(w http.ResponseWriter, r *http.Request) {
c := readCustomerForm(r)
c := s.readCustomerForm(r)
p := s.newPage(r, "新建客户", "customers")
if msg := validateCustomer(c); msg != "" {
p.D = &customerFormData{Customer: c, IsNew: true}
users, _ := s.db.ActiveUsers()
p.D = &customerFormData{Customer: c, IsNew: true, Users: users}
s.fail(w, r, "customer_form", msg, p)
return
}
@@ -117,7 +121,8 @@ func (s *Server) handleCustomerCreate(w http.ResponseWriter, r *http.Request) {
if errors.Is(err, store.ErrDuplicate) {
msg = "该「尾号 + 姓名」的客户已存在,请直接搜索后追加订阅"
}
p.D = &customerFormData{Customer: c, IsNew: true}
users, _ := s.db.ActiveUsers()
p.D = &customerFormData{Customer: c, IsNew: true, Users: users}
s.fail(w, r, "customer_form", msg, p)
return
}
@@ -143,7 +148,7 @@ func (s *Server) handleCustomerUpdate(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
return
}
c := readCustomerForm(r)
c := s.readCustomerForm(r)
c.ID = id
p := s.newPage(r, "编辑客户", "customers")
if msg := validateCustomer(c); msg != "" {
@@ -179,6 +184,7 @@ type customerDetailData struct {
AdjSubColor map[int64]string
Checkins []CheckinRow
Packages []store.Package
Users []store.User
Timeline *Timeline
Extend *ExtendPanel
TotalRemain string
@@ -208,7 +214,9 @@ func (s *Server) handleCustomerDetail(w http.ResponseWriter, r *http.Request) {
}
pauses, _ := s.db.PausesForSubs(ids)
users, _ := s.db.ActiveUsers()
data := &customerDetailData{
Users: users,
Customer: c,
Subs: subViews(subs, today, warn),
Packages: pkgs,
@@ -247,6 +255,31 @@ func (s *Server) handleCustomerDetail(w http.ResponseWriter, r *http.Request) {
s.render(w, r, "customer_detail", p)
}
// handleCustomerAssignee 从客户详情快速指派/取消打手。
func (s *Server) handleCustomerAssignee(w http.ResponseWriter, r *http.Request) {
id := pathID(r)
userID := int64(s.formInt(r, "assignee_id", 0))
if err := s.db.SetCustomerAssignee(id, userID); err != nil {
s.redirect(w, r, "/customers/"+itoa(id), "指派失败:"+err.Error())
return
}
c, _ := s.db.Customer(id)
word := "已取消指派"
if userID > 0 {
if u, err := s.db.UserByID(userID); err == nil {
name := u.DisplayName
if name == "" {
name = u.Username
}
word = "已指派给 " + name
}
}
if c != nil {
s.logOp(r, "指派打手", "customer", id, c.Tail+" "+c.Name+" "+word)
}
s.redirect(w, r, "/customers/"+itoa(id), word)
}
func (s *Server) handleSubCreate(w http.ResponseWriter, r *http.Request) {
customerID := pathID(r)
c, err := s.db.Customer(customerID)
+23
View File
@@ -152,6 +152,29 @@ func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
s.render(w, r, "settings", p)
}
// handleUserColor 设置管理员(打手)颜色。
func (s *Server) handleUserColor(w http.ResponseWriter, r *http.Request) {
id := pathID(r)
color, err := store.NormalizeColor(r.PostFormValue("color"))
if err != nil {
s.redirect(w, r, "/settings", "颜色格式不正确")
return
}
if err := s.db.SetUserColor(id, color); err != nil {
s.redirect(w, r, "/settings", "保存颜色失败:"+err.Error())
return
}
name := ""
if u, err := s.db.UserByID(id); err == nil {
name = u.DisplayName
if name == "" {
name = u.Username
}
}
s.logOp(r, "设置打手颜色", "user", id, name+" → "+color)
s.redirect(w, r, "/settings", "已更新 "+name+" 的颜色")
}
func (s *Server) handleSettingsSave(w http.ResponseWriter, r *http.Request) {
warn := s.formInt(r, "warn_days", 3)
if warn < 0 {
+17
View File
@@ -18,6 +18,8 @@ type screenDay struct {
Count int
Urgent int
Heat int
// Strip 打手色竖条(去重)
Strip []string
}
type screenTask struct {
@@ -124,6 +126,8 @@ func (s *Server) handleScreen(w http.ResponseWriter, r *http.Request) {
d := domain.AddDays(gridStart, wi*7+di)
ds := domain.Date(d)
count, urgentN := 0, 0
strip := []string{}
seenColor := map[string]bool{}
for _, cs := range list {
if heatMode == "due" {
if cs.sub.EndDate == ds {
@@ -142,6 +146,14 @@ func (s *Server) handleScreen(w http.ResponseWriter, r *http.Request) {
if urgent[cs.sub.ID][ds] {
urgentN++
}
color := cs.sub.AssigneeColor
if color == "" {
color = "#8A8F98"
}
if !seenColor[color] {
seenColor[color] = true
strip = append(strip, color)
}
}
if d.Month() == today.Month() {
data.HeatTotal += count
@@ -155,6 +167,7 @@ func (s *Server) handleScreen(w http.ResponseWriter, r *http.Request) {
Count: count,
Urgent: urgentN,
Heat: heatLevel(count),
Strip: strip,
})
}
data.Weeks = append(data.Weeks, week)
@@ -225,6 +238,8 @@ func (s *Server) handleScreenDay(w http.ResponseWriter, r *http.Request) {
Remain string `json:"remain"`
Urgent bool `json:"urgent"`
Done bool `json:"done"`
Who string `json:"who"`
WhoCol string `json:"who_color"`
}
out := struct {
Date string `json:"date"`
@@ -256,6 +271,8 @@ func (s *Server) handleScreenDay(w http.ResponseWriter, r *http.Request) {
Remain: row.RemainText,
Urgent: row.Urgent,
Done: row.Done || row.Skipped,
Who: row.AssigneeLabel(),
WhoCol: row.AssigneeHex(),
})
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
+2
View File
@@ -104,6 +104,7 @@ func (s *Server) routes() http.Handler {
mux.HandleFunc("GET /customers/{id}", s.auth(s.handleCustomerDetail))
mux.HandleFunc("GET /customers/{id}/edit", s.auth(s.handleCustomerEditPage))
mux.HandleFunc("POST /customers/{id}/edit", s.auth(s.handleCustomerUpdate))
mux.HandleFunc("POST /customers/{id}/assignee", s.auth(s.handleCustomerAssignee))
mux.HandleFunc("POST /customers/{id}/subscriptions", s.auth(s.handleSubCreate))
mux.HandleFunc("GET /subscriptions", s.auth(s.handleSubscriptions))
mux.HandleFunc("POST /subscriptions/{id}/extend", s.auth(s.handleSubExtend))
@@ -136,6 +137,7 @@ func (s *Server) routes() http.Handler {
mux.HandleFunc("POST /settings/password", s.auth(s.handlePasswordChange))
mux.HandleFunc("POST /settings/users", s.auth(s.handleUserCreate))
mux.HandleFunc("POST /settings/users/{id}/toggle", s.auth(s.handleUserToggle))
mux.HandleFunc("POST /settings/users/{id}/color", s.auth(s.handleUserColor))
mux.HandleFunc("POST /settings/backup", s.auth(s.handleBackupNow))
mux.HandleFunc("GET /export/{kind}", s.auth(s.handleExport))
+54
View File
@@ -845,3 +845,57 @@ fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
/* ---------- 打手颜色 ---------- */
/* 日历格左侧竖条:同一天有几种打手就分几段 */
.asg-strip {
position: absolute; left: 0; top: 0; bottom: 0;
display: flex; flex-direction: column;
width: 5px; overflow: hidden;
border-radius: 2px 0 0 2px;
z-index: 1;
}
.asg-strip i { display: block; flex: 1 1 0; min-height: 3px; background: var(--muted); }
.asg-strip.sm { width: 3px; }
.cal-cell { padding-left: 12px; }
/* 打手标签:小圆点 + 名字 */
.asg {
display: inline-flex; align-items: center; gap: 4px;
font-size: 11px; color: var(--ink-2); white-space: nowrap;
}
.asg i {
width: 8px; height: 8px; border-radius: 2px;
display: inline-block; flex: 0 0 auto;
}
.asg.none { color: var(--muted); }
.asg.none i { background: var(--muted); opacity: 0.55; }
/* 待办行左侧打手色(加急时红底仍在,色条让位给背景) */
.task { border-left: 3px solid var(--asg, transparent); padding-left: 8px; margin-left: -3px; }
.task.urgent { border-left-color: var(--danger); }
/* 设置页颜色表单 */
.color-form { display: flex; align-items: center; gap: 6px; }
.color-form input[type="color"] {
width: 34px; height: 26px; padding: 1px;
border: 1px solid var(--line); border-radius: var(--radius);
background: var(--surface); cursor: pointer;
}
/* 客户详情里的快速指派 */
.assign-form { display: inline-flex; align-items: center; gap: 6px; margin-left: 8px; }
.assign-form select {
height: 26px; padding: 0 6px; font-size: 12px;
border: 1px solid var(--line); border-radius: var(--radius);
background: var(--surface); color: var(--ink);
}
.cust-meta dd { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; }
@media (max-width: 560px) {
.asg { font-size: 10px; }
.assign-form { margin-left: 0; }
}
+28
View File
@@ -381,3 +381,31 @@ body::before {
.scr-main, .scr-bottom { grid-template-columns: 1fr; }
.scr-tabs, .scr-links { display: none; }
}
/* ---------- 打手颜色(大屏) ---------- */
.asg-strip {
position: absolute; left: 0; top: 0; bottom: 0;
display: flex; flex-direction: column;
width: 3px; overflow: hidden;
}
.asg-strip i { display: block; flex: 1 1 0; min-height: 3px; }
.heat-cell { padding-left: 9px; }
.asg {
display: inline-flex; align-items: center; gap: 4px;
font-size: 10px; color: var(--ink-2); white-space: nowrap;
overflow: hidden; text-overflow: ellipsis;
}
.asg i { width: 7px; height: 7px; border-radius: 1px; flex: 0 0 auto; }
.asg.none { color: var(--muted); }
.asg.none i { opacity: 0.5; }
.today-list li { grid-template-columns: auto minmax(0, 1fr) auto auto auto; }
.pop-who {
display: inline-block; width: 7px; height: 7px;
border-radius: 1px; margin-right: 4px; vertical-align: 0;
}
+1 -1
View File
@@ -85,7 +85,7 @@
data.items.forEach(function (it) {
html += '<li class="' + (it.urgent ? 'urgent ' : '') + (it.done ? 'done' : '') + '">' +
'<span class="pop-tail">' + esc(it.tail) + '</span>' +
'<span class="pop-name">' + esc(it.name) + ' · ' + esc(it.pkg) + '</span>' +
'<span class="pop-name"><i class="pop-who" style="background:' + esc(it.who_color || '#8A8F98') + '"></i>' + esc(it.who || '未指派') + ' · ' + esc(it.pkg) + '</span>' +
'<span class="pop-rem">' + (it.urgent ? '加急 · ' : '') + esc(it.remain) + '</span>' +
'</li>';
});
+3 -1
View File
@@ -43,6 +43,7 @@
{{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}}&pkg={{$.D.PkgID}}" role="gridcell">
{{if .Strip}}<span class="asg-strip" aria-hidden="true">{{range .Strip}}<i style="background:{{.}}"></i>{{end}}</span>{{end}}
<div class="cell-top">
<span class="dnum">{{.Day}}</span>
<span class="cell-badges">
@@ -124,7 +125,7 @@
{{if .D.Tasks}}
<ul class="task-list">
{{range .D.Tasks}}
<li class="task{{if .Done}} done{{end}}{{if .Skipped}} skipped{{end}}{{if .Urgent}} urgent{{end}}">
<li class="task{{if .Done}} done{{end}}{{if .Skipped}} skipped{{end}}{{if .Urgent}} urgent{{end}}" style="--asg:{{.AssigneeHex}}">
<div class="task-main">
<a class="task-title" href="/customers/{{.CustomerID}}">
<span class="ticket" style="--tag:{{.PackageColor}}"><b>{{.CustomerTail}}</b>{{.CustomerName}}</span>
@@ -132,6 +133,7 @@
{{if .Urgent}}<span class="tag-urgent">加急</span>{{end}}
</a>
<div class="task-meta">
<span class="asg{{if .AssigneeNone}} none{{end}}"><i style="background:{{.AssigneeHex}}"></i>{{.AssigneeLabel}}</span>
<span>{{.RemainText}}</span>
{{if .DueText}}<span class="due due-{{.Eval.Level}}">{{.DueText}}</span>{{end}}
</div>
@@ -15,6 +15,16 @@
{{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">
@@ -29,7 +29,17 @@
<label class="field"><span>联系方式</span>
<input name="contact" value="{{.D.Customer.Contact}}" maxlength="64" placeholder="微信 / QQ">
</label>
<label class="field"><span>来源渠道</span>
<label class="field"><span>归属打手</span>
<select name="assignee_id">
<option value="0"{{if eq .D.Customer.AssigneeID 0}} selected{{end}}>未指派</option>
{{range .D.Users}}
<option value="{{.ID}}"{{if eq $.D.Customer.AssigneeID .ID}} selected{{end}}>{{if .DisplayName}}{{.DisplayName}}{{else}}{{.Username}}{{end}}</option>
{{end}}
</select>
<small class="muted">该客户之后的所有单子都记在这个打手名下</small>
</label>
<label class="field"><span>来源渠道</span>
<input name="source" value="{{.D.Customer.Source}}" maxlength="32" placeholder="抖音 / 朋友介绍">
</label>
</div>
+2
View File
@@ -54,6 +54,7 @@
{{range .}}
<div class="heat-cell h{{.Heat}}{{if not .InMonth}} out{{end}}{{if .IsToday}} today{{end}}{{if .IsPast}} past{{end}}"
data-date="{{.Date}}" title="{{.Date}}">
{{if .Strip}}<span class="asg-strip sm" aria-hidden="true">{{range .Strip}}<i style="background:{{.}}"></i>{{end}}</span>{{end}}
<span>{{.Day}}</span>
{{if gt .Urgent 0}}<u class="cell-urgent"></u>{{end}}
{{if .Count}}<b>{{.Count}}</b>{{end}}
@@ -122,6 +123,7 @@
<li class="{{if .Urgent}}urgent{{end}} lv-{{.Eval.Level}}">
<span class="tchip" style="--tag:{{.PackageColor}}"><b>{{.CustomerTail}}</b>{{.CustomerName}}</span>
<span class="pkg">{{.PackageName}}</span>
<span class="asg{{if .AssigneeNone}} none{{end}}"><i style="background:{{.AssigneeHex}}"></i>{{.AssigneeLabel}}</span>
{{if .Urgent}}<span class="tag-urgent">加急</span>{{end}}
<span class="rem">{{.RemainShort}}</span>
</li>
+8 -2
View File
@@ -25,16 +25,22 @@
<section class="section card">
<h2>管理员</h2>
<p class="muted small">所有账户权限相同,任何操作都会记在日志里。</p>
<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>
<thead><tr><th>用户名</th><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>
<form method="post" action="/settings/users/{{.ID}}/color" class="color-form">
<input type="color" name="color" value="{{if .Color}}{{.Color}}{{else}}#8A8F98{{end}}" title="打手颜色">
<button class="btn btn-sm btn-ghost" type="submit">保存</button>
</form>
</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>
+26 -7
View File
@@ -31,6 +31,25 @@ type SubView struct {
CanCheckinToday bool
}
// AssigneeLabel 返回打手显示名;未指派时给灰字提示。
func (v SubView) AssigneeLabel() string {
if v.AssigneeName == "" {
return "未指派"
}
return v.AssigneeName
}
// AssigneeHex 返回打手颜色;未指派用中性灰。
func (v SubView) AssigneeHex() string {
if v.AssigneeColor == "" {
return "#8A8F98"
}
return v.AssigneeColor
}
// AssigneeNone 是否未指派。
func (v SubView) AssigneeNone() bool { return v.AssigneeName == "" }
func stateLabel(state string) (string, string) {
switch state {
case domain.StateActive:
@@ -329,13 +348,13 @@ func buildTimeline(subs []store.Subscription, pauses map[int64][]store.Pause, to
en = st
}
b := Bar{
SubID: s.ID,
Name: s.PackageName,
Color: s.PackageColor,
Left: pos(st),
Width: float64(domain.DayDiff(st, en)+1) / float64(totalDays) * 100,
Label: domain.Date(st) + " → " + domain.Date(en),
State: s.Status,
SubID: s.ID,
Name: s.PackageName,
Color: s.PackageColor,
Left: pos(st),
Width: float64(domain.DayDiff(st, en)+1) / float64(totalDays) * 100,
Label: domain.Date(st) + " → " + domain.Date(en),
State: s.Status,
HasDue: s.EndDate != "",
}
b.DueLeft = pos(en)