Files
subtrack/internal/web/static/screen.js
T
subtrack 8cfc9f5891 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
2026-10-19 00:16:34 +08:00

133 lines
4.9 KiB
JavaScript

/* 大屏:时钟 + 全屏 + 主题 + 日历悬停明细 */
(function () {
'use strict';
function pad(n) { return n < 10 ? '0' + n : '' + n; }
function tick() {
var d = new Date();
var clock = document.querySelector('[data-clock]');
if (clock) clock.textContent = pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
var dateEl = document.querySelector('[data-date]');
if (dateEl) {
dateEl.textContent = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
' 周' + '日一二三四五六'.charAt(d.getDay());
}
}
tick();
setInterval(tick, 1000);
document.addEventListener('keydown', function (e) {
if (e.key === 'f' || e.key === 'F') {
if (document.fullscreenElement) {
document.exitFullscreen();
} else if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
}
}
});
/* ---------- 主题切换 ---------- */
var THEME_KEY = 'daili_theme';
function effectiveTheme() {
var attr = document.documentElement.getAttribute('data-theme');
if (attr === 'light' || attr === 'dark') return attr;
return window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
document.querySelectorAll('[data-theme-toggle]').forEach(function (btn) {
btn.addEventListener('click', function () {
var next = effectiveTheme() === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
document.cookie = THEME_KEY + '=' + next + '; path=/; max-age=31536000; SameSite=Lax';
});
});
/* ---------- 日历格子悬停明细 ---------- */
var root = document.querySelector('.scr');
var pop = document.getElementById('day-pop');
if (!root || !pop) return;
var api = root.getAttribute('data-api') || '/api/screen/day';
var pkg = root.getAttribute('data-pkg') || '0';
var cache = {};
var hideTimer = null;
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
function place(cell) {
var r = cell.getBoundingClientRect();
var w = 340, h = Math.min(360, pop.offsetHeight || 240);
var left = r.right + 8;
if (left + w > window.innerWidth - 8) left = Math.max(8, r.left - w - 8);
var top = r.top + r.height / 2 - h / 2;
top = Math.max(8, Math.min(top, window.innerHeight - h - 8));
pop.style.left = left + 'px';
pop.style.top = top + 'px';
}
function render(data, cell) {
if (!data) {
pop.innerHTML = '<div class="pop-empty">读取失败</div>';
return;
}
var d = new Date(data.date + 'T00:00:00');
var label = data.date + ' 周' + '日一二三四五六'.charAt(d.getDay());
var html = '<h4>' + esc(label) + '<small>' + data.total + ' 单 · 已完成 ' + data.done +
(data.urgent ? ' · <span style="color:var(--danger)">加急 ' + data.urgent + '</span>' : '') + '</small></h4>';
if (!data.items || !data.items.length) {
html += '<div class="pop-empty">这天没有排单</div>';
} else {
html += '<ul>';
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"><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>';
});
html += '</ul>';
if (data.more > 0) html += '<div class="pop-more">另有 ' + data.more + ' 单未显示</div>';
}
pop.innerHTML = html;
pop.hidden = false;
place(cell);
}
function show(cell) {
clearTimeout(hideTimer);
var date = cell.getAttribute('data-date');
var key = date + '|' + pkg;
pop.hidden = false;
pop.innerHTML = '<div class="pop-empty">载入中…</div>';
place(cell);
if (cache[key]) {
render(cache[key], cell);
return;
}
fetch(api + '?date=' + encodeURIComponent(date) + '&pkg=' + encodeURIComponent(pkg), {
headers: { 'Accept': 'application/json' }
}).then(function (r) {
if (!r.ok) throw new Error('bad');
return r.json();
}).then(function (data) {
cache[key] = data;
render(data, cell);
}).catch(function () {
pop.innerHTML = '<div class="pop-empty">读取失败</div>';
});
}
function hideSoon() {
hideTimer = setTimeout(function () { pop.hidden = true; }, 160);
}
document.querySelectorAll('.heat-cell[data-date]').forEach(function (cell) {
cell.addEventListener('mouseenter', function () { show(cell); });
cell.addEventListener('mouseleave', hideSoon);
});
})();