fix(theme): align reference layouts and restore passing contracts
Align homepage and category discovery, modern widgets, admin controls, quotes, and comments with the reference theme. Remove superseded paths and add rendering and behavior coverage. Update stale asset and finance contracts; all 760 Node tests and 139 widget assertions pass.
This commit is contained in:
1 parent
b520d29368
commit
8aa116ec43
63 files changed
+1985
-2304
No files matched your search
+69
-239
@@ -1,255 +1,85 @@
|
||||
/**
|
||||
* AKSRT Yiyan — 一言轮播组件
|
||||
*
|
||||
* Fetches random quotes from hitokoto.cn API and auto-rotates.
|
||||
* Public v1 returns one sentence per request; multi-quote carousels
|
||||
* issue parallel fetches. Category `c` must be a letter a–l (or omitted).
|
||||
* Bilingual quote switcher, matching the reference theme's local quote file.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var API_BASE = 'https://v1.hitokoto.cn/';
|
||||
var TYPE_RE = /^[a-l]$/i;
|
||||
var MIN_COUNT = 1;
|
||||
var MAX_COUNT = 10;
|
||||
var DEFAULT_COUNT = 5;
|
||||
var DEFAULT_INTERVAL_SEC = 5;
|
||||
var REFRESH_MS = 30000;
|
||||
|
||||
function normalizeType(raw) {
|
||||
if (raw == null || raw === '') {
|
||||
return '';
|
||||
}
|
||||
var t = String(raw).trim().toLowerCase();
|
||||
return TYPE_RE.test(t) ? t : '';
|
||||
}
|
||||
function updateQuote(el) {
|
||||
if (el.dataset.loading === '1') return;
|
||||
el.dataset.loading = '1';
|
||||
|
||||
function normalizeCount(raw) {
|
||||
var n = parseInt(raw, 10);
|
||||
if (isNaN(n) || n < MIN_COUNT) {
|
||||
return DEFAULT_COUNT;
|
||||
}
|
||||
return Math.min(MAX_COUNT, n);
|
||||
}
|
||||
fetch(el.dataset.endpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: 'action=aksrt_yiyan',
|
||||
})
|
||||
.then(function (response) {
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(function (response) {
|
||||
var quote = response && response.success && response.data;
|
||||
if (!quote || !quote.cn || !quote.en) throw new Error('empty quote');
|
||||
|
||||
function normalizeIntervalMs(raw) {
|
||||
var sec = parseInt(raw, 10);
|
||||
if (isNaN(sec) || sec < 3) {
|
||||
sec = DEFAULT_INTERVAL_SEC;
|
||||
}
|
||||
return Math.min(60, sec) * 1000;
|
||||
}
|
||||
|
||||
function buildApiUrl(type) {
|
||||
var params = new URLSearchParams();
|
||||
params.set('encode', 'json');
|
||||
if (type) {
|
||||
params.set('c', type);
|
||||
}
|
||||
return API_BASE + '?' + params.toString();
|
||||
}
|
||||
|
||||
function mapQuote(data) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return null;
|
||||
}
|
||||
var text = data.hitokoto || data.text || '';
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
text: String(text),
|
||||
from: data.from ? String(data.from) : '',
|
||||
};
|
||||
}
|
||||
|
||||
function ensureStructure(el) {
|
||||
var textEl = el.querySelector('.aksrt-yiyan__text');
|
||||
if (textEl) {
|
||||
return {
|
||||
textEl: textEl,
|
||||
fromEl: el.querySelector('.aksrt-yiyan__from'),
|
||||
dotsEl: el.querySelector('.aksrt-yiyan__dots'),
|
||||
};
|
||||
}
|
||||
|
||||
var content = document.createElement('div');
|
||||
content.className = 'aksrt-yiyan__content';
|
||||
|
||||
textEl = document.createElement('p');
|
||||
textEl.className = 'aksrt-yiyan__text';
|
||||
textEl.textContent = '加载中…';
|
||||
content.appendChild(textEl);
|
||||
|
||||
var fromEl = document.createElement('p');
|
||||
fromEl.className = 'aksrt-yiyan__from';
|
||||
content.appendChild(fromEl);
|
||||
|
||||
el.appendChild(content);
|
||||
|
||||
var dotsEl = document.createElement('div');
|
||||
dotsEl.className = 'aksrt-yiyan__dots';
|
||||
dotsEl.setAttribute('aria-hidden', 'true');
|
||||
el.appendChild(dotsEl);
|
||||
|
||||
return { textEl: textEl, fromEl: fromEl, dotsEl: dotsEl };
|
||||
}
|
||||
|
||||
function fetchOneQuote(type) {
|
||||
return fetch(buildApiUrl(type), {
|
||||
credentials: 'omit',
|
||||
mode: 'cors',
|
||||
}).then(function (r) {
|
||||
return r
|
||||
.json()
|
||||
.catch(function () {
|
||||
return null;
|
||||
})
|
||||
.then(function (data) {
|
||||
if (!r.ok) {
|
||||
var msg =
|
||||
data && data.message
|
||||
? String(data.message)
|
||||
: 'HTTP ' + r.status;
|
||||
throw new Error(msg);
|
||||
}
|
||||
var quote = mapQuote(data);
|
||||
if (!quote) {
|
||||
throw new Error('empty');
|
||||
}
|
||||
return quote;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fetchQuotes(type, count) {
|
||||
var jobs = [];
|
||||
var i;
|
||||
for (i = 0; i < count; i += 1) {
|
||||
jobs.push(
|
||||
fetchOneQuote(type).catch(function () {
|
||||
return null;
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.all(jobs).then(function (results) {
|
||||
var quotes = [];
|
||||
var seen = Object.create(null);
|
||||
results.forEach(function (q) {
|
||||
if (!q || !q.text) {
|
||||
return;
|
||||
}
|
||||
// Dedupe identical text from parallel random draws.
|
||||
if (seen[q.text]) {
|
||||
return;
|
||||
}
|
||||
seen[q.text] = 1;
|
||||
quotes.push(q);
|
||||
});
|
||||
return quotes;
|
||||
});
|
||||
}
|
||||
|
||||
function initYiyan(el) {
|
||||
if (el.dataset.yiyanBound) {
|
||||
return;
|
||||
}
|
||||
el.dataset.yiyanBound = '1';
|
||||
|
||||
var count = normalizeCount(el.dataset.count);
|
||||
var interval = normalizeIntervalMs(el.dataset.interval);
|
||||
var type = normalizeType(el.dataset.type);
|
||||
var parts = ensureStructure(el);
|
||||
var textEl = parts.textEl;
|
||||
var fromEl = parts.fromEl;
|
||||
var dotsEl = parts.dotsEl;
|
||||
var quotes = [];
|
||||
var currentIndex = 0;
|
||||
var timer = null;
|
||||
|
||||
function showQuote(index) {
|
||||
if (!quotes.length || !textEl) {
|
||||
return;
|
||||
}
|
||||
var q = quotes[index % quotes.length];
|
||||
textEl.style.opacity = '0';
|
||||
setTimeout(function () {
|
||||
textEl.textContent = '"' + q.text + '"';
|
||||
textEl.style.opacity = '1';
|
||||
if (fromEl) {
|
||||
fromEl.textContent = q.from ? '—— ' + q.from : '';
|
||||
}
|
||||
}, 200);
|
||||
|
||||
if (dotsEl) {
|
||||
dotsEl.querySelectorAll('.aksrt-yiyan__dot').forEach(function (dot, i) {
|
||||
dot.classList.toggle('is-active', i === index % quotes.length);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function nextQuote() {
|
||||
if (quotes.length < 2) {
|
||||
return;
|
||||
}
|
||||
currentIndex = (currentIndex + 1) % quotes.length;
|
||||
showQuote(currentIndex);
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
if (quotes.length > 1) {
|
||||
timer = setInterval(nextQuote, interval);
|
||||
}
|
||||
}
|
||||
|
||||
function buildDots() {
|
||||
if (!dotsEl || quotes.length < 2) {
|
||||
if (dotsEl) {
|
||||
dotsEl.innerHTML = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
dotsEl.innerHTML = '';
|
||||
quotes.forEach(function (_, i) {
|
||||
var dot = document.createElement('span');
|
||||
dot.className = 'aksrt-yiyan__dot' + (i === 0 ? ' is-active' : '');
|
||||
dotsEl.appendChild(dot);
|
||||
});
|
||||
}
|
||||
|
||||
el.addEventListener('mouseenter', function () {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
el.addEventListener('mouseleave', function () {
|
||||
startTimer();
|
||||
});
|
||||
|
||||
fetchQuotes(type, count)
|
||||
.then(function (list) {
|
||||
quotes = list;
|
||||
if (!quotes.length) {
|
||||
textEl.textContent = '暂无内容';
|
||||
return;
|
||||
}
|
||||
buildDots();
|
||||
showQuote(0);
|
||||
startTimer();
|
||||
var cn = el.querySelector('.cn');
|
||||
var en = el.querySelector('.en');
|
||||
if (cn) cn.textContent = quote.cn;
|
||||
if (en) en.textContent = quote.en;
|
||||
el.classList.remove('is-loading');
|
||||
el.classList.remove('is-error');
|
||||
el.classList.add('is-ready');
|
||||
})
|
||||
.catch(function () {
|
||||
textEl.textContent = '加载失败,请稍后再试';
|
||||
el.classList.remove('is-loading');
|
||||
el.classList.add('is-error');
|
||||
var cn = el.querySelector('.cn');
|
||||
if (cn && !el.classList.contains('is-ready')) cn.textContent = '加载失败,点击重试';
|
||||
})
|
||||
.finally(function () {
|
||||
delete el.dataset.loading;
|
||||
});
|
||||
}
|
||||
|
||||
jQuery(document).off('auto_fun.aksrt-yiyan').on('auto_fun.aksrt-yiyan', function () {
|
||||
document.querySelectorAll('[data-aksrt-yiyan]').forEach(initYiyan);
|
||||
});
|
||||
function init(root) {
|
||||
var scope = root || document;
|
||||
var elements = [];
|
||||
if (scope.matches && scope.matches('[data-aksrt-yiyan]')) elements.push(scope);
|
||||
if (scope.querySelectorAll) elements = elements.concat(Array.prototype.slice.call(scope.querySelectorAll('[data-aksrt-yiyan]')));
|
||||
elements.forEach(function (el) {
|
||||
if (el.dataset.yiyanBound) return;
|
||||
el.dataset.yiyanBound = '1';
|
||||
|
||||
el.addEventListener('click', function () {
|
||||
updateQuote(el);
|
||||
});
|
||||
el.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
updateQuote(el);
|
||||
}
|
||||
});
|
||||
updateQuote(el);
|
||||
window.setInterval(function () {
|
||||
if (!document.hidden) updateQuote(el);
|
||||
}, REFRESH_MS);
|
||||
});
|
||||
}
|
||||
|
||||
window.AksrtYiyan = { init: init };
|
||||
|
||||
if (window.jQuery) {
|
||||
window.jQuery(document).off('auto_fun.aksrt-yiyan').on('auto_fun.aksrt-yiyan', function () {
|
||||
init(document);
|
||||
});
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init, { once: true });
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user