build / build-and-test (push) Successful in 3m38s
背景:v1.9.0 把界面迁到 Bootstrap 5 时保留了旧自研 CSS,v1.9.2 又叠加了一层 「墨绿渐变」覆盖层,形成三套令牌(--paper/--ink、prefers-color-scheme、 data-bs-theme)并存。系统偏好与 Cookie 不一致时,页面会出现半亮半暗、表单 标签/抽屉标题/订阅页签不可见、输入框变黑块等问题。 本次改动: - 样式加载顺序改为 bootstrap → app.css;删除旧纸墨令牌与 v1.9.2 覆盖层, app.css 重写为单一主题层(:root/[data-bs-theme=light] 与 [data-bs-theme=dark] 两套语义令牌)。组件只读 --app-*,解决 --ink-2 同时被当作「次级文字色」和 「卡片底色」的语义冲突。 - 主题只认 <html data-bs-theme>:服务端按 Cookie 直出;没有 Cookie 时由 <head> 内联脚本在首屏按系统偏好补齐,修好「跟随系统」并避免闪白。大屏同理。 - 修复 Bootstrap 覆盖自研设计的问题:正文 14px + 中文字体栈、22/16/14px 标题、 按钮/输入框/焦点环/卡片回到设计值;普通按钮新增 .btn-soft,不再变透明文字。 - 布局修复:客户/套餐表单的 .grid-2 不再塞 3 个字段;包详情页把筛选独立成 GET 表单(在搜索框回车不再误触批量开通);客户详情操作区与 meta 重排; 抽屉时钟改为固定 46px 并用它对齐抽屉头的 sticky 偏移;追加面板在抽屉里 两列月历、不再 1100px 竖排;手机端日历数字不再换行溢出,移动端表格隐藏 低优先级列,不再被裁切。 - 大屏:KPI/面板去掉 Bootstrap utility,统一由 screen.css 控制;TOP/今日待打 改为固定列宽 + grid-column 对齐;浅色主题的链接、热力、图例对比度补齐; 热力格不再整体降透明度;当日浮层可滚动并支持点击/键盘打开。 - 配色与可读性:普通文本对比度全部达标(截图 lint 由 65 处降到 0);日历 外月日期改为浅底深字,不再整格 opacity;archived 行改为中性底色而非降透明。 - 缓存:新增内嵌静态资源内容指纹 AssetVersion(12 位 sha256),所有 /static 链接带 ?v=;带指纹的响应 public, max-age=31536000, immutable,不带指纹的 回源 no-cache;HTML 继续 no-store。改样式重新编译即自动破缓存,不再依赖 手改版本号(仍保留 v1.10.0 作为产品版本)。 - 认证页重做:登录/初始化页改用 .auth 卡片(品牌、主题切换、绿色焦点环), 去掉 min-height:100vh 叠加 main padding 造成的 40px 纵向滚动。 - JS:修 toISOString 计算自然日差一天的时区 bug;主题切换读 data-bs-theme 并同步 aria-pressed;全选支持半选;大屏切主题同时更新 data-bs-theme; 提示条改为固定定位、淡出不再顶动页面。 - README 更新主题与缓存说明;版本号 1.9.5 → 1.10.0。 验证:gofmt / go vet / go test 全绿;1440、1100、390 三档,明暗两套,工作台/ 客户/套餐/订阅/设置/日志/大屏/登录全部截图走查,无横向溢出与控制台错误。
163 lines
5.8 KiB
JavaScript
163 lines
5.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();
|
|
}
|
|
}
|
|
if (e.key === 'Escape') { hide(); }
|
|
});
|
|
|
|
/* ---------- 主题切换:唯一状态源 data-bs-theme ---------- */
|
|
var THEME_KEY = 'daili_theme';
|
|
var root = document.documentElement;
|
|
var themeBtns = document.querySelectorAll('[data-theme-toggle]');
|
|
|
|
function currentTheme() {
|
|
return root.getAttribute('data-bs-theme') === 'light' ? 'light' : 'dark';
|
|
}
|
|
function paintTheme() {
|
|
var dark = currentTheme() === 'dark';
|
|
themeBtns.forEach(function (b) { b.setAttribute('aria-pressed', dark ? 'true' : 'false'); });
|
|
}
|
|
|
|
themeBtns.forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var next = currentTheme() === 'dark' ? 'light' : 'dark';
|
|
root.setAttribute('data-bs-theme', next);
|
|
document.cookie = THEME_KEY + '=' + next + '; path=/; max-age=31536000; SameSite=Lax';
|
|
paintTheme();
|
|
});
|
|
});
|
|
paintTheme();
|
|
|
|
/* ---------- 日历格子明细 ---------- */
|
|
var scr = document.querySelector('.scr');
|
|
var pop = document.getElementById('day-pop');
|
|
if (!scr || !pop) return;
|
|
|
|
var api = scr.getAttribute('data-api') || '/api/screen/day';
|
|
var pkg = scr.getAttribute('data-pkg') || '0';
|
|
var cache = {};
|
|
var hideTimer = null;
|
|
var pinned = false;
|
|
|
|
function esc(s) {
|
|
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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(--scr-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 hide() {
|
|
clearTimeout(hideTimer);
|
|
pinned = false;
|
|
pop.hidden = true;
|
|
}
|
|
function hideSoon() {
|
|
if (pinned) return;
|
|
clearTimeout(hideTimer);
|
|
hideTimer = setTimeout(function () { pop.hidden = true; }, 260);
|
|
}
|
|
function keepOpen() { clearTimeout(hideTimer); }
|
|
|
|
document.querySelectorAll('.heat-cell[data-date]').forEach(function (cell) {
|
|
cell.addEventListener('mouseenter', function () { show(cell); });
|
|
cell.addEventListener('mouseleave', hideSoon);
|
|
cell.addEventListener('focus', function () { show(cell); });
|
|
cell.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
var openForCell = !pop.hidden && pop.dataset.date === cell.getAttribute('data-date');
|
|
if (openForCell) { hide(); return; }
|
|
pinned = true;
|
|
show(cell);
|
|
pop.dataset.date = cell.getAttribute('data-date');
|
|
});
|
|
});
|
|
|
|
pop.addEventListener('mouseenter', keepOpen);
|
|
pop.addEventListener('mouseleave', function () { if (!pinned) hideSoon(); });
|
|
})();
|