feat(identity): author card, per-user tip, comment caps, task status

Author card uses configurable limit/orderby/style and self CTAs.
Tip is per-user weixin/alipay QR modal (no site-global image).
Comment action permissions honor user_caps matrix.
Free points tasks expose done_today/passive status + better comment deep link.
audit-admin-contracts errors=0.
This commit is contained in:
2026-07-17 01:21:35 +08:00
parent b4d04ae346
commit 8d4e94a02c
10 changed files with 374 additions and 148 deletions
@@ -49,10 +49,11 @@ export const pageSchema = {
post_article_video_cover_scale: 56,
item_heading_bold: false,
post_button_toptext: '喜欢就支持一下吧',
post_rewards_enabled: false,
post_rewards_text: '',
post_rewards_image: '',
post_rewards_btn_text: '赞赏',
post_rewards_s: false,
post_rewards_text: '赞赏',
post_author_card_limit: 6,
post_author_card_orderby: 'views',
post_author_card_style: 'card',
article_maxheight_kg: false,
article_maxheight: 1000,
},
@@ -71,6 +72,17 @@ export const pageSchema = {
{ key: 'breadcrumbs_single_text', label: '末项显示「正文」', type: 'switch', default: true, dependsOn: { key: 'breadcrumbs_single_s', value: true }, help: '关闭后末项显示文章标题' },
{ key: 'post_prevnext_s', label: '上一篇/下一篇', type: 'switch', default: true },
{ key: 'post_authordesc_s', label: '作者简介', type: 'switch', default: true },
{ key: 'post_author_card_limit', label: '作者卡文章数', type: 'number', default: 6, min: 0, max: 12, dependsOn: { key: 'post_authordesc_s', value: true } },
{ key: 'post_author_card_orderby', label: '作者卡文章排序', type: 'select', default: 'views', options: [
{ label: '浏览量', value: 'views' },
{ label: '发布时间', value: 'date' },
{ label: '更新时间', value: 'modified' },
{ label: '评论数', value: 'comment_count' },
], dependsOn: { key: 'post_authordesc_s', value: true } },
{ key: 'post_author_card_style', label: '作者卡文章样式', type: 'radio', default: 'card', options: [
{ label: '卡片', value: 'card' },
{ label: '列表', value: 'list' },
], dependsOn: { key: 'post_authordesc_s', value: true } },
{ key: 'post_single_hide_time_s', label: '隐藏文章时间', type: 'switch', default: false },
],
},
@@ -137,10 +149,8 @@ export const pageSchema = {
{ key: 'post_copyright_enabled', label: '版权信息', type: 'switch', default: true },
{ key: 'post_copyright_text', label: '版权默认文案', type: 'textarea', default: '', help: '留空则自动生成"本文由 XX 原创发布"' },
{ key: 'post_button_toptext', label: '按钮上方文案', type: 'input', default: '喜欢就支持一下吧', help: '显示在点赞/打赏/分享按钮上方' },
{ key: 'post_rewards_enabled', label: '打赏', type: 'switch', default: false },
{ key: 'post_rewards_btn_text', label: '打赏按钮文案', type: 'input', default: '赞赏', dependsOn: { key: 'post_rewards_enabled', value: true } },
{ key: 'post_rewards_text', label: '打赏说明', type: 'input', default: '', dependsOn: { key: 'post_rewards_enabled', value: true } },
{ key: 'post_rewards_image', label: '打赏二维码 URL', type: 'input', default: '', dependsOn: { key: 'post_rewards_enabled', value: true } },
{ key: 'post_rewards_s', label: '用户打赏', type: 'switch', default: false, help: '开启后用户可配置微信/支付宝收款码;无码则不显示按钮。' },
{ key: 'post_rewards_text', label: '打赏按钮文案', type: 'input', default: '赞赏', dependsOn: { key: 'post_rewards_s', value: true } },
{ key: 'article_maxheight_kg', label: '正文限高', type: 'switch', default: false, help: '开启后长文默认折叠,可展开全文' },
{ key: 'article_maxheight', label: '限高像素', type: 'number', default: 1000, min: 200, max: 5000, dependsOn: { key: 'article_maxheight_kg', value: true } },
],
+28 -74
View File
@@ -1,8 +1,5 @@
/**
* Reward (打赏) — dedicated module.
*
* Handles the reward modal: amount selection, custom amount input,
* QR code display, and open/close interaction.
* Reward (打赏) — per-user QR modal openers.
*
* @package AKSRT
*/
@@ -10,95 +7,52 @@
'use strict';
function initReward() {
var btn = document.getElementById('action-reward');
if (!btn) return;
function closeAll() {
document.querySelectorAll('.reward-modal.is-open').forEach(function (m) {
m.classList.remove('is-open');
m.setAttribute('aria-hidden', 'true');
});
document.body.style.overflow = '';
}
var modal = document.getElementById('reward-modal');
if (!modal) return;
var closeBtn = modal.querySelector('.reward-modal__close');
var overlay = modal.querySelector('.reward-modal__overlay');
var amountBtns = modal.querySelectorAll('.reward-amounts__btn');
var customWrap = modal.querySelector('.reward-custom');
var customInput = modal ? modal.querySelector('.reward-custom__input') : null;
var customConfirm = modal ? modal.querySelector('.reward-custom__confirm') : null;
var selectedAmount = 0;
/* ---- open / close ---- */
function openReward() {
function openForUser(userId) {
var id = String(userId || '');
var modal = id
? document.getElementById('reward-modal-' + id)
: document.querySelector('.reward-modal');
if (!modal) return;
closeAll();
modal.classList.add('is-open');
modal.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
}
function closeReward() {
modal.classList.remove('is-open');
document.body.style.overflow = '';
// reset selection
selectedAmount = 0;
amountBtns.forEach(function (b) { b.classList.remove('is-active'); });
if (customInput) customInput.value = '';
}
btn.addEventListener('click', openReward);
document.querySelectorAll('[data-aksrt-reward-open]').forEach(function (el) {
if (el === btn) return;
el.addEventListener('click', function (e) {
e.preventDefault();
openReward();
openForUser(el.getAttribute('data-user-id') || '');
});
});
document.querySelectorAll('[data-aksrt-reward-close]').forEach(function (el) {
el.addEventListener('click', function (e) {
e.preventDefault();
closeAll();
});
});
if (closeBtn) closeBtn.addEventListener('click', closeReward);
if (overlay) overlay.addEventListener('click', closeReward);
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && modal.classList.contains('is-open')) closeReward();
if (e.key === 'Escape') closeAll();
});
/* ---- amount selection ---- */
amountBtns.forEach(function (b) {
b.addEventListener('click', function () {
amountBtns.forEach(function (x) { x.classList.remove('is-active'); });
b.classList.add('is-active');
selectedAmount = parseInt(b.dataset.amount || '0', 10);
// clear custom input when preset is chosen
if (customInput) customInput.value = '';
});
});
/* ---- custom amount ---- */
if (customInput) {
customInput.addEventListener('focus', function () {
amountBtns.forEach(function (x) { x.classList.remove('is-active'); });
selectedAmount = 0;
});
customInput.addEventListener('input', function () {
// strip non-digits
customInput.value = customInput.value.replace(/[^\d]/g, '');
});
}
if (customConfirm) {
customConfirm.addEventListener('click', function () {
var val = customInput ? parseInt(customInput.value, 10) : 0;
if (val > 0) {
selectedAmount = val;
amountBtns.forEach(function (x) { x.classList.remove('is-active'); });
}
});
}
}
/* ---- register with AksrtFrontend lifecycle ---- */
if (window.AksrtFrontend && typeof window.AksrtFrontend.register === 'function') {
window.AksrtFrontend.register('aksrt-reward', function () {
initReward();
});
} else if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initReward);
} else {
// fallback: run on DOMContentLoaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initReward);
} else {
initReward();
}
initReward();
}
})();