计算器改分步模式(wizard.js,12 个含产假计算器模式)、测试卡图文分开、百宝箱火苗去圈、关注前3条头像各自2~6秒换(v26110419)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
deploy
2026-11-04 08:44:16 +00:00
parent e57bc9f4a5
commit a6e4a73d21
28 changed files with 342 additions and 90 deletions

144
topic/_shared/wizard.js Normal file
View File

@@ -0,0 +1,144 @@
/* 计算器分步模式(11-04 用户:「其他计算器都按生娃计算器那种模式:点进去一步一步填,最后出答案」)
用法:页面原来的计算逻辑不动(照样实时往结果框里写),本脚本只管「显示哪一块」:
CKW.init({ root: '.calc', res: '#res', after: '#scroll > section.calc ~ *' })
· root 里每个 .fld 是一题;.tabs(算法切换)单独一题,题目取 data-q;包在 div 里的 .fld 会被拆开,带 .hide 的跳过;
.calc 直属的 .hint 跟着上一题。
· 必填:有 input/select/.seg 的题要先填;标签里写了「选填」或 .fld 带 data-opt 的不拦。
· 只有一组 .seg / .tabs 的题,点完自动下一题(同生娃计算器)。
· 最后一题按钮是「看结果」:结果框里还是 .empty(没填全 / 数不对)就不放行,把那句提示显示出来。
· 结果屏:只出结果框 + after 选中的内容(详细说明等)+「重新算」(回第 1 题,填过的都留着)。
· 记住停在哪一屏(AGENTS 规则:测试 / 计算器再进来回到最后停的屏),键 mmbox.wz.v1,按页面路径分。 */
(function (g) {
'use strict';
var KEY = 'mmbox.wz.v1';
function load() { try { return JSON.parse(localStorage.getItem(KEY) || '{}') || {}; } catch (e) { return {}; } }
function save(v) { try { var m = load(); m[location.pathname] = v; localStorage.setItem(KEY, JSON.stringify(m)); } catch (e) {} }
function hidden(el) { return el.classList.contains('hide') || el.hidden; }
function txt(el) { return el ? el.textContent.replace(/\s+/g, ' ').trim() : ''; }
function init(o) {
var root = typeof o.root === 'string' ? document.querySelector(o.root) : o.root;
var res = typeof o.res === 'string' ? document.querySelector(o.res) : o.res;
if (!root || !res || root.__wz) return;
root.__wz = 1;
var after = o.after ? [].slice.call(document.querySelectorAll(o.after)).filter(function (e) { return !/toast|mask/.test(e.className + ' ' + e.id); }) : [];
var sc = document.getElementById('scroll') || document.scrollingElement;
root.classList.add('wz');
/* 顶部进度 + 底部按钮 */
var top = document.createElement('div'); top.className = 'wz-top';
top.innerHTML = '<span class="wz-n"></span><span class="wz-dots"></span>';
var bot = document.createElement('div'); bot.className = 'wz-bot';
bot.innerHTML = '<p class="wz-err" role="alert"></p><div class="wz-act"><button type="button" class="wz-prev">‹ 上一步</button><button type="button" class="wz-next"></button></div><p class="wz-tip"></p>';
var again = document.createElement('div'); again.className = 'wz-again';
again.innerHTML = '<button type="button" class="wz-re"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 11a8 8 0 0 0-14.3-4.9L4 8M4 4v4h4M4 13a8 8 0 0 0 14.3 4.9L20 16M20 20v-4h-4"/></svg>重新算</button><small>填过的都还在,改哪项点哪项</small>';
root.insertBefore(top, root.firstChild);
root.appendChild(bot);
res.parentNode.insertBefore(again, res.nextSibling);
var nEl = top.querySelector('.wz-n'), dEl = top.querySelector('.wz-dots'), err = bot.querySelector('.wz-err'),
prev = bot.querySelector('.wz-prev'), next = bot.querySelector('.wz-next'), tip = bot.querySelector('.wz-tip');
/* 题目单元:每次现算(页面切 tabs 会显示 / 隐藏不同的 .fld) */
function units() {
var out = [];
[].forEach.call(root.children, function (c) {
if (c === top || c === bot || c === res || c === again || hidden(c)) return;
if (c.classList.contains('fld') || c.classList.contains('tabs')) out.push([c]);
else if (c.querySelector('.fld')) [].forEach.call(c.querySelectorAll('.fld'), function (f) { if (!hidden(f)) out.push([f]); });
else if (out.length) out[out.length - 1].push(c); /* 跟在题后的 .hint / 「再加一个孩子」等 */
else out.push([c]);
});
return out;
}
/* 能被藏的块:root 的直属子元素(除进度 / 按钮 / 结果框)+ 壳里的 .fld;题里面的东西不碰 */
function all() {
var out = [];
[].forEach.call(root.children, function (c) {
if (c === top || c === bot || c === res || c === again) return;
out.push(c);
if (!c.classList.contains('fld') && !c.classList.contains('tabs')) [].push.apply(out, c.querySelectorAll('.fld'));
});
return out;
}
function single(u) { /* 只有一组按钮可选 → 选完自动下一题 */
var el = u[0];
if (el.classList.contains('tabs')) return true;
return el.querySelectorAll('.seg').length === 1 && !el.querySelector('input,select,.stp');
}
function missing(u) {
var el = u[0];
if (el.hasAttribute('data-opt') || /选填/.test(txt(el.querySelector('.lb')))) return '';
var bad = '';
[].forEach.call(el.querySelectorAll('input,select'), function (i) {
if (bad || i.type === 'hidden' || i.disabled) return;
var lb = i.closest('div') && i.closest('div').querySelector('.lb');
if (lb && /选填/.test(txt(lb))) return;
if (!String(i.value).trim()) bad = '先填一下「' + (txt(lb || el.querySelector('.lb')).replace(/选填.*$/, '') || '这一项') + '」';
});
if (!bad) [].forEach.call(el.querySelectorAll('.seg'), function (s) { if (!bad && !s.querySelector('.on')) bad = '先选一下' + (txt(el.querySelector('.lb')) ? '「' + txt(el.querySelector('.lb')).replace(/\s.*$/, '') + '」' : ''); });
return bad;
}
var i = 0, mode = 'q';
function show() {
var us = units();
if (i >= us.length) i = us.length - 1;
if (i < 0) i = 0;
all().forEach(function (e) { e.classList.add('wz-off'); });
[].forEach.call(root.querySelectorAll('.wz-in'), function (e) { e.classList.remove('wz-in'); });
root.classList.toggle('wz-res', mode === 'r');
document.documentElement.classList.toggle('wz-q', mode !== 'r'); /* 答题时页面底部后加的相关推荐 / 评论区也先不出 */
after.forEach(function (e) { e.classList.toggle('wz-hold', mode !== 'r'); });
err.textContent = '';
if (mode === 'r') {
res.classList.remove('wz-pop'); void res.offsetWidth; res.classList.add('wz-pop');
save('r'); sc.scrollTop = 0; return;
}
var u = us[i];
u.forEach(function (e) {
e.classList.remove('wz-off'); e.classList.add('wz-in');
if (e.parentNode !== root) e.parentNode.classList.remove('wz-off'); /* 包在 #fLmp 这类壳里的题 */
});
var q = u[0].classList.contains('tabs') ? (u[0].getAttribute('data-q') || '先选一种算法') : '';
u[0].setAttribute('data-wzq', q);
nEl.textContent = '第 ' + (i + 1) + ' / ' + us.length + ' 步';
dEl.innerHTML = us.map(function (_, k) { return '<i' + (k < i ? ' class="d"' : k === i ? ' class="c"' : '') + '></i>'; }).join('');
prev.hidden = !i;
var last = i === us.length - 1;
next.textContent = last ? '看结果 ›' : '下一步 ›';
next.classList.toggle('go', last);
tip.textContent = single(u) && !last ? '选完自动下一步' : '';
save(i); sc.scrollTop = 0;
}
function go(d) {
var us = units();
if (d > 0) {
var m = missing(us[i]);
if (m) { err.textContent = m; root.classList.remove('wz-shake'); void root.offsetWidth; root.classList.add('wz-shake'); return; }
if (i === us.length - 1) {
var e = res.querySelector('.empty');
if (e) { err.textContent = txt(e); return; }
mode = 'r'; return show();
}
}
i += d; show();
}
prev.addEventListener('click', function () { go(-1); });
next.addEventListener('click', function () { go(1); });
again.querySelector('.wz-re').addEventListener('click', function () { mode = 'q'; i = 0; show(); });
/* 单选题点完自动下一步(等页面自己的点击处理先跑完) */
root.addEventListener('click', function (e) {
if (mode !== 'q') return;
var b = e.target.closest('.seg button,.tabs button'); if (!b) return;
var us = units(), u = us[i];
if (!u || u.indexOf(b.closest('.fld,.tabs')) < 0 || !single(u) || i === us.length - 1) return;
setTimeout(function () { if (mode === 'q' && units()[i] && units()[i][0] === u[0]) go(1); }, 320);
});
var sv = load()[location.pathname];
if (sv === 'r' && !res.querySelector('.empty')) mode = 'r';
else if (typeof sv === 'number') i = sv;
show();
}
g.CKW = { init: init };
})(window);