Files
subtrack/internal/web/static/screen.js
T
subtrack 85f98dff5c feat(v1.2): 亮暗主题、按尾号快速完成、加急置顶、按套餐分日历、大屏升级
1) 全站亮色 / 暗色
- 管理端与大屏都提供切换按钮(顶栏 ☾/☀、大屏右上角),偏好写 cookie,服务端首屏直出,不闪
- 未选择时跟随系统(prefers-color-scheme);两套配色都改成 CSS 变量,色签、热力、表格、抽屉全部适配
- 大屏默认仍是深色记分板,浅色为纸感背景

2) 工作台 → 当日待完成:按手机尾号快速完成
- 输入尾号回车:唯一匹配直接完成并提示
- 同尾号多客户 / 一单多服务:跳到选择面板,默认全选,可多选后一次完成
- 新增 POST /checkins/quick 与 POST /checkins/batch;抽屉标题改为「未完成数 / 总单数」

3) 加急
- 每条任务右侧可勾选/取消加急;加急任务红 tag + 置顶(抽屉与大屏「今日待打」)
- 日历格子出现红点,单据色签加红边;大屏热力格也带红点
- 新增表 task_flags(sub_id, service_date, urgent),按 订阅+日期 存,避免和打卡状态耦合

4) 套餐「在日历中独立显示」
- 套餐表单新增勾选项,列表页显示「独立日历」标记
- 勾选后工作台与大屏顶部出现套餐标签页(全部 / 每日任务 / …),可只看某类委托的排单、热量与待办
- 新增列 packages.calendar_separate;老库打开时自动 ALTER,无需手工迁移

5) 大屏
- 热力默认改为「每日待完成单量」,右上角可切回「到期热力」
- 鼠标悬停任意日期格,浮出当日待办明细(客户、套餐、剩余、加急),数据来自 GET /api/screen/day
- 底部面板改为「今日到期」(左)+「今日待打」(右,加急置顶),最近追加改为最底部细条

数据库兼容性:
- 只做加法:新增 task_flags 表与 packages.calendar_separate 列,原有表结构、字段、接口与已有数据均不变
- 迁移可重复执行(CREATE TABLE IF NOT EXISTS + PRAGMA 检查后 ALTER)

测试:
- 新增单元测试:套餐独立日历标记、加急增删查、旧库自动加列与重复迁移
- 新增功能冒烟 33 项(主题 cookie、套餐标签、快速完成单/多匹配、加急排序、大屏接口)全部通过
- 原有 119 项验收回归全绿(3 条断言按新 UI 更新:抽屉标题、热力默认口径、今日待打数取值方式)
- go vet / go test ./... 通过;版本号 1.2.0
2026-10-18 22:07:25 +08:00

133 lines
4.8 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">' + esc(it.name) + ' · ' + 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);
});
})();