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:
+10
-218
@@ -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(); }
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user