Files
wordpress-theme-aksrt/assets/js/user-ban.js
T
kate522 a4af024f32 feat(ban): align ban prompts with the reference theme modals and block page
对照参考主题(Zibll)修正四项偏差,申诉链路从页面整体迁移至弹窗:

- feat(ban): 新增三个 1:1 复刻弹窗(REST /user-ban/{info,appeal,log}-modal):
  查看禁封状态(多彩头 c-gray + 信息行 + 申诉按钮)、禁封申诉(edit-header
  gavel/c-blue + 信息框 + 资料项 data[key] 输入 + 审核中态 + 禁止申诉态)、
  禁封记录(多彩头 c-yellow + 计数 + 逐条卡片);user-ban.js 增加
  data-aksrt-ban-info/appeal/log 委托与 admin-ajax 申诉提交
- feat(ban): 禁封/解封弹窗的「当前用户已有N次禁封记录」静态文案改为可点击
  黄链(N>1),打开禁封记录弹窗;解封视图信息行改复用统一数据源
- refactor(ban): 治理展示态收敛至 Restriction::state_badges()(封号=c-red
  违规已封号 / 小黑屋=c-yellow 小黑屋禁封中 / 禁言=c-gray 禁言中,颜色与
  字段统一管理)+ get_ban_info()(状态/起止时间/原因/说明/申诉权限,
  三弹窗与禁封页共用)
- feat(ban): 登录行为对齐参考主题——移除登录期拦截(enforce_login_ban)
  与 complete_login 禁封闸门,封号用户可正常登录;新增 template_redirect
  前台禁封页(1:1 zib_user_ban_restrict_login/zib_die_page:红盒提示 +
  信息行 + 申诉权限 + 退出登录/申诉按钮,模板 templates/parts/user/ban-block.php)
- feat(profile): 公开主页禁封状态从资料卡下方提示框改为占据身份行元信息位
  的 badg 徽章(参考主题 author_header_identity 语义);用户中心元信息改为
  but 按钮(徽章+箭头)打开查看禁封状态弹窗;评论拒绝态链接同源
- refactor: 移除申诉页面模板(page-ban-appeal.php)及其资产/注册/入队、
  get_profile_notice/profile_notice 链、state_labels/state_notices/
  login_ban_message、UserRuntime 的 public_notice/ban_notice_text 死配置、
  report_settings 死键、无消费方的 aksrt_ban_appeal_status 端点
- feat(ban): BanAppeal::ajax_submit 兼容弹窗 data[key] 表单结构
  (逐项清洗入库,申诉说明取 data['申诉说明'],旧平铺字段保持兼容)
- chore: 重新构建 public.css/public.min.css(三个新弹窗几何与头部样式)
- test: 契约测试重写(徽章元信息位、三弹窗与记录链接、禁封页、允许登录 +
  前台拦截、死代码移除断言),全量 362 项通过
2026-09-22 19:14:02 +08:00

