feat(v1.11.5-L): lite 精简版——移除大屏/客户自查/多用户/小键盘,套餐仅保留按日

需求与确认(用户逐项确认):
- 老数据「按次 / 通行证 / 深渊」统一迁移成按日,代码里彻底删除三种模式
- 打手归属一并移除(只剩一个人,颜色与指派没有区分意义)
- 盲水印做成完全不可见:只存在于 DOM / 源码,肉眼与截图都看不到
- 拼写采用更正后的 Powered by Baka春秋
- 版本号与品牌全站统一带 -L
- screen_public / portal_public 保留解析但不起作用,保证旧 ini 能直接启动
- README 同步更新

品牌与版本(全站统一):
- const version = "1.11.5-L":<title>、启动横幅、日志前缀 [代打台-L]、
  设置页版本号、页脚版本号全部带 -L
- 左上角导航品牌 代打台 → 代打台-L(登录页、初始化页同步)
- 页脚新增可见署名 Powered by Baka春秋

盲水印:
- _layout.html 在 <body> 起始处注入 .wm-blind 容器(含注释与 data-wm 双标记),
  覆盖包括登录/初始化在内的所有页面
- app.css 用零尺寸 + 透明 + opacity:0 + clip-path:inset(50%) 四重隐藏,
  同时留同名注释;不占位、不上色、不进无障碍树、不影响打印

功能移除:
- 大屏:删除 handlers_screen.go、pages/screen.html、_screen_layout.html、
  static/screen.css、static/screen.js、store.Stats/ScreenStats;
  导航「大屏 ↗」、页脚「大屏」、设置页「大屏刷新/大屏」行、启动横幅大屏行一并删除
- 客户自查:删除 handlers_portal.go、pages/portal.html 与 .portal-*/.pcal-* 样式,
  设置页「客户自助」行删除
- 多管理员:删除 handleUserCreate/Toggle/Color/UserEditPage/UserEditSave、
  pages/user_form.html、store 侧 ActiveUsers/SetUserColor/SetUserActive/
  UpdateUserByAdmin/NormalizeColor/AssigneePalette;设置页「管理员」整块移除,
  只保留首次启动创建的那一个账户与「修改我的密码」
- 打手归属:删除 customers.assignee_id 相关读写与 UI(客户详情指派下拉、
  客户表单「归属打手」、日历格左侧打手色竖条、任务卡打手标签、套餐详情客户列)
- 登录页与初始化页的说明文案同步改为单账户口径(「只有一个管理员账户」
  「创建后不能再添加其他管理员」),字数与 main 对齐,卡片不换行、高度不变
- 触摸小键盘:删除 3×4 键盘、⌨/触控开关、localStorage 记忆、.keypad* 样式
- 套餐计费方式:只保留按日。删除按次/通行证/深渊的 Package 与 Subscription 字段、
  domain 里的 UnitCount/StateUsedUp/CycleIndex、package_form 计费方式单选与
  通行证/深渊/有效期字段、日历整段色块(has-block/cell-block/season-band)、
  追加次数分支、SeasonComplete/AddCount、CountActiveSubs 的次数条件

数据库迁移(幂等,Migrate 内自动执行):
- normalizeLegacyData:套餐一律 unit='day' 并清 season/abyss/cycle_days/
  default_validity_days/season_*;订阅清 season/abyss/cycle_days/season_done
- 通行证、深渊当年就是 unit='day' 且起止日完整,仅清标记,服务区间原样保留
- 按次按「1 次 = 1 天」折算:结束日 = 开始日 + 总次数 − 1;仍缺结束日退回开始日
- 只动数据,不删表不删列:老库的 season/abyss/cycle_days/total_count/used_count
  保留但不再使用,升级可逆、不会因为 DDL 失败卡住
- 新库 schema 干净:不再创建上述列,users.color / customers.assignee_id 也不创建
- 老库 ensureColumn 保留 calendar_separate / tag / archived 三项(真增量列)

兼容:
- config 仍解析 screen_public / portal_public(结构体字段注明已废弃),
  默认 ini 模板里改为「已废弃,写了不报错、不生效」

测试:
- go vet ./... 通过;go test ./... 全部通过
  (新增 TestNormalizeLegacyTypeData 验证三模式折算成按日;
   TestSingleUserAccount / TestSessionLifecycle 覆盖单用户与会话;
   删除按次、深渊、打手颜色、多用户等已不存在的能力的用例)
- 新增 scripts/smoke-lite.ps1:起隔离实例,19 个页面级请求全部 200,
  已移除的 /screen、/customer、/api/screen/day 均 404,
  水印/品牌/v1.11.5-L/无残留词 12 项断言通过,
  真实数据下日历出现待打任务、「剩 30 天」、今天到期徽标 4 项断言通过,
  本次 0 失败

