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:
+203
-57
@@ -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);
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-flow.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-homepage.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-single-components.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-single.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-yiyan.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-single-render.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-archive.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-archive-filters.php';
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* AKSRT Yiyan Widget — 一言轮播组件
|
||||
*
|
||||
* Fetches random quotes from hitokoto.cn API and displays with auto-rotation.
|
||||
* Category values map to Hitokoto `c` letters a–l; empty = mixed random.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
@@ -11,115 +12,142 @@ defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Widget_Yiyan extends WP_Widget {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'aksrt_yiyan',
|
||||
__('一言轮播', 'aksrt'),
|
||||
['description' => __('显示随机一言/每日语录,支持自动轮播。', 'aksrt')]
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Hitokoto category codes → labels (API `c` param).
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
private static function type_labels(): array {
|
||||
return [
|
||||
'' => __('随机', 'aksrt'),
|
||||
'a' => __('动画', 'aksrt'),
|
||||
'b' => __('漫画', 'aksrt'),
|
||||
'c' => __('游戏', 'aksrt'),
|
||||
'd' => __('文学', 'aksrt'),
|
||||
'e' => __('原创', 'aksrt'),
|
||||
'f' => __('来自网络', 'aksrt'),
|
||||
'g' => __('其他', 'aksrt'),
|
||||
'h' => __('影视', 'aksrt'),
|
||||
'i' => __('诗词', 'aksrt'),
|
||||
'j' => __('网易云', 'aksrt'),
|
||||
'k' => __('哲学', 'aksrt'),
|
||||
'l' => __('抖机灵', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
public function widget($args, $instance): void {
|
||||
$title = !empty($instance['title']) ? $instance['title'] : '';
|
||||
$count = !empty($instance['count']) ? max(3, min(20, (int) $instance['count'])) : 5;
|
||||
$interval = !empty($instance['interval']) ? max(3, min(30, (int) $instance['interval'])) : 5;
|
||||
$type = !empty($instance['type']) ? sanitize_text_field($instance['type']) : 'a';
|
||||
$icon = !empty($instance['icon']) ? $instance['icon'] : 'fas fa-quote-left';
|
||||
/**
|
||||
* Sanitize stored type to a valid Hitokoto category letter or empty.
|
||||
*/
|
||||
private static function sanitize_type(string $type): string {
|
||||
$type = strtolower(trim($type));
|
||||
return preg_match('/^[a-l]$/', $type) ? $type : '';
|
||||
}
|
||||
|
||||
echo $args['before_widget'];
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'aksrt_yiyan',
|
||||
__('一言轮播', 'aksrt'),
|
||||
['description' => __('显示随机一言/每日语录,支持自动轮播。', 'aksrt')]
|
||||
);
|
||||
}
|
||||
|
||||
if ($title) {
|
||||
echo $args['before_title'] . esc_html($title) . $args['after_title'];
|
||||
}
|
||||
public function widget($args, $instance): void {
|
||||
$title = !empty($instance['title']) ? $instance['title'] : '';
|
||||
$count = !empty($instance['count']) ? max(1, min(10, (int) $instance['count'])) : 5;
|
||||
$interval = !empty($instance['interval']) ? max(3, min(30, (int) $instance['interval'])) : 5;
|
||||
$type = self::sanitize_type((string) ($instance['type'] ?? ''));
|
||||
$icon = !empty($instance['icon']) ? $instance['icon'] : 'fas fa-quote-left';
|
||||
|
||||
$types_map = [
|
||||
'a' => '动画',
|
||||
'b' => '漫画',
|
||||
'c' => '游戏',
|
||||
'd' => '文学',
|
||||
'e' => '原创',
|
||||
'f' => '来自网络',
|
||||
'g' => '其他',
|
||||
'h' => '影视',
|
||||
'i' => '诗词',
|
||||
'j' => '网易云',
|
||||
'k' => '哲学',
|
||||
'l' => '抖机灵',
|
||||
];
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
$type_label = $types_map[$type] ?? '综合';
|
||||
if ($title) {
|
||||
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
?>
|
||||
<div class="aksrt-yiyan"
|
||||
data-aksrt-yiyan
|
||||
data-count="<?php echo esc_attr((string) $count); ?>"
|
||||
data-interval="<?php echo esc_attr((string) $interval); ?>"
|
||||
data-type="<?php echo esc_attr($type); ?>">
|
||||
<div class="aksrt-yiyan__content">
|
||||
<i class="<?php echo esc_attr($icon); ?> aksrt-yiyan__icon" aria-hidden="true"></i>
|
||||
<p class="aksrt-yiyan__text">加载中…</p>
|
||||
<p class="aksrt-yiyan__from"></p>
|
||||
</div>
|
||||
<div class="aksrt-yiyan__dots" aria-hidden="true"></div>
|
||||
</div>
|
||||
<?php
|
||||
if (function_exists('aksrt_render_yiyan_block')) {
|
||||
aksrt_render_yiyan_block([
|
||||
'class' => 'aksrt-yiyan',
|
||||
'type' => $type,
|
||||
'count' => $count,
|
||||
'interval' => $interval,
|
||||
'icon' => $icon,
|
||||
]);
|
||||
} else {
|
||||
// Fallback if single-render helpers are not loaded.
|
||||
?>
|
||||
<div class="aksrt-yiyan"
|
||||
data-aksrt-yiyan
|
||||
data-count="<?php echo esc_attr((string) $count); ?>"
|
||||
data-interval="<?php echo esc_attr((string) $interval); ?>"
|
||||
<?php echo $type !== '' ? ' data-type="' . esc_attr($type) . '"' : ''; ?>>
|
||||
<div class="aksrt-yiyan__content">
|
||||
<i class="<?php echo esc_attr($icon); ?> aksrt-yiyan__icon" aria-hidden="true"></i>
|
||||
<p class="aksrt-yiyan__text"><?php esc_html_e('加载中…', 'aksrt'); ?></p>
|
||||
<p class="aksrt-yiyan__from"></p>
|
||||
</div>
|
||||
<div class="aksrt-yiyan__dots" aria-hidden="true"></div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
echo $args['after_widget'];
|
||||
}
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
public function form($instance): void {
|
||||
$title = !empty($instance['title']) ? $instance['title'] : '一言';
|
||||
$count = !empty($instance['count']) ? (int) $instance['count'] : 5;
|
||||
$interval = !empty($instance['interval']) ? (int) $instance['interval'] : 5;
|
||||
$type = !empty($instance['type']) ? $instance['type'] : 'a';
|
||||
$icon = !empty($instance['icon']) ? $instance['icon'] : 'fas fa-quote-left';
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题', 'aksrt'); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>"
|
||||
name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text"
|
||||
value="<?php echo esc_attr($title); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('count')); ?>"><?php esc_html_e('轮播条数', 'aksrt'); ?></label>
|
||||
<input id="<?php echo esc_attr($this->get_field_id('count')); ?>"
|
||||
name="<?php echo esc_attr($this->get_field_name('count')); ?>" type="number"
|
||||
min="3" max="20" value="<?php echo esc_attr((string) $count); ?>" style="width:80px;">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('interval')); ?>"><?php esc_html_e('切换间隔(秒)', 'aksrt'); ?></label>
|
||||
<input id="<?php echo esc_attr($this->get_field_id('interval')); ?>"
|
||||
name="<?php echo esc_attr($this->get_field_name('interval')); ?>" type="number"
|
||||
min="3" max="30" value="<?php echo esc_attr((string) $interval); ?>" style="width:80px;">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('type')); ?>"><?php esc_html_e('分类', 'aksrt'); ?></label>
|
||||
<select id="<?php echo esc_attr($this->get_field_id('type')); ?>"
|
||||
name="<?php echo esc_attr($this->get_field_name('type')); ?>">
|
||||
<option value="a" <?php selected($type, 'a'); ?>><?php esc_html_e('综合', 'aksrt'); ?></option>
|
||||
<option value="d" <?php selected($type, 'd'); ?>><?php esc_html_e('文学', 'aksrt'); ?></option>
|
||||
<option value="i" <?php selected($type, 'i'); ?>><?php esc_html_e('诗词', 'aksrt'); ?></option>
|
||||
<option value="k" <?php selected($type, 'k'); ?>><?php esc_html_e('哲学', 'aksrt'); ?></option>
|
||||
<option value="h" <?php selected($type, 'h'); ?>><?php esc_html_e('影视', 'aksrt'); ?></option>
|
||||
<option value="j" <?php selected($type, 'j'); ?>><?php esc_html_e('网易云', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('icon')); ?>"><?php esc_html_e('图标', 'aksrt'); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('icon')); ?>"
|
||||
name="<?php echo esc_attr($this->get_field_name('icon')); ?>" type="text"
|
||||
value="<?php echo esc_attr($icon); ?>">
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
public function form($instance): void {
|
||||
$title = !empty($instance['title']) ? $instance['title'] : '一言';
|
||||
$count = !empty($instance['count']) ? (int) $instance['count'] : 5;
|
||||
$interval = !empty($instance['interval']) ? (int) $instance['interval'] : 5;
|
||||
$type = self::sanitize_type((string) ($instance['type'] ?? ''));
|
||||
$icon = !empty($instance['icon']) ? $instance['icon'] : 'fas fa-quote-left';
|
||||
$labels = self::type_labels();
|
||||
// Curated subset for the form (full map still used for sanitization).
|
||||
$form_types = ['', 'a', 'd', 'i', 'k', 'h', 'j'];
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题', 'aksrt'); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>"
|
||||
name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text"
|
||||
value="<?php echo esc_attr($title); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('count')); ?>"><?php esc_html_e('轮播条数', 'aksrt'); ?></label>
|
||||
<input id="<?php echo esc_attr($this->get_field_id('count')); ?>"
|
||||
name="<?php echo esc_attr($this->get_field_name('count')); ?>" type="number"
|
||||
min="1" max="10" value="<?php echo esc_attr((string) $count); ?>" style="width:80px;">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('interval')); ?>"><?php esc_html_e('切换间隔(秒)', 'aksrt'); ?></label>
|
||||
<input id="<?php echo esc_attr($this->get_field_id('interval')); ?>"
|
||||
name="<?php echo esc_attr($this->get_field_name('interval')); ?>" type="number"
|
||||
min="3" max="30" value="<?php echo esc_attr((string) $interval); ?>" style="width:80px;">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('type')); ?>"><?php esc_html_e('分类', 'aksrt'); ?></label>
|
||||
<select id="<?php echo esc_attr($this->get_field_id('type')); ?>"
|
||||
name="<?php echo esc_attr($this->get_field_name('type')); ?>">
|
||||
<?php foreach ($form_types as $code) : ?>
|
||||
<option value="<?php echo esc_attr($code); ?>" <?php selected($type, $code); ?>>
|
||||
<?php echo esc_html($labels[$code] ?? $code); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('icon')); ?>"><?php esc_html_e('图标', 'aksrt'); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('icon')); ?>"
|
||||
name="<?php echo esc_attr($this->get_field_name('icon')); ?>" type="text"
|
||||
value="<?php echo esc_attr($icon); ?>">
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance): array {
|
||||
$instance = [];
|
||||
$instance['title'] = sanitize_text_field($new_instance['title'] ?? '');
|
||||
$instance['count'] = absint($new_instance['count'] ?? 5);
|
||||
$instance['interval'] = absint($new_instance['interval'] ?? 5);
|
||||
$instance['type'] = sanitize_text_field($new_instance['type'] ?? 'a');
|
||||
$instance['icon'] = sanitize_text_field($new_instance['icon'] ?? 'fas fa-quote-left');
|
||||
return $instance;
|
||||
}
|
||||
public function update($new_instance, $old_instance): array {
|
||||
$instance = [];
|
||||
$instance['title'] = sanitize_text_field($new_instance['title'] ?? '');
|
||||
$instance['count'] = max(1, min(10, absint($new_instance['count'] ?? 5)));
|
||||
$instance['interval'] = max(3, min(30, absint($new_instance['interval'] ?? 5)));
|
||||
$instance['type'] = self::sanitize_type((string) ($new_instance['type'] ?? ''));
|
||||
$instance['icon'] = sanitize_text_field($new_instance['icon'] ?? 'fas fa-quote-left');
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,7 +313,11 @@ if (!function_exists('aksrt_single_render_content_footer')) :
|
||||
$post_id = (int) $ctx['post_id'];
|
||||
?>
|
||||
<?php if (!empty($settings['yiyan_footer'])) : ?>
|
||||
<div class="article-yiyan yiyan-box aksrt-yiyan yiyan--footer" data-aksrt-yiyan data-type="hitokoto"></div>
|
||||
<?php
|
||||
aksrt_render_yiyan_block([
|
||||
'class' => 'article-yiyan yiyan-box aksrt-yiyan yiyan--footer',
|
||||
]);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($settings['copyright_enabled'])) : ?>
|
||||
@@ -855,7 +859,11 @@ if (!function_exists('aksrt_single_render_article')) :
|
||||
|
||||
<div class="article-content">
|
||||
<?php if (!empty($settings['yiyan_header'])) : ?>
|
||||
<div class="article-yiyan yiyan-box aksrt-yiyan yiyan--header theme-box text-center radius8 main-shadow" data-aksrt-yiyan data-type="hitokoto"></div>
|
||||
<?php
|
||||
aksrt_render_yiyan_block([
|
||||
'class' => 'article-yiyan yiyan-box aksrt-yiyan yiyan--header theme-box text-center radius8 main-shadow',
|
||||
]);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php do_action('aksrt_single_content_before', get_post()); ?>
|
||||
@@ -910,9 +918,9 @@ if (!function_exists('aksrt_single_render_after_box')) :
|
||||
$settings = is_array($ctx['settings'] ?? null) ? $ctx['settings'] : [];
|
||||
|
||||
if (!empty($settings['yiyan_box'])) {
|
||||
?>
|
||||
<div class="yiyan-box main-bg theme-box text-center box-body radius8 main-shadow aksrt-yiyan" data-aksrt-yiyan data-type="hitokoto"></div>
|
||||
<?php
|
||||
aksrt_render_yiyan_block([
|
||||
'class' => 'yiyan-box main-bg theme-box text-center box-body radius8 main-shadow aksrt-yiyan',
|
||||
]);
|
||||
}
|
||||
|
||||
// Accept both `authordesc` (Settings DTO) and legacy `authordesc_s`.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* 一言 (Hitokoto) markup helper.
|
||||
*
|
||||
* Shared shell for single-post placements and the yiyan widget.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
if (!function_exists('aksrt_render_yiyan_block')) :
|
||||
/**
|
||||
* Shared 一言 shell for single-post placements (and reusable markup).
|
||||
*
|
||||
* Hitokoto category `c` must be a letter a–l, or empty for mixed random.
|
||||
* Client loads quotes via parallel fetches when count > 1.
|
||||
*
|
||||
* @param array<string,mixed> $args {
|
||||
* @type string $class Extra CSS classes on the root.
|
||||
* @type string $type Category letter a–l, or empty for random.
|
||||
* @type int $count Number of quotes to load (1–10).
|
||||
* @type int $interval Rotation interval in seconds.
|
||||
* @type string $icon Optional FA icon class; empty = no icon.
|
||||
* }
|
||||
*/
|
||||
function aksrt_render_yiyan_block(array $args = []): void {
|
||||
$class = trim((string) ($args['class'] ?? 'aksrt-yiyan'));
|
||||
$type_raw = strtolower(trim((string) ($args['type'] ?? '')));
|
||||
$type = preg_match('/^[a-l]$/', $type_raw) ? $type_raw : '';
|
||||
$count = max(1, min(10, (int) ($args['count'] ?? 5)));
|
||||
$interval = max(3, min(30, (int) ($args['interval'] ?? 8)));
|
||||
$icon = sanitize_text_field((string) ($args['icon'] ?? ''));
|
||||
|
||||
if (strpos($class, 'aksrt-yiyan') === false) {
|
||||
$class = trim($class . ' aksrt-yiyan');
|
||||
}
|
||||
?>
|
||||
<div class="<?php echo esc_attr($class); ?>"
|
||||
data-aksrt-yiyan
|
||||
data-count="<?php echo esc_attr((string) $count); ?>"
|
||||
data-interval="<?php echo esc_attr((string) $interval); ?>"
|
||||
<?php echo $type !== '' ? ' data-type="' . esc_attr($type) . '"' : ''; ?>>
|
||||
<div class="aksrt-yiyan__content">
|
||||
<?php if ($icon !== '') : ?>
|
||||
<i class="<?php echo esc_attr($icon); ?> aksrt-yiyan__icon" aria-hidden="true"></i>
|
||||
<?php endif; ?>
|
||||
<p class="aksrt-yiyan__text"><?php esc_html_e('加载中…', 'aksrt'); ?></p>
|
||||
<p class="aksrt-yiyan__from"></p>
|
||||
</div>
|
||||
<div class="aksrt-yiyan__dots" aria-hidden="true"></div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
endif;
|
||||
Reference in New Issue
Block a user