394 lines
15 KiB
JavaScript
Raw 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.
/**
* AKSRT User Ban — 前台“禁封/解封用户”弹窗
*
* 1:1 复刻参考主题 Zibll 前台禁封能力:
* - zib_get_edit_user_ban_link → [data-aksrt-ban] 委托入口(三点菜单内)
* - zib_get_set_user_ban_modal → REST /user-ban/modal 拉取弹窗内容
* - zib_ajax_set_user_ban → REST /user-ban/set 提交
* - zib_get_ban_info_modal → [data-aksrt-ban-info](查看禁封状态)
* - zib_get_user_ban_appeal_modal → [data-aksrt-ban-appeal](禁封申诉)
* - zib_get_ban_log_modal → [data-aksrt-ban-log](禁封记录)
* - zib_user_ban_appeal(wp-ajax) → admin-ajax aksrt_ban_appeal_submit 提交
*
* 弹窗经 AksrtModalRuntime.openRemote 打开(与参考主题 refresh_modal 每次刷新
* 同语义):桌面 600px 宽(参考主题未传 data-class → Bootstrap .modal-dialog
* 默认宽 600px)/ min-height 360px(data-height=360 → .modal-content 初始高度),
* 移动端底部抽屉(mobileBottom)。提交成功语义与参考主题 zib_ajax_set_user_ban
* 返回值一致:禁封 → reload:true(整页刷新),解封 → hide_modal:true(仅关闭弹窗)。
*
* @package AKSRT
*/
(function () {
'use strict';
var API_ROOT = (function () {
var d = window.aksrtData || {};
var raw = d.apiUrl || d.restUrl || d.rest_url || '/index.php?rest_route=/aksrt/v1';
return String(raw).replace(/\/$/, '');
})();
var NONCE = (window.aksrtData && window.aksrtData.restNonce) || '';
var AJAX_URL = (window.aksrtData && window.aksrtData.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.AksrtModalRuntime || typeof window.AksrtModalRuntime.openRemote !== 'function') {
notyf('禁封组件加载失败,请刷新页面后重试', 'danger');
return;
}
window.AksrtModalRuntime.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: 'aksrt-user-ban-modal',
onContent: function (html, session) {
if (session && session.shell) bindBanModal(session.shell);
},
});
}
// 查看禁封状态弹窗(参考主题 zib_get_ban_info_modal / user_ban_info_modal)。
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: 'aksrt-user-ban-info-modal',
});
}
// 禁封申诉弹窗(参考主题 zib_get_user_ban_appeal_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: 'aksrt-user-ban-appeal-modal',
onContent: function (html, session) {
if (session && session.shell) bindAppealForm(session.shell);
},
});
}
// 禁封记录弹窗(参考主题 zib_get_ban_log_modal)。
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: 'aksrt-user-ban-log-modal',
});
}
function bindAppealForm(shell) {
var form = shell.querySelector('[data-aksrt-appeal-form]');
if (!form || form.dataset.aksrtAppealBound === '1') return;
form.dataset.aksrtAppealBound = '1';
Array.prototype.forEach.call(form.querySelectorAll('textarea.form-control'), bindAutoHeight);
form.addEventListener('submit', function (event) {
event.preventDefault();
submitAppeal(form);
});
}
function submitAppeal(form) {
var btn = form.querySelector('[data-aksrt-appeal-submit]');
if (btn) btn.disabled = true;
notyf('正在提交申诉…', 'load', 3000, 'aksrt-appeal-submit');
var fd = new FormData(form);
if (!fd.get('action')) fd.set('action', 'aksrt_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.AksrtModalRuntime && typeof window.AksrtModalRuntime.close === 'function') {
window.AksrtModalRuntime.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('aksrt-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-aksrt-ban-form] [name="desc"]');
payload.desc = (descInput && descInput.value || '').trim();
return payload;
}
var form = shell.querySelector('[data-aksrt-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-aksrt-unban-submit]' : '[data-aksrt-ban-submit]');
if (submitBtn) submitBtn.disabled = true;
notyf(unban ? '正在解封…' : '正在提交禁封…', 'load', 3000, 'aksrt-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');
// 参考主题 zib_ajax_set_user_ban 返回值语义:
// 禁封 → reload:true(整页刷新,入口按钮状态随页面重渲染);
// 解封 → hide_modal:true(仅关闭弹窗)。
if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.close === 'function') {
window.AksrtModalRuntime.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('aksrt-ban-submit');
if (toast) toast.remove();
});
}
function bindAutoHeight(textarea) {
// 1:1 复刻参考主题 textarea[autoHeight] 行为(input/focus 触发):
// 高度 = scrollHeight + 上下边框;超过 maxHeight 属性则锁定 maxHeight 并
// 打开自身滚动条(参考主题 main.js autoHeight 同语义)。
if (!textarea || textarea.dataset.aksrtAutoHeight === '1') return;
textarea.dataset.aksrtAutoHeight = '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-aksrt-ban-form]');
if (!form || form.getAttribute('data-aksrt-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-aksrt-ban-desc]'), function (el) {
el.style.display = el.getAttribute('data-aksrt-ban-desc') === ban ? '' : 'none';
});
var reasonInput = form.querySelector('input[name="reason"]:checked');
var otherBox = form.querySelector('[data-aksrt-ban-reason-other]');
if (otherBox) otherBox.style.display = reasonInput && reasonInput.value === 'other' ? '' : 'none';
var noAppealBox = form.querySelector('[data-aksrt-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);
}
/**
* 收起封面三点菜单面板(参考主题 author_header_drop_lists 菜单项点击后
* 不自动收起,需手动关闭;评论区与文章作者下拉由各自脚本点击项后自动收起)。
*/
function closeCoverMenu(trigger) {
var menu = trigger && trigger.closest ? trigger.closest('[data-aksrt-menu]') : null;
if (!menu) return;
var panel = menu.querySelector('.aksrt-cover-menu__panel');
if (panel) panel.hidden = true;
var menuTrigger = menu.querySelector('.aksrt-cover-menu__trigger');
if (menuTrigger) menuTrigger.setAttribute('aria-expanded', 'false');
}
function initBanBindings() {
if (document.documentElement.dataset.aksrtBanInitialized === '1') return;
document.documentElement.dataset.aksrtBanInitialized = '1';
document.addEventListener('click', function (event) {
if (!event.target || !event.target.closest) return;
// 禁封 / 解封入口(封面菜单按钮 / 评论区菜单项 / 文章作者菜单项)。
var banBtn = event.target.closest('[data-aksrt-ban]');
if (banBtn) {
event.preventDefault();
var userId = parseInt(banBtn.getAttribute('data-aksrt-ban'), 10);
if (userId > 0) {
closeCoverMenu(banBtn);
openBanModal(userId);
}
return;
}
// 查看禁封状态入口(用户中心元信息 / 评论拒绝态 / 禁封信息弹窗内申诉按钮)。
var infoBtn = event.target.closest('[data-aksrt-ban-info]');
if (infoBtn) {
event.preventDefault();
var infoUser = parseInt(infoBtn.getAttribute('data-aksrt-ban-info'), 10);
if (infoUser > 0) openInfoModal(infoUser);
return;
}
// 申诉入口(禁封信息弹窗 / 登录后禁封页)。
var appealBtn = event.target.closest('[data-aksrt-ban-appeal]');
if (appealBtn) {
event.preventDefault();
var appealUser = parseInt(appealBtn.getAttribute('data-aksrt-ban-appeal'), 10);
if (appealUser > 0) openAppealModal(appealUser);
return;
}
// 禁封记录入口(禁封/解封弹窗内的记录链接,仅管理员)。
var logBtn = event.target.closest('[data-aksrt-ban-log]');
if (logBtn) {
event.preventDefault();
var logUser = parseInt(logBtn.getAttribute('data-aksrt-ban-log'), 10);
if (logUser > 0) openLogModal(logUser);
return;
}
// 弹窗内交互(事件委托到 document,弹窗壳由 runtime portal 到 body)。
var active = window.AksrtModalRuntime && window.AksrtModalRuntime.getActive
? window.AksrtModalRuntime.getActive()
: null;
if (!active || !active.contains(event.target)) return;
var shell = active;
var form = shell.querySelector('[data-aksrt-ban-form]');
var modalUser = form ? parseInt(form.getAttribute('data-aksrt-ban-user'), 10) : 0;
var submitBtn = event.target.closest('[data-aksrt-ban-submit]');
if (submitBtn) {
event.preventDefault();
submitBtn.disabled = true;
submitBan(shell, modalUser, false);
return;
}
var unbanBtn = event.target.closest('[data-aksrt-unban-submit]');
if (unbanBtn) {
event.preventDefault();
unbanBtn.disabled = true;
submitBan(shell, modalUser, true);
return;
}
});
document.addEventListener('change', function (event) {
var active = window.AksrtModalRuntime && window.AksrtModalRuntime.getActive
? window.AksrtModalRuntime.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('aksrt-user-ban-modal')
? event.target
: null;
if (shell) syncBanForm(shell);
});
}
jQuery(document)
.off('auto_fun.aksrt-ban')
.on('auto_fun.aksrt-ban', initBanBindings);
})();