README:改为 lite 版说明,删除大屏/客户自查/通行证/深渊/按次/小键盘/多用户章节,
新增「盲水印」与「与 main 分支的差异」两节(含老数据折算规则与列保留说明)。
This commit is contained in:
2026-10-21 03:33:27 +08:00
parent 0392f44cf1
commit cde1c84c9a
45 changed files with 786 additions and 4391 deletions
+10 -218
View File
@@ -1,12 +1,10 @@
/* 代打台 · 交互增强(无框架,全部渐进增强) */
/* 代打台-L · 交互增强(无框架,全部渐进增强) */
/* Powered by Baka春秋 */
(function () {
'use strict';
/* ---------- 工具:本地日期,禁止用 toISOString 做“自然日”计算 ---------- */
function pad2(n) { return n < 10 ? '0' + n : '' + n; }
function parseDay(s) { return new Date(s + 'T00:00:00'); }
function fmtDay(d) { return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()); }
function addDays(s, n) { var d = parseDay(s); d.setDate(d.getDate() + n); return fmtDay(d); }
function diffDays(a, b) { return Math.round((b.getTime() - a.getTime()) / 86400000); }
/* ---------- 提示条:自动淡出(.flash 固定定位,不会顶动页面) ---------- */
@@ -67,136 +65,18 @@
if (input.value) paint(input.value);
});
/* ---------- 新订阅表单:按套餐类型切换「天数 / 次数 / 终止日」 ---------- */
/* ---------- 新订阅表单:选套餐时带出默认天数 ---------- */
document.querySelectorAll('form[data-sub-form]').forEach(function (form) {
var sel = form.querySelector('[data-pkg-select]');
var daysWrap = form.querySelector('[data-days-wrap]');
var countWrap = form.querySelector('[data-count-wrap]');
var endWrap = form.querySelector('[data-end-wrap]');
var startWrap = form.querySelector('[data-start-wrap]');
var startInput = form.querySelector('input[name="start_date"]');
var endInput = endWrap && endWrap.querySelector('input');
var cyclesWrap = form.querySelector('[data-cycles-wrap]');
var cyclesInput = cyclesWrap && cyclesWrap.querySelector('input');
if (!sel) return;
if (!sel || !daysWrap) return;
var daysInput = daysWrap.querySelector('input');
/* 只覆盖“上一次自动填的”终止日,不碰用户手填的值 */
function setAutoEnd(v) {
if (!endInput || !v) return;
if (!endInput.value || endInput.dataset.auto === endInput.value) {
endInput.value = v;
endInput.dataset.auto = v;
}
}
/* 显示/隐藏一组字段;隐藏时一并 disable,避免同名字段重复提交 */
function setWrap(wrap, show) {
if (!wrap) return;
wrap.hidden = !show;
wrap.querySelectorAll('input, select, textarea').forEach(function (el) { el.disabled = !show; });
}
function sync() {
sel.addEventListener('change', function () {
var opt = sel.options[sel.selectedIndex];
var unit = opt && opt.getAttribute('data-unit');
var amount = opt && opt.getAttribute('data-amount');
var validity = opt && parseInt(opt.getAttribute('data-validity') || '0', 10);
if (unit === 'season') {
setWrap(daysWrap, false);
setWrap(countWrap, false);
setWrap(endWrap, false);
setWrap(startWrap, false);
setWrap(cyclesWrap, false);
if (startInput) startInput.required = false;
return;
}
setWrap(startWrap, true);
if (startInput) startInput.required = true;
if (unit === 'abyss') {
setWrap(daysWrap, false);
setWrap(countWrap, false);
setWrap(endWrap, false);
setWrap(cyclesWrap, true);
if (cyclesInput) {
cyclesInput.required = true;
if (amount) cyclesInput.value = amount;
}
return;
}
setWrap(cyclesWrap, false);
if (cyclesInput) cyclesInput.required = false;
if (unit === 'count') {
setWrap(daysWrap, false);
setWrap(countWrap, true);
setWrap(endWrap, true);
if (amount) countWrap.querySelector('input').value = amount;
if (validity > 0 && startInput && startInput.value) setAutoEnd(addDays(startInput.value, validity - 1));
} else if (unit === 'day') {
setWrap(daysWrap, true);
setWrap(countWrap, false);
setWrap(endWrap, true);
if (amount) daysWrap.querySelector('input').value = amount;
} else {
setWrap(daysWrap, true);
setWrap(countWrap, false);
setWrap(endWrap, false);
}
}
sel.addEventListener('change', sync);
if (startInput) {
startInput.addEventListener('change', function () {
var opt = sel.options[sel.selectedIndex];
var validity = opt && parseInt(opt.getAttribute('data-validity') || '0', 10);
if (opt && opt.getAttribute('data-unit') === 'count' && validity > 0 && startInput.value) {
setAutoEnd(addDays(startInput.value, validity - 1));
}
});
}
sync();
});
/* ---------- 套餐表单:切换计费方式与对应字段 ---------- */
document.querySelectorAll('form[data-package-form]').forEach(function (form) {
var label = form.querySelector('[data-unit-label]');
var amountTitle = form.querySelector('[data-amount-title]');
var radios = form.querySelectorAll('input[name="unit"]');
var seasonWrap = form.querySelector('[data-season-wrap]');
var validityWrap = form.querySelector('[data-validity-wrap]');
var abyssWrap = form.querySelector('[data-abyss-wrap]');
var amountWrap = form.querySelector('[data-amount-wrap]');
var cycleInput = form.querySelector('input[name="cycle_days"]');
function setWrap(wrap, show) {
if (!wrap) return;
wrap.hidden = !show;
wrap.querySelectorAll('input, select, textarea').forEach(function (el) { el.disabled = !show; });
}
function sync() {
var v = 'day';
radios.forEach(function (r) { if (r.checked) v = r.value; });
if (label) label.textContent = v === 'count' ? '次' : (v === 'abyss' ? '个' : '天');
if (amountTitle) amountTitle.textContent = v === 'abyss' ? '周期数' : '默认量';
setWrap(seasonWrap, v === 'season');
setWrap(validityWrap, v === 'count');
setWrap(abyssWrap, v === 'abyss');
setWrap(amountWrap, v !== 'season');
if (seasonWrap) {
seasonWrap.querySelectorAll('input').forEach(function (i) { i.required = v === 'season'; });
}
if (cycleInput) cycleInput.required = v === 'abyss';
}
radios.forEach(function (r) { r.addEventListener('change', sync); });
sync();
var amount = opt && parseInt(opt.getAttribute('data-amount') || '0', 10);
if (amount > 0 && daysInput) daysInput.value = amount;
});
});
/* ---------- 批量开通:表头全选 + 半选状态 ---------- */
@@ -253,7 +133,7 @@
paint();
})();
/* ---------- 触摸小键盘 + 抽屉时钟 + 更多菜单 ---------- */
/* ---------- 抽屉时钟 + 更多菜单 ---------- */
(function () {
'use strict';
@@ -267,51 +147,6 @@
}
if (clocks.length) { tick(); setInterval(tick, 1000); }
/* 小键盘:输入手机尾号
关键点:鼠标按下时阻止默认行为,否则按钮会先抢走输入框焦点;
每次按键后再把焦点和光标送回输入框末尾。 */
document.querySelectorAll('[data-keypad]').forEach(function (pad) {
// 键盘是表单的兄弟节点(不在 <form> 内),所以先找输入框,再用 input.form 反查表单
var scope = pad.closest('.drawer-block') || pad.parentElement || document;
var input = scope.querySelector('input[name="tail"]');
if (!input) { return; }
var form = input.form;
if (!form) { return; }
function keepFocus() {
try { input.focus({ preventScroll: true }); } catch (err) { input.focus(); }
var end = input.value.length;
try { input.setSelectionRange(end, end); } catch (err) { /* number 类型时忽略 */ }
}
// mousedown 会触发“按钮获得焦点”,先拦下来;pointerdown 再兜底一次
pad.addEventListener('mousedown', function (e) {
e.preventDefault();
keepFocus();
});
pad.addEventListener('pointerdown', function () {
// 这里不能 preventDefault:会连带取消后面的 click 事件
keepFocus();
});
pad.addEventListener('click', function (e) {
var btn = e.target.closest('button[data-key]');
if (!btn) return;
var key = btn.getAttribute('data-key');
if (key === 'back') {
input.value = input.value.slice(0, -1);
} else if (key === 'ok') {
if (input.value) {
if (form.requestSubmit) { form.requestSubmit(); } else { form.submit(); }
}
return;
} else if (input.value.length < 8) {
input.value += key;
}
keepFocus();
});
});
/* 更多菜单:点开一个时收起其它,点空白处关闭 */
document.addEventListener('click', function (e) {
document.querySelectorAll('.more-actions[open]').forEach(function (d) {
@@ -319,46 +154,3 @@
});
});
})();
/* ---------- 触控/PC 小键盘模式 ---------- */
(function () {
'use strict';
var KEY = 'daili_keypad_mode';
function applyMode(touch) {
document.querySelectorAll('[data-keypad]').forEach(function (pad) {
pad.hidden = !touch;
var block = pad.closest('.drawer-block') || document;
var input = block.querySelector('input[name="tail"]');
if (input) {
input.readOnly = touch;
input.setAttribute('inputmode', touch ? 'none' : 'numeric');
input.setAttribute('placeholder', touch ? '点下方键盘输入尾号' : '输入手机尾号,回车即可完成');
}
var btn = block.querySelector('[data-keypad-toggle]');
if (btn) {
btn.textContent = touch ? '触控' : '⌨';
btn.classList.toggle('on', touch);
btn.title = touch ? '当前触控模式,点此切回 PC' : '当前 PC 模式,点此打开触控键盘';
}
});
}
var touchMode = false;
try { touchMode = localStorage.getItem(KEY) === 'touch'; } catch (err) { }
applyMode(touchMode);
document.querySelectorAll('[data-keypad-toggle]').forEach(function (btn) {
btn.addEventListener('click', function () {
touchMode = !touchMode;
try { localStorage.setItem(KEY, touchMode ? 'touch' : 'pc'); } catch (err) { }
applyMode(touchMode);
if (!touchMode) {
var block = btn.closest('.drawer-block') || document;
var input = block.querySelector('input[name="tail"]');
if (input) { input.focus(); }
}
});
});
})();