Files
kate522andClaude Opus 5 427968a721 chore(branding): remove leftover upstream theme identifiers
The theme was renamed Zibll/子比 → AKSRT → Jio, but upstream branding
survived the sweeps in CSS classes, the SVG sprite prefix, a persisted
widget field ID, JS events, admin Vue class names, test infrastructure
and docblocks.

Renames:
- zib-widget / zib-widget-wrap / zib-slider → jio-widget-box /
  jio-widget-box-wrap / jio-slider-box (jio-widget is taken by the modern
  widget shell, jio-box-* by the card primitives)
- zib-partners-scroll + zps-* → jio-partners-scroll + jps-*
- zib-combo-info-card + zci-* → jio-combo-info-card + jci-*
- zibsvg- → jiosvg-, including the exported picker helpers and the
  fa-zibsvg- family-prefix guard
- zibpay_type → pay_type; the dual-read fallback in PostQuery collapses
  to a single key
- [zibajax="submit"] dropped (no PHP emitter); zib_ajax.success →
  jio_ajax.success
- aks-* admin Vue classes → jio-*
- zib_palette() → palette()

User-facing vendor traces are gone: the floating "more" button default no
longer ships a zibll.com link or 子比主题 marketing copy, the notice
widget ships no preset promo entries, and the four 官网教程 links are
removed from the post-list admin schema.

Test infrastructure renamed to neutral names (docker-compose.reference.yml,
Caddyfile, article-baseline.json, modern-widgets-contract.test.cjs) with
their DB/volume/env/slug identifiers following.

Kept intentionally, documented in tests/fixtures/README.md: the golden
fixture reference fingerprint (a SHA-256 record of an external directory,
read by validate-widget-golden / generate-widget-golden-fixture /
audit-widget-system), the doesNotMatch guards that prove the old tokens are
gone, and the reference theme's own meta key in the visual fixtures.

No backward-compat or data migration: saved widget configs holding old
icon keys or the old field ID silently lose them, per the project's
rewrite-over-compatibility rule.

Verified: 132/132 node contract tests, php tests/widget-system/run.php
(146 assertions), audit-admin-contracts, audit-psr4-paths and
validate-widget-golden all green; tracked bundles regenerated and in sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-02 00:06:54 +08:00

