fix(yiyan): stop invalid Hitokoto category requests

Public hitokoto v1 rejects c=hitokoto (and ignores batch count). Whitelist
categories a–l, multi-fetch for carousel, and share full markup via
aksrt_render_yiyan_block so single-post shells actually render quotes.
This commit is contained in:
2026-07-17 10:33:00 +08:00
parent 7cc25ef68c
commit 6bd7676b73
5 changed files with 402 additions and 164 deletions
+203 -57
View File
@@ -2,6 +2,8 @@
* 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).
*
* @package AKSRT
*/
@@ -9,22 +11,168 @@
(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;
function normalizeType(raw) {
if (raw == null || raw === '') {
return '';
}
var t = String(raw).trim().toLowerCase();
return TYPE_RE.test(t) ? t : '';
}
function normalizeCount(raw) {
var n = parseInt(raw, 10);
if (isNaN(n) || n < MIN_COUNT) {
return DEFAULT_COUNT;
}
return Math.min(MAX_COUNT, n);
}
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;
if (el.dataset.yiyanBound) {
return;
}
el.dataset.yiyanBound = '1';
var count = parseInt(el.dataset.count || '5', 10);
var interval = parseInt(el.dataset.interval || '5', 10) * 1000;
var type = el.dataset.type || 'a';
var textEl = el.querySelector('.aksrt-yiyan__text');
var fromEl = el.querySelector('.aksrt-yiyan__from');
var dotsEl = el.querySelector('.aksrt-yiyan__dots');
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;
if (!quotes.length || !textEl) {
return;
}
var q = quotes[index % quotes.length];
textEl.style.opacity = '0';
setTimeout(function () {
@@ -35,7 +183,6 @@
}
}, 200);
// Update dots
if (dotsEl) {
dotsEl.querySelectorAll('.aksrt-yiyan__dot').forEach(function (dot, i) {
dot.classList.toggle('is-active', i === index % quotes.length);
@@ -44,65 +191,64 @@
}
function nextQuote() {
if (quotes.length < 2) {
return;
}
currentIndex = (currentIndex + 1) % quotes.length;
showQuote(currentIndex);
}
function fetchQuotes() {
var url = 'https://v1.hitokoto.cn/?c=' + encodeURIComponent(type) + '&encode=json&count=' + count;
fetch(url)
.then(function (r) { return r.json(); })
.then(function (data) {
// API returns single object or array
if (Array.isArray(data)) {
quotes = data.map(function (item) {
return { text: item.hitokoto || '', from: item.from || '' };
});
} else if (data && data.hitokoto) {
quotes = [{ text: data.hitokoto, from: data.from || '' }];
}
if (!quotes.length) {
if (textEl) textEl.textContent = '暂无内容';
return;
}
// Build dots
if (dotsEl && quotes.length > 1) {
dotsEl.innerHTML = '';
quotes.forEach(function (_, i) {
var dot = document.createElement('span');
dot.className = 'aksrt-yiyan__dot' + (i === 0 ? ' is-active' : '');
dotsEl.appendChild(dot);
});
}
showQuote(0);
// Auto-rotate
if (quotes.length > 1) {
timer = setInterval(nextQuote, interval);
}
})
.catch(function () {
if (textEl) textEl.textContent = '加载失败,请稍后再试';
});
}
// Pause on hover
el.addEventListener('mouseenter', function () {
if (timer) clearInterval(timer);
});
el.addEventListener('mouseleave', function () {
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();
fetchQuotes(type, count)
.then(function (list) {
quotes = list;
if (!quotes.length) {
textEl.textContent = '暂无内容';
return;
}
buildDots();
showQuote(0);
startTimer();
})
.catch(function () {
textEl.textContent = '加载失败,请稍后再试';
});
}
// Auto-init all yiyan widgets
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-aksrt-yiyan]').forEach(initYiyan);
});