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.
256 lines
6.2 KiB
JavaScript
256 lines
6.2 KiB
JavaScript
/**
|
||
* 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
|
||
*/
|
||
|
||
(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;
|
||
}
|
||
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();
|
||
})
|
||
.catch(function () {
|
||
textEl.textContent = '加载失败,请稍后再试';
|
||
});
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
document.querySelectorAll('[data-aksrt-yiyan]').forEach(initYiyan);
|
||
});
|
||
})();
|