400 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Jio User Ban — 前台“禁封/解封用户”弹窗
*
* 前台用户禁封能力:
* - 编辑禁封链接 → [data-jio-ban] 委托入口(三点菜单内)
* - 禁封弹窗内容 → REST /user-ban/modal 拉取
* - 禁封提交 → REST /user-ban/set
* - 查看禁封状态弹窗 → [data-jio-ban-info]
* - 禁封申诉弹窗 → [data-jio-ban-appeal]
* - 禁封记录弹窗 → [data-jio-ban-log]
* - 禁封申诉提交 → admin-ajax jio_ban_appeal_submit
*
* 弹窗经 JioModalRuntime.openRemote 打开(每次刷新同语义):桌面 600px 宽
* (未传 data-class → Bootstrap .modal-dialog
* 默认宽 600px)/ min-height 360px(data-height=360 → .modal-content 初始高度),
* 移动端底部抽屉(mobileBottom)。提交成功语义:
* 禁封 → reload:true(整页刷新),解封 → hide_modal:true(仅关闭弹窗)。
*
* @package Jio
*/
(function () {
'use strict';
var API_ROOT = (function () {
var d = window.jioData || {};
var raw = d.apiUrl || d.restUrl || d.rest_url || '/index.php?rest_route=/jio/v1';
return String(raw).replace(/\/$/, '');
})();
var NONCE = (window.jioData && window.jioData.restNonce) || '';
var AJAX_URL = (window.jioData && window.jioData.ajaxUrl)
|| (window._win && window._win.ajax_url)
|| '/wp-admin/admin-ajax.php';
var MODAL_HEIGHT = 360;
// data_class='modal-mini' + data-height=240:400px 宽、初始高 240。
var MINI_MODAL_HEIGHT = 240;
function ajaxUrl() {
return AJAX_URL;
}
function openRemoteModal(options) {
if (!window.JioModalRuntime || typeof window.JioModalRuntime.openRemote !== 'function') {
notyf('禁封组件加载失败,请刷新页面后重试', 'danger');
return;
}
window.JioModalRuntime.openRemote(options);
}
function openBanModal(userId) {
openRemoteModal({
url: API_ROOT + '/user-ban/modal',
method: 'GET',
query: { user_id: userId },
cache: false,
height: MODAL_HEIGHT,
mobileBottom: true,
kind: 'user-ban',
className: 'jio-user-ban-modal',
onContent: function (html, session) {
if (session && session.shell) bindBanModal(session.shell);
},
});
}
// 查看禁封状态弹窗。
function openInfoModal(userId) {
openRemoteModal({
url: API_ROOT + '/user-ban/info-modal',
method: 'GET',
query: { user_id: userId },
cache: false,
height: MINI_MODAL_HEIGHT,
mobileBottom: true,
kind: 'user-ban-info',
className: 'jio-user-ban-info-modal',
});
}
// 禁封申诉弹窗(表单走 admin-ajax)。
function openAppealModal(userId) {
openRemoteModal({
url: API_ROOT + '/user-ban/appeal-modal',
method: 'GET',
query: { user_id: userId },
cache: false,
height: MINI_MODAL_HEIGHT,
mobileBottom: true,
kind: 'user-ban-appeal',
className: 'jio-user-ban-appeal-modal',
onContent: function (html, session) {
if (session && session.shell) bindAppealForm(session.shell);
},
});
}
// 禁封记录弹窗。
function openLogModal(userId) {
openRemoteModal({
url: API_ROOT + '/user-ban/log-modal',
method: 'GET',
query: { user_id: userId },
cache: false,
height: MINI_MODAL_HEIGHT,
mobileBottom: true,
kind: 'user-ban-log',
className: 'jio-user-ban-log-modal',
});
}
function bindAppealForm(shell) {
var form = shell.querySelector('[data-jio-appeal-form]');
if (!form || form.dataset.jioAppealBound === '1') return;
form.dataset.jioAppealBound = '1';
// autoHeight 仅对显式声明 autoHeight/maxHeight 的文本框生效:
// 申诉说明框为 rows=3 自然高度 + 容器滚动,
// 无 maxHeight 时执行 grow 会被压成 0px。
Array.prototype.forEach.call(
form.querySelectorAll('textarea[autoHeight], textarea[maxHeight]'),
bindAutoHeight
);
form.addEventListener('submit', function (event) {
event.preventDefault();
submitAppeal(form);
});
}
function submitAppeal(form) {
var btn = form.querySelector('[data-jio-appeal-submit]');
if (btn) btn.disabled = true;
notyf('正在提交申诉…', 'load', 3000, 'jio-appeal-submit');
var fd = new FormData(form);
if (!fd.get('action')) fd.set('action', 'jio_ban_appeal_submit');
fetch(ajaxUrl(), { method: 'POST', body: fd, credentials: 'same-origin' })
.then(function (res) { return res.json(); })
.then(function (data) {
if (data && !data.error) {
notyf(data.msg || '申诉已提交', 'success');
if (data.reload) {
window.setTimeout(function () { window.location.reload(); }, 400);
} else if (window.JioModalRuntime && typeof window.JioModalRuntime.close === 'function') {
window.JioModalRuntime.close('submit');
}
} else {
if (btn) btn.disabled = false;
notyf((data && data.msg) || '提交失败', 'danger');
}
})
.catch(function () {
if (btn) btn.disabled = false;
notyf('网络错误,请重试', 'danger');
})
.then(function () {
var toast = document.getElementById('jio-appeal-submit');
if (toast) toast.remove();
});
}
function collectPayload(shell, userId, unban) {
var payload = { user_id: userId, ban: unban ? 0 : 1 };
if (unban) {
var descInput = shell.querySelector('[data-jio-ban-form] [name="desc"]');
payload.desc = (descInput && descInput.value || '').trim();
return payload;
}
var form = shell.querySelector('[data-jio-ban-form]');
if (!form) return payload;
var banInput = form.querySelector('input[name="ban"]:checked');
payload.ban = banInput ? parseInt(banInput.value, 10) : 1;
var timeInput = form.querySelector('input[name="time_day"]:checked');
payload.time_day = timeInput ? parseInt(timeInput.value, 10) : 7;
var reasonInput = form.querySelector('input[name="reason"]:checked');
payload.reason = reasonInput ? reasonInput.value : '';
var otherInput = form.querySelector('[name="reason_other"]');
payload.reason_other = (otherInput && otherInput.value || '').trim();
var desc = form.querySelector('[name="desc"]');
payload.desc = (desc && desc.value || '').trim();
payload.delete_content = !!(form.querySelector('[name="delete_content"]') || {}).checked;
payload.no_appeal = !!(form.querySelector('[name="no_appeal"]') || {}).checked;
var noAppealDesc = form.querySelector('[name="no_appeal_desc"]');
payload.no_appeal_desc = (noAppealDesc && noAppealDesc.value || '').trim();
return payload;
}
function submitBan(shell, userId, unban) {
var payload = collectPayload(shell, userId, unban);
if (!unban) {
var reason = payload.reason === 'other' || !payload.reason ? payload.reason_other : payload.reason;
if (!reason) {
notyf('请选择或输入禁封原因', 'danger');
return;
}
if (payload.no_appeal && !payload.no_appeal_desc) {
notyf('请输入禁止用户申诉的说明或原因', 'danger');
return;
}
}
var submitBtn = shell.querySelector(unban ? '[data-jio-unban-submit]' : '[data-jio-ban-submit]');
if (submitBtn) submitBtn.disabled = true;
notyf(unban ? '正在解封…' : '正在提交禁封…', 'load', 3000, 'jio-ban-submit');
window
.fetch(API_ROOT + '/user-ban/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': NONCE },
body: JSON.stringify(payload),
})
.then(function (res) { return res.json(); })
.then(function (result) {
if (result && result.success) {
notyf(result.message || '操作成功', 'success');
// 提交返回值语义:
// 禁封 → reload:true(整页刷新,入口按钮状态随页面重渲染);
// 解封 → hide_modal:true(仅关闭弹窗)。
if (window.JioModalRuntime && typeof window.JioModalRuntime.close === 'function') {
window.JioModalRuntime.close('submit');
}
if (!unban) window.location.reload();
} else {
if (submitBtn) submitBtn.disabled = false;
notyf((result && result.message) || '操作失败', 'danger');
}
})
.catch(function () {
if (submitBtn) submitBtn.disabled = false;
notyf('网络错误,请重试', 'danger');
})
.then(function () {
var toast = document.getElementById('jio-ban-submit');
if (toast) toast.remove();
});
}
function bindAutoHeight(textarea) {
// textarea[autoHeight] 行为(input/focus 触发):
// 高度 = scrollHeight + 上下边框;超过 maxHeight 属性则锁定 maxHeight 并
// 打开自身滚动条(main.js autoHeight 同语义)。
if (!textarea || textarea.dataset.jioAutoHeight === '1') return;
textarea.dataset.jioAutoHeight = '1';
var grow = function () {
var style = getComputedStyle(textarea);
var border = (parseInt(style.getPropertyValue('border-top-width'), 10) || 0)
+ (parseInt(style.getPropertyValue('border-bottom-width'), 10) || 0);
var max = parseInt(textarea.getAttribute('maxHeight'), 10) || 0;
textarea.style.height = 'auto';
if (max > textarea.scrollHeight) {
textarea.style.height = textarea.scrollHeight + border + 'px';
textarea.style.overflow = 'hidden';
} else {
textarea.style.height = max + 'px';
textarea.style.overflow = 'auto';
}
};
textarea.addEventListener('input', grow);
textarea.addEventListener('focus', grow);
if (textarea.offsetHeight > 0) grow();
}
function syncBanForm(shell) {
var form = shell.querySelector('[data-jio-ban-form]');
if (!form || form.getAttribute('data-jio-ban-mode') !== 'ban') return;
var banInput = form.querySelector('input[name="ban"]:checked');
var ban = banInput ? banInput.value : '1';
Array.prototype.forEach.call(form.querySelectorAll('[data-jio-ban-desc]'), function (el) {
el.style.display = el.getAttribute('data-jio-ban-desc') === ban ? '' : 'none';
});
var reasonInput = form.querySelector('input[name="reason"]:checked');
var otherBox = form.querySelector('[data-jio-ban-reason-other]');
if (otherBox) otherBox.style.display = reasonInput && reasonInput.value === 'other' ? '' : 'none';
var noAppealBox = form.querySelector('[data-jio-ban-noappeal-desc]');
var noAppealInput = form.querySelector('[name="no_appeal"]');
if (noAppealBox) noAppealBox.style.display = noAppealInput && noAppealInput.checked ? '' : 'none';
}
function bindBanModal(shell) {
Array.prototype.forEach.call(shell.querySelectorAll('textarea.form-control'), bindAutoHeight);
syncBanForm(shell);
}
/**
* 收起封面三点菜单面板(作者头部下拉菜单项点击后
* 不自动收起,需手动关闭;评论区与文章作者下拉由各自脚本点击项后自动收起)。
*/
function closeCoverMenu(trigger) {
var menu = trigger && trigger.closest ? trigger.closest('[data-jio-menu]') : null;
if (!menu) return;
var panel = menu.querySelector('.jio-cover-menu__panel');
if (panel) panel.hidden = true;
var menuTrigger = menu.querySelector('.jio-cover-menu__trigger');
if (menuTrigger) menuTrigger.setAttribute('aria-expanded', 'false');
}
function initBanBindings() {
if (document.documentElement.dataset.jioBanInitialized === '1') return;
document.documentElement.dataset.jioBanInitialized = '1';
document.addEventListener('click', function (event) {
if (!event.target || !event.target.closest) return;
// 禁封 / 解封入口(封面菜单按钮 / 评论区菜单项 / 文章作者菜单项)。
var banBtn = event.target.closest('[data-jio-ban]');
if (banBtn) {
event.preventDefault();
var userId = parseInt(banBtn.getAttribute('data-jio-ban'), 10);
if (userId > 0) {
closeCoverMenu(banBtn);
openBanModal(userId);
}
return;
}
// 查看禁封状态入口(用户中心元信息 / 评论拒绝态 / 禁封信息弹窗内申诉按钮)。
var infoBtn = event.target.closest('[data-jio-ban-info]');
if (infoBtn) {
event.preventDefault();
var infoUser = parseInt(infoBtn.getAttribute('data-jio-ban-info'), 10);
if (infoUser > 0) openInfoModal(infoUser);
return;
}
// 申诉入口(禁封信息弹窗 / 登录后禁封页)。
var appealBtn = event.target.closest('[data-jio-ban-appeal]');
if (appealBtn) {
event.preventDefault();
var appealUser = parseInt(appealBtn.getAttribute('data-jio-ban-appeal'), 10);
if (appealUser > 0) openAppealModal(appealUser);
return;
}
// 禁封记录入口(禁封/解封弹窗内的记录链接,仅管理员)。
var logBtn = event.target.closest('[data-jio-ban-log]');
if (logBtn) {
event.preventDefault();
var logUser = parseInt(logBtn.getAttribute('data-jio-ban-log'), 10);
if (logUser > 0) openLogModal(logUser);
return;
}
// 弹窗内交互(事件委托到 document,弹窗壳由 runtime portal 到 body)。
var active = window.JioModalRuntime && window.JioModalRuntime.getActive
? window.JioModalRuntime.getActive()
: null;
if (!active || !active.contains(event.target)) return;
var shell = active;
var form = shell.querySelector('[data-jio-ban-form]');
var modalUser = form ? parseInt(form.getAttribute('data-jio-ban-user'), 10) : 0;
var submitBtn = event.target.closest('[data-jio-ban-submit]');
if (submitBtn) {
event.preventDefault();
submitBtn.disabled = true;
submitBan(shell, modalUser, false);
return;
}
var unbanBtn = event.target.closest('[data-jio-unban-submit]');
if (unbanBtn) {
event.preventDefault();
unbanBtn.disabled = true;
submitBan(shell, modalUser, true);
return;
}
});
document.addEventListener('change', function (event) {
var active = window.JioModalRuntime && window.JioModalRuntime.getActive
? window.JioModalRuntime.getActive()
: null;
if (!active || !event.target || !active.contains(event.target)) return;
syncBanForm(active);
});
// 弹窗内容注入后由 onContent 绑定;bs.modal 展示契约下对 user-ban 弹窗做同步。
jQuery(document).on('loaded.bs.modal show.bs.modal', function (event) {
var shell = event.target && event.target.classList && event.target.classList.contains('jio-user-ban-modal')
? event.target
: null;
if (shell) syncBanForm(shell);
});
}
jQuery(document)
.off('auto_fun.jio-ban')
.on('auto_fun.jio-ban', initBanBindings);
})();