修复了一些问题
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
!/tests/widget-system/user-badges-contract.test.cjs
|
||||
!/tests/widget-system/user-features-contract.test.cjs
|
||||
!/tests/widget-system/notification-preferences-contract.test.cjs
|
||||
!/tests/widget-system/message-center-contract.test.cjs
|
||||
!/tests/widget-system/asset-ledger-contract.test.cjs
|
||||
!/tests/widget-system/financial-flow-contract.test.cjs
|
||||
!/tests/widget-system/pay-orders-filter-contract.test.cjs
|
||||
|
||||
@@ -21,18 +21,6 @@
|
||||
<n-form-item label="资源下载就绪通知">
|
||||
<n-switch v-model:value="settings.enabled_types.download_ready" />
|
||||
</n-form-item>
|
||||
<n-form-item label="积分增加通知">
|
||||
<n-switch v-model:value="settings.enabled_types.asset_points_increase" />
|
||||
</n-form-item>
|
||||
<n-form-item label="积分扣减通知">
|
||||
<n-switch v-model:value="settings.enabled_types.asset_points_decrease" />
|
||||
</n-form-item>
|
||||
<n-form-item label="余额增加通知">
|
||||
<n-switch v-model:value="settings.enabled_types.asset_balance_increase" />
|
||||
</n-form-item>
|
||||
<n-form-item label="余额扣减通知">
|
||||
<n-switch v-model:value="settings.enabled_types.asset_balance_decrease" />
|
||||
</n-form-item>
|
||||
<n-form-item label="邮箱待验证提醒">
|
||||
<n-switch v-model:value="settings.enabled_types.verify_email" />
|
||||
</n-form-item>
|
||||
@@ -96,10 +84,6 @@ const settings = reactive({
|
||||
comment_reply: true,
|
||||
comment_approved: true,
|
||||
download_ready: true,
|
||||
asset_points_increase: true,
|
||||
asset_points_decrease: true,
|
||||
asset_balance_increase: true,
|
||||
asset_balance_decrease: true,
|
||||
verify_email: true,
|
||||
pending_comments: true,
|
||||
upload_moderation: true,
|
||||
|
||||
@@ -163,10 +163,6 @@ function getTypeTag(type) {
|
||||
system: 'info',
|
||||
comment_reply: 'success',
|
||||
comment_approved: 'success',
|
||||
asset_points_increase: 'warning',
|
||||
asset_points_decrease: 'warning',
|
||||
asset_balance_increase: 'success',
|
||||
asset_balance_decrease: 'error',
|
||||
download_ready: 'primary',
|
||||
verify_email: 'info',
|
||||
pending_comments: 'warning',
|
||||
@@ -180,10 +176,6 @@ function getTypeLabel(type) {
|
||||
system: '系统通知',
|
||||
comment_reply: '评论回复',
|
||||
comment_approved: '评论审核',
|
||||
asset_points_increase: '积分增加',
|
||||
asset_points_decrease: '积分扣减',
|
||||
asset_balance_increase: '余额增加',
|
||||
asset_balance_decrease: '余额扣减',
|
||||
download_ready: '下载就绪',
|
||||
verify_email: '邮箱验证',
|
||||
pending_comments: '待审评论',
|
||||
|
||||
@@ -101,6 +101,13 @@
|
||||
/* ---- 右侧内容 ---- */
|
||||
.messages-center__content {
|
||||
min-width: 0;
|
||||
outline: none;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.messages-center.is-loading .messages-center__content {
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.messages-list {
|
||||
@@ -166,6 +173,20 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button.message-item__title {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.message-item__title:hover {
|
||||
color: var(--color-primary, #3b82f6);
|
||||
}
|
||||
|
||||
.message-item__new-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
@@ -177,6 +198,94 @@
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ---- 消息详情(右侧 DOM 视图,不改变地址) ---- */
|
||||
.message-detail {
|
||||
min-height: 360px;
|
||||
padding: 28px 32px;
|
||||
background: var(--surface-card, #fff);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.message-detail__header {
|
||||
position: relative;
|
||||
padding: 48px 0 22px;
|
||||
border-bottom: 1px solid var(--color-line);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message-detail__back {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 7px 10px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: var(--surface-hover, rgba(0, 0, 0, 0.04));
|
||||
color: var(--color-ink-3, #666);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-detail__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
margin-bottom: 14px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary-light, rgba(59, 130, 246, 0.1));
|
||||
color: var(--color-primary, #3b82f6);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.message-detail__title {
|
||||
margin: 0 0 8px;
|
||||
color: var(--color-ink);
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.message-detail__meta {
|
||||
margin: 0;
|
||||
color: var(--color-ink-4, #999);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-detail__content {
|
||||
min-height: 140px;
|
||||
padding: 28px 4px;
|
||||
color: var(--color-ink-2, #444);
|
||||
font-size: 15px;
|
||||
line-height: 1.9;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.message-detail__content p {
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
.message-detail__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--color-line);
|
||||
}
|
||||
|
||||
.message-detail__primary {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
background: var(--color-primary, #3b82f6);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
html.dark .message-detail {
|
||||
background: var(--surface-1);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.message-item__desc {
|
||||
font-size: 13px;
|
||||
color: var(--color-ink-3, #666);
|
||||
@@ -301,6 +410,25 @@
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.message-detail {
|
||||
min-height: 300px;
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.message-detail__title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.message-detail__actions {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message-detail__primary,
|
||||
.message-detail__actions .message-item__action-btn {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 暗色模式 ---- */
|
||||
|
||||
+122
-125
@@ -1,175 +1,172 @@
|
||||
/**
|
||||
* AKSRT messages center interactions.
|
||||
* AKSRT message center DOM navigation.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var root = document.querySelector('[data-message-center]');
|
||||
if (!root) return;
|
||||
|
||||
var nonce = typeof aksrtData !== 'undefined' ? aksrtData.nonce : '';
|
||||
var ajaxUrl = typeof aksrtData !== 'undefined' ? aksrtData.ajaxUrl : '/wp-admin/admin-ajax.php';
|
||||
var pollTimer = null;
|
||||
var content = root.querySelector('[data-message-content]');
|
||||
var nav = root.querySelector('[data-message-nav]');
|
||||
var state = { view: root.dataset.view || 'unread', page: 1, detail: 0 };
|
||||
var pollInterval = 60000;
|
||||
|
||||
function post(action, data) {
|
||||
var body = new URLSearchParams({ action: action, nonce: nonce });
|
||||
if (data) {
|
||||
Object.keys(data).forEach(function (key) {
|
||||
var value = data[key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(function (item) {
|
||||
body.append(key + '[]', item);
|
||||
});
|
||||
return;
|
||||
}
|
||||
Object.keys(data || {}).forEach(function (key) {
|
||||
var value = data[key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(function (item) { body.append(key + '[]', item); });
|
||||
} else {
|
||||
body.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
return fetch(ajaxUrl, { method: 'POST', body: body, credentials: 'same-origin' })
|
||||
.then(function (response) { return response.json(); });
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var item = event.target.closest('.messages-nav__item[data-type]');
|
||||
if (!item) return;
|
||||
function routeFor(view) {
|
||||
var link = nav.querySelector('[data-message-view="' + view + '"]');
|
||||
return link ? link.href : window.location.href;
|
||||
}
|
||||
|
||||
function updateHeaderBadge(unread) {
|
||||
var count = Math.max(0, Number(unread) || 0);
|
||||
var headerBadge = document.querySelector('.aksrt-notification-badge');
|
||||
var avatarDot = document.querySelector('.nav-user__dot');
|
||||
if (headerBadge) {
|
||||
headerBadge.textContent = String(count);
|
||||
headerBadge.style.display = count > 0 ? '' : 'none';
|
||||
}
|
||||
if (avatarDot) avatarDot.style.display = count > 0 ? '' : 'none';
|
||||
}
|
||||
|
||||
function renderError(message) {
|
||||
content.innerHTML = '<div class="messages-empty"><span class="messages-empty__icon"><i class="fa fas fa-exclamation-circle"></i></span><p class="messages-empty__text"></p></div>';
|
||||
content.querySelector('.messages-empty__text').textContent = message || '加载失败,请稍后重试';
|
||||
}
|
||||
|
||||
function load(next, options) {
|
||||
var requested = {
|
||||
view: next.view || state.view,
|
||||
page: next.page || 1,
|
||||
detail: next.detail || 0,
|
||||
};
|
||||
var settings = options || {};
|
||||
root.classList.add('is-loading');
|
||||
content.setAttribute('aria-busy', 'true');
|
||||
|
||||
return post('aksrt_load_message_center', {
|
||||
view: requested.view,
|
||||
page: requested.page,
|
||||
notification_id: requested.detail,
|
||||
}).then(function (result) {
|
||||
if (!result.success || !result.data) {
|
||||
throw new Error(result.data && result.data.message ? result.data.message : '加载失败,请稍后重试');
|
||||
}
|
||||
state = requested;
|
||||
root.dataset.view = state.view;
|
||||
nav.innerHTML = result.data.nav;
|
||||
content.innerHTML = result.data.content;
|
||||
updateHeaderBadge(result.data.unread);
|
||||
|
||||
if (settings.history === 'push') {
|
||||
window.history.pushState(state, '', routeFor(state.view));
|
||||
} else if (settings.history === 'replace') {
|
||||
window.history.replaceState(state, '', routeFor(state.view));
|
||||
}
|
||||
content.focus({ preventScroll: true });
|
||||
}).catch(function (error) {
|
||||
renderError(error.message);
|
||||
}).finally(function () {
|
||||
root.classList.remove('is-loading');
|
||||
content.removeAttribute('aria-busy');
|
||||
});
|
||||
}
|
||||
|
||||
function reloadList() {
|
||||
return load({ view: state.view, page: state.page, detail: 0 });
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var tab = event.target.closest('[data-message-view]');
|
||||
if (!tab || !root.contains(tab) || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
||||
event.preventDefault();
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.set('msg_type', item.dataset.type);
|
||||
url.searchParams.delete('msg_page');
|
||||
window.location.href = url.toString();
|
||||
load({ view: tab.dataset.messageView, page: 1, detail: 0 }, { history: 'push' });
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var button = event.target.closest('.js-msg-detail');
|
||||
if (!button || !root.contains(button)) return;
|
||||
event.preventDefault();
|
||||
load({ view: button.dataset.view || state.view, page: state.page, detail: button.dataset.id });
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var button = event.target.closest('[data-message-back]');
|
||||
if (!button || !root.contains(button)) return;
|
||||
load({ view: button.dataset.view || state.view, page: state.page, detail: 0 });
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var button = event.target.closest('[data-message-page]');
|
||||
if (!button || !root.contains(button)) return;
|
||||
load({ view: state.view, page: button.dataset.messagePage, detail: 0 });
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var button = event.target.closest('.js-msg-mark-read');
|
||||
if (!button) return;
|
||||
|
||||
var id = button.dataset.id;
|
||||
var msgItem = button.closest('.message-item');
|
||||
post('aksrt_mark_notification_read', { notification_id: id }).then(function (result) {
|
||||
if (!result.success) return;
|
||||
|
||||
if (msgItem) {
|
||||
msgItem.classList.remove('is-unread');
|
||||
}
|
||||
button.remove();
|
||||
updateBadges(result.data && result.data.unread !== undefined ? result.data.unread : countUnread());
|
||||
if (!button || !root.contains(button)) return;
|
||||
post('aksrt_mark_notification_read', { notification_id: button.dataset.id }).then(function (result) {
|
||||
if (result.success) reloadList();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var button = event.target.closest('.js-msg-delete');
|
||||
if (!button) return;
|
||||
|
||||
var id = button.dataset.id;
|
||||
var msgItem = button.closest('.message-item');
|
||||
post('aksrt_delete_notification', { notification_id: id }).then(function (result) {
|
||||
if (!result.success) return;
|
||||
|
||||
if (msgItem) {
|
||||
msgItem.style.opacity = '0';
|
||||
msgItem.style.transform = 'translateX(20px)';
|
||||
msgItem.style.transition = 'all 0.2s';
|
||||
setTimeout(function () { msgItem.remove(); }, 200);
|
||||
}
|
||||
|
||||
updateBadges(result.data && result.data.unread !== undefined ? result.data.unread : countUnread());
|
||||
checkEmpty();
|
||||
if (!button || !root.contains(button)) return;
|
||||
post('aksrt_delete_notification', { notification_id: button.dataset.id }).then(function (result) {
|
||||
if (result.success) reloadList();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var button = event.target.closest('.js-msg-mark-all-read');
|
||||
if (!button) return;
|
||||
|
||||
if (!button || !root.contains(button)) return;
|
||||
post('aksrt_mark_notification_read', { mark_all: '1' }).then(function (result) {
|
||||
if (!result.success) return;
|
||||
|
||||
document.querySelectorAll('.message-item.is-unread').forEach(function (item) {
|
||||
item.classList.remove('is-unread');
|
||||
});
|
||||
document.querySelectorAll('.js-msg-mark-read').forEach(function (item) {
|
||||
item.remove();
|
||||
});
|
||||
updateBadges(0);
|
||||
if (result.success) reloadList();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var button = event.target.closest('.js-msg-clear-read');
|
||||
if (!button) return;
|
||||
|
||||
var readItems = document.querySelectorAll('.message-item:not(.is-unread)');
|
||||
if (!button || !root.contains(button)) return;
|
||||
var ids = [];
|
||||
readItems.forEach(function (item) {
|
||||
var id = item.dataset.notificationId;
|
||||
if (id) ids.push(id);
|
||||
root.querySelectorAll('.message-item:not(.is-unread)[data-notification-id]').forEach(function (item) {
|
||||
if (/^\d+$/.test(item.dataset.notificationId)) ids.push(item.dataset.notificationId);
|
||||
});
|
||||
if (!ids.length) return;
|
||||
|
||||
post('aksrt_delete_notifications', { notification_ids: ids }).then(function (result) {
|
||||
if (!result.success) return;
|
||||
|
||||
readItems.forEach(function (item) {
|
||||
item.style.opacity = '0';
|
||||
item.style.transform = 'translateX(20px)';
|
||||
item.style.transition = 'all 0.2s';
|
||||
setTimeout(function () { item.remove(); }, 200);
|
||||
});
|
||||
setTimeout(checkEmpty, 250);
|
||||
updateBadges(result.data && result.data.unread !== undefined ? result.data.unread : countUnread());
|
||||
if (result.success) reloadList();
|
||||
});
|
||||
});
|
||||
|
||||
function countUnread() {
|
||||
return document.querySelectorAll('.message-item.is-unread').length;
|
||||
}
|
||||
|
||||
function updateBadges(unread) {
|
||||
var nextUnread = unread === undefined ? countUnread() : unread;
|
||||
|
||||
document.querySelectorAll('.messages-nav__badge[data-type="unread"]').forEach(function (badge) {
|
||||
badge.textContent = String(nextUnread);
|
||||
badge.dataset.count = String(nextUnread);
|
||||
badge.style.display = nextUnread > 0 ? '' : 'none';
|
||||
});
|
||||
|
||||
// Numeric badge inside the user dropdown only — never target the pure avatar
|
||||
// .nav-user__dot.aksrt-notification-badge (no textContent on a dot indicator).
|
||||
var headerBadge = document.querySelector('.aksrt-notification-badge');
|
||||
if (headerBadge) {
|
||||
headerBadge.textContent = String(nextUnread);
|
||||
headerBadge.style.display = nextUnread > 0 ? '' : 'none';
|
||||
window.addEventListener('popstate', function (event) {
|
||||
var previous = event.state;
|
||||
if (previous && previous.view) {
|
||||
load(previous);
|
||||
}
|
||||
});
|
||||
|
||||
var avatarDot = document.querySelector('.nav-user__dot');
|
||||
if (avatarDot) {
|
||||
avatarDot.style.display = nextUnread > 0 ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function checkEmpty() {
|
||||
var list = document.querySelector('.messages-list');
|
||||
if (list && list.children.length === 0) {
|
||||
list.outerHTML = '<div class="messages-empty"><span class="messages-empty__icon"><i class="fa fas fa-bell-slash"></i></span><p class="messages-empty__text">暂无消息</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
pollTimer = setInterval(function () {
|
||||
post('aksrt_check_new_notifications', {}).then(function (result) {
|
||||
if (!result.success || !result.data) return;
|
||||
if (result.data.unread !== undefined) {
|
||||
updateBadges(result.data.unread);
|
||||
return;
|
||||
}
|
||||
if (result.data.count !== undefined) {
|
||||
updateBadges(result.data.count);
|
||||
}
|
||||
}).catch(function () {});
|
||||
}, pollInterval);
|
||||
}
|
||||
|
||||
startPolling();
|
||||
window.history.replaceState(state, '', routeFor(state.view));
|
||||
window.setInterval(function () {
|
||||
post('aksrt_check_new_notifications', {}).then(function (result) {
|
||||
if (result.success && result.data) updateHeaderBadge(result.data.unread);
|
||||
}).catch(function () {});
|
||||
}, pollInterval);
|
||||
})();
|
||||
|
||||
@@ -249,9 +249,6 @@ if (class_exists('\AKSRT\Ai\AI')) {
|
||||
if (class_exists('\AKSRT\Payment\WeChatNotify')) {
|
||||
\AKSRT\Payment\WeChatNotify::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Messages\Message')) {
|
||||
\AKSRT\Messages\Message::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Messages\ReportCenter')) {
|
||||
\AKSRT\Messages\ReportCenter::get_instance()->init();
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-shortcodes.php';
|
||||
require_once AKSRT_DIR . '/inc/modules/captcha.php';
|
||||
require_once AKSRT_DIR . '/inc/modules/user-hub.php';
|
||||
require_once AKSRT_DIR . '/inc/modules/user-notifications.php';
|
||||
require_once AKSRT_DIR . '/inc/modules/message-center.php';
|
||||
require_once AKSRT_DIR . '/inc/modules/creator-revenue.php';
|
||||
|
||||
// Stage 3: core hooks
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
/**
|
||||
* Message center query and server-side fragment renderers.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** @return array<string,string> */
|
||||
function aksrt_message_center_views(): array {
|
||||
return [
|
||||
'unread' => __('未读消息', 'aksrt'),
|
||||
'posts' => __('文章评论', 'aksrt'),
|
||||
'like' => __('点赞喜欢', 'aksrt'),
|
||||
'system' => __('系统通知', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
function aksrt_normalize_message_view(string $view): string {
|
||||
$view = sanitize_key($view);
|
||||
return array_key_exists($view, aksrt_message_center_views()) ? $view : 'unread';
|
||||
}
|
||||
|
||||
function aksrt_message_category_for_type(string $type): string {
|
||||
$type = sanitize_key($type);
|
||||
if (str_starts_with($type, 'asset_')) {
|
||||
return '';
|
||||
}
|
||||
if (
|
||||
in_array($type, ['like', 'post_like', 'comment_like', 'follow', 'followed', 'hot'], true)
|
||||
|| str_starts_with($type, 'like_')
|
||||
|| str_starts_with($type, 'follow_')
|
||||
|| str_starts_with($type, 'followed_')
|
||||
) {
|
||||
return 'like';
|
||||
}
|
||||
if (
|
||||
in_array($type, ['posts', 'post', 'comment', 'comment_reply', 'comment_approved', 'pending_comments', 'favorite', 'post_favorite'], true)
|
||||
|| str_starts_with($type, 'comment_')
|
||||
|| str_starts_with($type, 'favorite_')
|
||||
) {
|
||||
return 'posts';
|
||||
}
|
||||
return 'system';
|
||||
}
|
||||
|
||||
/** @return array<int,array<string,mixed>> */
|
||||
function aksrt_get_message_center_items(int $user_id, string $view): array {
|
||||
$view = aksrt_normalize_message_view($view);
|
||||
$items = function_exists('aksrt_get_user_all_notifications')
|
||||
? aksrt_get_user_all_notifications($user_id)
|
||||
: [];
|
||||
|
||||
return array_values(array_filter($items, static function (array $item) use ($view): bool {
|
||||
$category = aksrt_message_category_for_type((string) ($item['type'] ?? 'notice'));
|
||||
if ($category === '') {
|
||||
return false;
|
||||
}
|
||||
if ($view === 'unread') {
|
||||
return empty($item['read']);
|
||||
}
|
||||
return $category === $view;
|
||||
}));
|
||||
}
|
||||
|
||||
/** @return array{unread:int,posts:int,like:int,system:int} */
|
||||
function aksrt_get_message_center_counts(int $user_id): array {
|
||||
$counts = ['unread' => 0, 'posts' => 0, 'like' => 0, 'system' => 0];
|
||||
$items = function_exists('aksrt_get_user_all_notifications')
|
||||
? aksrt_get_user_all_notifications($user_id)
|
||||
: [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$category = aksrt_message_category_for_type((string) ($item['type'] ?? 'notice'));
|
||||
if ($category === '' || !empty($item['read'])) {
|
||||
continue;
|
||||
}
|
||||
$counts['unread']++;
|
||||
$counts[$category]++;
|
||||
}
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
function aksrt_get_message_center_item(int $user_id, int $notification_id): ?array {
|
||||
if ($user_id <= 0 || $notification_id <= 0) {
|
||||
return null;
|
||||
}
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
if ($table === '') return null;
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT id, type, title, description, target_url, is_read, created_at, read_at FROM {$table} WHERE id = %d AND user_id = %d",
|
||||
$notification_id,
|
||||
$user_id
|
||||
), ARRAY_A);
|
||||
if (!is_array($row) || aksrt_message_category_for_type((string) ($row['type'] ?? 'notice')) === '') return null;
|
||||
return [
|
||||
'id' => (string) $row['id'],
|
||||
'type' => (string) $row['type'],
|
||||
'title' => (string) $row['title'],
|
||||
'desc' => (string) ($row['description'] ?? ''),
|
||||
'url' => (string) ($row['target_url'] ?? ''),
|
||||
'read' => !empty($row['is_read']) ? 1 : 0,
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
'read_at' => (string) ($row['read_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
function aksrt_message_icon_class(string $type): string {
|
||||
$icons = [
|
||||
'comment_reply' => 'fa fas fa-comment-dots',
|
||||
'comment_approved' => 'fa fas fa-check-circle',
|
||||
'pending_comments' => 'fa fas fa-clock',
|
||||
'post_favorite' => 'fa fas fa-star',
|
||||
'post_like' => 'fa fas fa-heart',
|
||||
'comment_like' => 'fa fas fa-thumbs-up',
|
||||
'followed' => 'fa fas fa-user-plus',
|
||||
'download_ready' => 'fa fas fa-download',
|
||||
'upload_moderation'=> 'fa fas fa-image',
|
||||
'verify_email' => 'fa fas fa-envelope-open',
|
||||
'pay' => 'fa fas fa-file-alt',
|
||||
'order_paid' => 'fa fas fa-file-alt',
|
||||
'order_refund' => 'fa fas fa-undo',
|
||||
'vip' => 'fa fas fa-certificate',
|
||||
'pay_vip' => 'fa fas fa-certificate',
|
||||
'medal' => 'fa fas fa-certificate',
|
||||
'badge' => 'fa fas fa-certificate',
|
||||
'ban_appeal' => 'fa fas fa-gavel',
|
||||
'user_ban' => 'fa fas fa-ban',
|
||||
'auth_apply' => 'fa fas fa-id-badge',
|
||||
'auth_apply_reply' => 'fa fas fa-id-badge',
|
||||
];
|
||||
return $icons[$type] ?? 'fa fas fa-bell';
|
||||
}
|
||||
|
||||
function aksrt_message_time(array $item): string {
|
||||
if (!empty($item['stateful'])) {
|
||||
return __('状态提醒', 'aksrt');
|
||||
}
|
||||
$created_at = (string) ($item['created_at'] ?? '');
|
||||
return $created_at !== '' ? get_date_from_gmt($created_at, 'Y-m-d H:i') : '';
|
||||
}
|
||||
|
||||
function aksrt_render_message_center_nav(string $view, array $counts): void {
|
||||
$view = aksrt_normalize_message_view($view);
|
||||
?>
|
||||
<ul class="messages-nav__list">
|
||||
<?php foreach (aksrt_message_center_views() as $key => $label) : ?>
|
||||
<li>
|
||||
<a href="<?php echo esc_url(aksrt_get_messages_page_url($key)); ?>"
|
||||
class="messages-nav__item<?php echo $view === $key ? ' is-active' : ''; ?>"
|
||||
data-message-view="<?php echo esc_attr($key); ?>"
|
||||
<?php echo $view === $key ? 'aria-current="page"' : ''; ?>>
|
||||
<span><?php echo esc_html($label); ?></span>
|
||||
<span class="messages-nav__badge" data-type="<?php echo esc_attr($key); ?>" data-count="<?php echo esc_attr((string) ($counts[$key] ?? 0)); ?>"><?php echo esc_html((string) ($counts[$key] ?? 0)); ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<div class="messages-nav__actions">
|
||||
<button type="button" class="messages-nav__action-btn js-msg-mark-all-read"><?php esc_html_e('全部已读', 'aksrt'); ?></button>
|
||||
<button type="button" class="messages-nav__action-btn js-msg-clear-read"><?php esc_html_e('清除已读', 'aksrt'); ?></button>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
function aksrt_render_message_item(array $item, string $view): void {
|
||||
$type = (string) ($item['type'] ?? 'notice');
|
||||
$id = (string) ($item['id'] ?? '');
|
||||
$title = (string) ($item['title'] ?? '');
|
||||
$description = (string) ($item['desc'] ?? '');
|
||||
$target_url = (string) ($item['url'] ?? '');
|
||||
$is_read = !empty($item['read']);
|
||||
$is_stateful = !empty($item['stateful']);
|
||||
?>
|
||||
<article class="message-item<?php echo $is_read ? '' : ' is-unread'; ?>" data-notification-id="<?php echo esc_attr($id); ?>">
|
||||
<span class="message-item__icon"><i class="<?php echo esc_attr(aksrt_message_icon_class($type)); ?>" aria-hidden="true"></i></span>
|
||||
<div class="message-item__body">
|
||||
<?php if (!$is_stateful) : ?>
|
||||
<button type="button" class="message-item__title js-msg-detail" data-id="<?php echo esc_attr($id); ?>" data-view="<?php echo esc_attr($view); ?>">
|
||||
<?php echo esc_html($title); ?>
|
||||
<?php if (!$is_read) : ?><span class="message-item__new-badge"><?php esc_html_e('新', 'aksrt'); ?></span><?php endif; ?>
|
||||
</button>
|
||||
<?php else : ?>
|
||||
<div class="message-item__title"><?php echo esc_html($title); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($description !== '') : ?><div class="message-item__desc"><?php echo esc_html($description); ?></div><?php endif; ?>
|
||||
<div class="message-item__time"><?php echo esc_html(aksrt_message_time($item)); ?></div>
|
||||
</div>
|
||||
<div class="message-item__actions">
|
||||
<?php if ($is_stateful && $target_url !== '') : ?><a href="<?php echo esc_url($target_url); ?>" class="message-item__action-btn"><?php esc_html_e('立即处理', 'aksrt'); ?></a><?php endif; ?>
|
||||
<?php if (!$is_stateful) : ?><button type="button" class="message-item__action-btn js-msg-detail" data-id="<?php echo esc_attr($id); ?>" data-view="<?php echo esc_attr($view); ?>"><?php esc_html_e('详情', 'aksrt'); ?></button><?php endif; ?>
|
||||
<?php if (!$is_stateful && !$is_read) : ?><button type="button" class="message-item__action-btn js-msg-mark-read" data-id="<?php echo esc_attr($id); ?>"><?php esc_html_e('已读', 'aksrt'); ?></button><?php endif; ?>
|
||||
<?php if (!$is_stateful) : ?><button type="button" class="message-item__action-btn message-item__action-btn--danger js-msg-delete" data-id="<?php echo esc_attr($id); ?>"><?php esc_html_e('删除', 'aksrt'); ?></button><?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php
|
||||
}
|
||||
|
||||
function aksrt_render_message_center_list(int $user_id, string $view, int $page = 1): void {
|
||||
$items = aksrt_get_message_center_items($user_id, $view);
|
||||
$per_page = 20;
|
||||
$total_pages = max(1, (int) ceil(count($items) / $per_page));
|
||||
$page = max(1, min($total_pages, $page));
|
||||
$items = array_slice($items, ($page - 1) * $per_page, $per_page);
|
||||
|
||||
if (!$items) {
|
||||
?>
|
||||
<div class="messages-empty"><span class="messages-empty__icon"><i class="fa fas fa-bell-slash"></i></span><p class="messages-empty__text"><?php esc_html_e('暂无消息', 'aksrt'); ?></p></div>
|
||||
<?php
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="messages-list">
|
||||
<?php foreach ($items as $item) { aksrt_render_message_item($item, $view); } ?>
|
||||
</div>
|
||||
<?php if ($total_pages > 1) : ?>
|
||||
<nav class="messages-pagination" aria-label="<?php esc_attr_e('消息分页', 'aksrt'); ?>">
|
||||
<?php for ($number = 1; $number <= $total_pages; $number++) : ?>
|
||||
<button type="button" class="profile-status-chip<?php echo $number === $page ? ' is-active' : ''; ?>" data-message-page="<?php echo esc_attr((string) $number); ?>" <?php disabled($number, $page); ?>><?php echo esc_html((string) $number); ?></button>
|
||||
<?php endfor; ?>
|
||||
</nav>
|
||||
<?php endif;
|
||||
}
|
||||
|
||||
function aksrt_render_message_center_detail(array $item, string $view): void {
|
||||
$type = (string) ($item['type'] ?? 'notice');
|
||||
$target_url = (string) ($item['url'] ?? '');
|
||||
?>
|
||||
<article class="message-detail" data-notification-id="<?php echo esc_attr((string) ($item['id'] ?? '')); ?>">
|
||||
<header class="message-detail__header">
|
||||
<button type="button" class="message-detail__back" data-message-back data-view="<?php echo esc_attr($view); ?>"><i class="fa fas fa-arrow-left" aria-hidden="true"></i> <?php esc_html_e('返回消息列表', 'aksrt'); ?></button>
|
||||
<span class="message-detail__icon"><i class="<?php echo esc_attr(aksrt_message_icon_class($type)); ?>" aria-hidden="true"></i></span>
|
||||
<h2 class="message-detail__title"><?php echo esc_html((string) ($item['title'] ?? '')); ?></h2>
|
||||
<p class="message-detail__meta"><?php echo esc_html(aksrt_notification_type_label($type)); ?> · <?php echo esc_html(aksrt_message_time($item)); ?></p>
|
||||
</header>
|
||||
<div class="message-detail__content"><?php echo wp_kses_post(wpautop(esc_html((string) ($item['desc'] ?? '')))); ?></div>
|
||||
<footer class="message-detail__actions">
|
||||
<?php if ($target_url !== '') : ?><a href="<?php echo esc_url($target_url); ?>" class="message-detail__primary"><?php esc_html_e('查看相关内容', 'aksrt'); ?></a><?php endif; ?>
|
||||
<button type="button" class="message-item__action-btn message-item__action-btn--danger js-msg-delete" data-id="<?php echo esc_attr((string) ($item['id'] ?? '')); ?>" data-detail="1"><?php esc_html_e('删除消息', 'aksrt'); ?></button>
|
||||
</footer>
|
||||
</article>
|
||||
<?php
|
||||
}
|
||||
|
||||
add_action('wp_ajax_aksrt_load_message_center', function (): void {
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401);
|
||||
}
|
||||
$nonce = isset($_POST['nonce']) ? (string) wp_unslash($_POST['nonce']) : '';
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) {
|
||||
wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403);
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$view = aksrt_normalize_message_view((string) ($_POST['view'] ?? 'unread'));
|
||||
$page = max(1, absint($_POST['page'] ?? 1));
|
||||
$notification_id = absint($_POST['notification_id'] ?? 0);
|
||||
$detail = null;
|
||||
if ($notification_id > 0) {
|
||||
$detail = aksrt_get_message_center_item($user_id, $notification_id);
|
||||
if (!$detail) {
|
||||
wp_send_json_error(['message' => __('消息不存在或已删除', 'aksrt')], 404);
|
||||
}
|
||||
aksrt_mark_user_notification_read($user_id, (string) $notification_id);
|
||||
$detail['read'] = 1;
|
||||
}
|
||||
|
||||
$counts = aksrt_get_message_center_counts($user_id);
|
||||
ob_start();
|
||||
aksrt_render_message_center_nav($view, $counts);
|
||||
$nav_html = (string) ob_get_clean();
|
||||
ob_start();
|
||||
if ($detail) {
|
||||
aksrt_render_message_center_detail($detail, $view);
|
||||
} else {
|
||||
aksrt_render_message_center_list($user_id, $view, $page);
|
||||
}
|
||||
$content_html = (string) ob_get_clean();
|
||||
|
||||
wp_send_json_success([
|
||||
'view' => $view,
|
||||
'page' => $page,
|
||||
'detail' => $notification_id,
|
||||
'nav' => $nav_html,
|
||||
'content' => $content_html,
|
||||
'unread' => $counts['unread'],
|
||||
]);
|
||||
});
|
||||
@@ -56,11 +56,12 @@ function aksrt_notification_type_label(string $type): string {
|
||||
$map = [
|
||||
'comment_reply' => __('评论回复', 'aksrt'),
|
||||
'comment_approved' => __('评论审核', 'aksrt'),
|
||||
'comment' => __('文章评论', 'aksrt'),
|
||||
'download_ready' => __('下载就绪', 'aksrt'),
|
||||
'asset_points_increase' => __('积分增加', 'aksrt'),
|
||||
'asset_points_decrease' => __('积分扣减', 'aksrt'),
|
||||
'asset_balance_increase' => __('余额增加', 'aksrt'),
|
||||
'asset_balance_decrease' => __('余额扣减', 'aksrt'),
|
||||
'post_favorite' => __('文章收藏', 'aksrt'),
|
||||
'post_like' => __('文章点赞', 'aksrt'),
|
||||
'comment_like' => __('评论点赞', 'aksrt'),
|
||||
'followed' => __('新增关注', 'aksrt'),
|
||||
'pending_comments' => __('待审核评论', 'aksrt'),
|
||||
'notice' => __('通知', 'aksrt'),
|
||||
];
|
||||
@@ -88,38 +89,18 @@ function aksrt_get_user_inbox_notifications(int $user_id = 0): array {
|
||||
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
$items = get_user_meta($user_id, 'aksrt_notifications', true);
|
||||
return is_array($items) ? array_values($items) : [];
|
||||
}
|
||||
|
||||
static $migrated = [];
|
||||
if (empty($migrated[$user_id])) {
|
||||
$legacy = get_user_meta($user_id, 'aksrt_notifications', true);
|
||||
if (is_array($legacy) && !empty($legacy)) {
|
||||
foreach ($legacy as $item) {
|
||||
aksrt_add_user_notification($user_id, [
|
||||
'id' => $item['id'] ?? '',
|
||||
'type' => $item['type'] ?? 'notice',
|
||||
'title' => $item['title'] ?? '',
|
||||
'desc' => $item['desc'] ?? '',
|
||||
'url' => $item['url'] ?? '',
|
||||
'read' => !empty($item['read']),
|
||||
'created_at' => $item['created_at'] ?? current_time('mysql', true),
|
||||
]);
|
||||
}
|
||||
delete_user_meta($user_id, 'aksrt_notifications');
|
||||
}
|
||||
$migrated[$user_id] = true;
|
||||
}
|
||||
if ($table === '') return [];
|
||||
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_notification_settings() : [];
|
||||
$limit = max(20, min(5000, (int) ($settings['max_per_user'] ?? 200)));
|
||||
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT id, type, title, description, target_url, is_read, created_at, read_at
|
||||
FROM {$table}
|
||||
WHERE user_id = %d
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 50",
|
||||
$user_id
|
||||
LIMIT %d",
|
||||
$user_id,
|
||||
$limit
|
||||
), ARRAY_A);
|
||||
|
||||
if (!is_array($rows)) return [];
|
||||
@@ -220,34 +201,6 @@ endif;
|
||||
add_action('wp', 'aksrt_schedule_notification_cleanup');
|
||||
add_action('aksrt_notification_cleanup', 'aksrt_cleanup_notifications');
|
||||
|
||||
if (!function_exists('aksrt_write_user_inbox_notifications')) :
|
||||
/** 写入用户通知收件箱 */
|
||||
function aksrt_write_user_inbox_notifications(int $user_id, array $items): void {
|
||||
if ($user_id <= 0) return;
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
update_user_meta($user_id, 'aksrt_notifications', array_values(array_slice($items, 0, 50)));
|
||||
return;
|
||||
}
|
||||
|
||||
$wpdb->delete($table, ['user_id' => $user_id], ['%d']);
|
||||
$items = array_values(array_slice($items, 0, 50));
|
||||
foreach ($items as $item) {
|
||||
$wpdb->insert($table, [
|
||||
'user_id' => $user_id,
|
||||
'type' => sanitize_key((string) ($item['type'] ?? 'notice')),
|
||||
'title' => sanitize_text_field((string) ($item['title'] ?? '')),
|
||||
'description' => sanitize_text_field((string) ($item['desc'] ?? '')),
|
||||
'target_url' => esc_url_raw((string) ($item['url'] ?? '')),
|
||||
'is_read' => !empty($item['read']) ? 1 : 0,
|
||||
'created_at' => (string) ($item['created_at'] ?? current_time('mysql', true)),
|
||||
'read_at' => !empty($item['read']) ? current_time('mysql', true) : null,
|
||||
], ['%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s']);
|
||||
}
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_add_user_notification')) :
|
||||
/** 添加一条用户通知 */
|
||||
function aksrt_add_user_notification(int $user_id, array $payload): void {
|
||||
@@ -264,20 +217,7 @@ function aksrt_add_user_notification(int $user_id, array $payload): void {
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
$created_at = (string) ($payload['created_at'] ?? current_time('mysql', true));
|
||||
|
||||
if ($table === '') {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
array_unshift($items, [
|
||||
'id' => isset($payload['id']) && $payload['id'] !== '' ? sanitize_text_field((string) $payload['id']) : uniqid('notif_', true),
|
||||
'type' => $type,
|
||||
'title' => sanitize_text_field((string) ($payload['title'] ?? '')),
|
||||
'desc' => sanitize_text_field((string) ($payload['desc'] ?? '')),
|
||||
'url' => esc_url_raw((string) ($payload['url'] ?? '')),
|
||||
'read' => !empty($payload['read']) ? 1 : 0,
|
||||
'created_at' => $created_at,
|
||||
]);
|
||||
aksrt_write_user_inbox_notifications($user_id, $items);
|
||||
return;
|
||||
}
|
||||
if ($table === '') return;
|
||||
|
||||
$wpdb->insert($table, [
|
||||
'user_id' => $user_id,
|
||||
@@ -298,17 +238,7 @@ function aksrt_mark_user_notification_read(int $user_id, string $notification_id
|
||||
if ($user_id <= 0) return;
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
foreach ($items as &$item) {
|
||||
if ($mark_all || (($item['id'] ?? '') === $notification_id)) {
|
||||
$item['read'] = 1;
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
aksrt_write_user_inbox_notifications($user_id, $items);
|
||||
return;
|
||||
}
|
||||
if ($table === '') return;
|
||||
|
||||
if ($mark_all) {
|
||||
$wpdb->update(
|
||||
@@ -337,14 +267,7 @@ function aksrt_delete_user_notification(int $user_id, string $notification_id):
|
||||
if ($user_id <= 0 || $notification_id === '') return;
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
$items = array_values(array_filter($items, static function ($item) use ($notification_id) {
|
||||
return (string) ($item['id'] ?? '') !== $notification_id;
|
||||
}));
|
||||
aksrt_write_user_inbox_notifications($user_id, $items);
|
||||
return;
|
||||
}
|
||||
if ($table === '') return;
|
||||
$wpdb->delete($table, ['id' => absint($notification_id), 'user_id' => $user_id], ['%d', '%d']);
|
||||
}
|
||||
endif;
|
||||
@@ -358,14 +281,7 @@ function aksrt_delete_user_notifications(int $user_id, array $notification_ids):
|
||||
|
||||
global $wpdb;
|
||||
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
|
||||
if ($table === '') {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
$items = array_values(array_filter($items, static function ($item) use ($ids) {
|
||||
return !in_array(absint($item['id'] ?? 0), $ids, true);
|
||||
}));
|
||||
aksrt_write_user_inbox_notifications($user_id, $items);
|
||||
return;
|
||||
}
|
||||
if ($table === '') return;
|
||||
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$table} WHERE user_id = " . (int) $user_id . " AND id IN (" . implode(',', $ids) . ")"
|
||||
@@ -379,7 +295,7 @@ function aksrt_get_user_unread_notification_count(int $user_id = 0): int {
|
||||
$items = aksrt_get_user_inbox_notifications($user_id);
|
||||
$count = 0;
|
||||
foreach ($items as $item) {
|
||||
if (empty($item['read'])) $count++;
|
||||
if (empty($item['read']) && !str_starts_with((string) ($item['type'] ?? ''), 'asset_')) $count++;
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
@@ -498,16 +414,33 @@ add_action('wp_ajax_aksrt_check_new_notifications', function () {
|
||||
wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403);
|
||||
}
|
||||
|
||||
$unread = aksrt_get_user_unread_notification_count(get_current_user_id());
|
||||
$counts = function_exists('aksrt_get_message_center_counts') ? aksrt_get_message_center_counts(get_current_user_id()) : [];
|
||||
$unread = isset($counts['unread']) ? (int) $counts['unread'] : aksrt_get_user_unread_notification_count(get_current_user_id());
|
||||
wp_send_json_success([
|
||||
'count' => $unread,
|
||||
'unread' => $unread,
|
||||
]);
|
||||
});
|
||||
|
||||
function aksrt_notify_post_author_of_comment(WP_Comment $comment): void {
|
||||
if ((int) $comment->comment_parent > 0 || get_comment_meta($comment->comment_ID, 'aksrt_post_comment_notified', true)) return;
|
||||
$post = get_post((int) $comment->comment_post_ID);
|
||||
$owner_id = $post instanceof WP_Post ? (int) $post->post_author : 0;
|
||||
if ($owner_id <= 0 || $owner_id === (int) $comment->user_id) return;
|
||||
aksrt_add_user_notification($owner_id, [
|
||||
'type' => 'comment',
|
||||
'title' => __('文章收到评论', 'aksrt'),
|
||||
'desc' => sprintf(__('%1$s 评论了你的文章《%2$s》。', 'aksrt'), get_comment_author($comment), get_the_title($post)),
|
||||
'url' => get_comment_link($comment),
|
||||
]);
|
||||
update_comment_meta($comment->comment_ID, 'aksrt_post_comment_notified', 1);
|
||||
}
|
||||
|
||||
add_action('wp_insert_comment', function ($comment_id, $comment) {
|
||||
if (!$comment instanceof WP_Comment) return;
|
||||
if ((int) $comment->comment_parent <= 0) return;
|
||||
if ((int) $comment->comment_approved !== 1) return;
|
||||
aksrt_notify_post_author_of_comment($comment);
|
||||
if ((int) $comment->comment_parent <= 0) return;
|
||||
if (get_comment_meta($comment_id, 'aksrt_reply_notified', true)) return;
|
||||
|
||||
$parent = get_comment((int) $comment->comment_parent);
|
||||
@@ -540,6 +473,7 @@ add_action('transition_comment_status', function ($new_status, $old_status, $com
|
||||
]);
|
||||
update_comment_meta($comment->comment_ID, 'aksrt_approved_notified', 1);
|
||||
}
|
||||
aksrt_notify_post_author_of_comment($comment);
|
||||
|
||||
if ((int) $comment->comment_parent <= 0) return;
|
||||
if (get_comment_meta($comment->comment_ID, 'aksrt_reply_notified', true)) return;
|
||||
@@ -558,38 +492,55 @@ add_action('transition_comment_status', function ($new_status, $old_status, $com
|
||||
update_comment_meta($comment->comment_ID, 'aksrt_reply_notified', 1);
|
||||
}, 20, 3);
|
||||
|
||||
add_action('aksrt_asset_transaction_committed', function ($result, $command): void {
|
||||
if (!$result instanceof \AKSRT\Ledger\LedgerResult || !$command instanceof \AKSRT\Ledger\LedgerCommand) {
|
||||
return;
|
||||
}
|
||||
$changes = [];
|
||||
foreach ($result->entries as $entry) {
|
||||
$user_id = (int) ($entry['user_id'] ?? 0);
|
||||
$asset = (string) ($entry['asset'] ?? '');
|
||||
if ($user_id <= 0 || !in_array($asset, ['points', 'balance'], true)) continue;
|
||||
$key = $user_id . ':' . $asset;
|
||||
$changes[$key] ??= ['user_id' => $user_id, 'asset' => $asset, 'delta' => 0, 'balance' => 0];
|
||||
$changes[$key]['delta'] += (int) ($entry['delta_minor'] ?? 0);
|
||||
$changes[$key]['balance'] = (int) ($entry['balance_after_minor'] ?? 0);
|
||||
}
|
||||
foreach ($changes as $change) {
|
||||
if ((int) $change['delta'] === 0) continue;
|
||||
$is_points = $change['asset'] === 'points';
|
||||
$positive = $change['delta'] > 0;
|
||||
$amount = $is_points
|
||||
? (string) abs((int) $change['delta'])
|
||||
: \AKSRT\Ledger\AssetAmount::from_minor(\AKSRT\Ledger\AssetType::BALANCE, abs((int) $change['delta']))->decimal();
|
||||
$balance = $is_points
|
||||
? (string) $change['balance']
|
||||
: \AKSRT\Ledger\AssetAmount::from_minor(\AKSRT\Ledger\AssetType::BALANCE, (int) $change['balance'])->decimal();
|
||||
aksrt_add_user_notification((int) $change['user_id'], [
|
||||
'type' => 'asset_' . $change['asset'] . '_' . ($positive ? 'increase' : 'decrease'),
|
||||
'title' => $is_points ? __('积分变动', 'aksrt') : __('余额变动', 'aksrt'),
|
||||
'desc' => sprintf(__('%1$s:%2$s%3$s,当前余额 %4$s。', 'aksrt'), \AKSRT\Ledger\LedgerPresentation::reason_label($command->reason), $positive ? '+' : '-', $amount, $balance),
|
||||
'url' => aksrt_get_user_center_url('credits'),
|
||||
]);
|
||||
}
|
||||
}, 10, 2);
|
||||
add_action('aksrt_post_favorited', function (int $post_id, int $actor_id, bool $active): void {
|
||||
$post = $active ? get_post($post_id) : null;
|
||||
$owner_id = $post instanceof WP_Post ? (int) $post->post_author : 0;
|
||||
if ($owner_id <= 0 || $owner_id === $actor_id) return;
|
||||
$actor = get_userdata($actor_id);
|
||||
aksrt_add_user_notification($owner_id, [
|
||||
'type' => 'post_favorite',
|
||||
'title' => __('文章被收藏', 'aksrt'),
|
||||
'desc' => sprintf(__('%1$s 收藏了你的文章《%2$s》。', 'aksrt'), $actor instanceof WP_User ? $actor->display_name : __('一位用户', 'aksrt'), get_the_title($post)),
|
||||
'url' => get_permalink($post),
|
||||
]);
|
||||
}, 30, 3);
|
||||
|
||||
add_action('aksrt_post_liked', function (int $post_id, int $actor_id, bool $active): void {
|
||||
$post = $active ? get_post($post_id) : null;
|
||||
$owner_id = $post instanceof WP_Post ? (int) $post->post_author : 0;
|
||||
if ($owner_id <= 0 || $owner_id === $actor_id) return;
|
||||
$actor = get_userdata($actor_id);
|
||||
aksrt_add_user_notification($owner_id, [
|
||||
'type' => 'post_like',
|
||||
'title' => __('文章收到点赞', 'aksrt'),
|
||||
'desc' => sprintf(__('%1$s 点赞了你的文章《%2$s》。', 'aksrt'), $actor instanceof WP_User ? $actor->display_name : __('一位用户', 'aksrt'), get_the_title($post)),
|
||||
'url' => get_permalink($post),
|
||||
]);
|
||||
}, 30, 3);
|
||||
|
||||
add_action('aksrt_comment_liked', function (int $comment_id, int $actor_id, bool $active): void {
|
||||
$comment = $active ? get_comment($comment_id) : null;
|
||||
$owner_id = $comment instanceof WP_Comment ? (int) $comment->user_id : 0;
|
||||
if ($owner_id <= 0 || $owner_id === $actor_id) return;
|
||||
$actor = get_userdata($actor_id);
|
||||
aksrt_add_user_notification($owner_id, [
|
||||
'type' => 'comment_like',
|
||||
'title' => __('评论收到点赞', 'aksrt'),
|
||||
'desc' => sprintf(__('%s 点赞了你的评论。', 'aksrt'), $actor instanceof WP_User ? $actor->display_name : __('一位用户', 'aksrt')),
|
||||
'url' => get_comment_link($comment),
|
||||
]);
|
||||
}, 30, 3);
|
||||
|
||||
add_action('aksrt_user_followed', function (int $follower_id, int $following_id): void {
|
||||
if ($following_id <= 0 || $following_id === $follower_id) return;
|
||||
$follower = get_userdata($follower_id);
|
||||
aksrt_add_user_notification($following_id, [
|
||||
'type' => 'followed',
|
||||
'title' => __('收到新的关注', 'aksrt'),
|
||||
'desc' => sprintf(__('%s 关注了你。', 'aksrt'), $follower instanceof WP_User ? $follower->display_name : __('一位用户', 'aksrt')),
|
||||
'url' => function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($follower_id) : '',
|
||||
]);
|
||||
}, 30, 2);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ add_filter('query_vars', function ($vars) {
|
||||
$vars[] = 'aksrt_user_center';
|
||||
$vars[] = 'aksrt_user_center_tab';
|
||||
$vars[] = 'aksrt_author_identifier';
|
||||
$vars[] = 'aksrt_message_view';
|
||||
return $vars;
|
||||
});
|
||||
|
||||
@@ -31,6 +32,13 @@ add_action('template_redirect', function () {
|
||||
nocache_headers();
|
||||
return;
|
||||
}
|
||||
if (!get_query_var('aksrt_message_view') && is_page() && get_page_template_slug(get_queried_object_id()) === 'templates/pages/page-messages.php') {
|
||||
global $wp_query;
|
||||
if ($wp_query instanceof WP_Query) { $wp_query->set_404(); }
|
||||
status_header(404);
|
||||
nocache_headers();
|
||||
return;
|
||||
}
|
||||
|
||||
$resolved_path = aksrt_get_current_special_template_path();
|
||||
if ($resolved_path === '') {
|
||||
@@ -68,6 +76,9 @@ add_action('init', function () {
|
||||
add_rewrite_tag('%aksrt_user_center%', '([^&]+)');
|
||||
add_rewrite_tag('%aksrt_user_center_tab%', '([^/]+)');
|
||||
add_rewrite_tag('%aksrt_author_identifier%', '([^/]+)');
|
||||
add_rewrite_tag('%aksrt_message_view%', '(unread|posts|like|system)');
|
||||
add_rewrite_rule('^message/?$', 'index.php?aksrt_message_view=unread', 'top');
|
||||
add_rewrite_rule('^message/(posts|like|system)/?$', 'index.php?aksrt_message_view=$matches[1]', 'top');
|
||||
add_rewrite_rule('^user/([a-z0-9_-]+)/?$', 'index.php?aksrt_user_center=1&aksrt_user_center_tab=$matches[1]', 'top');
|
||||
add_rewrite_rule('^author/([^/]+)/?$', 'index.php?aksrt_author_identifier=$matches[1]', 'top');
|
||||
});
|
||||
@@ -177,7 +188,7 @@ add_action('init', function () {
|
||||
}
|
||||
|
||||
// 统一 flush 检查 — bump when user-home rewrite / author override changes
|
||||
$rewrite_version = AKSRT_VERSION . ':user-center-author-routing-v6';
|
||||
$rewrite_version = AKSRT_VERSION . ':message-routing-v1';
|
||||
$flushed = get_option('aksrt_user_rules_flushed');
|
||||
$rules = get_option('rewrite_rules');
|
||||
$need_flush = ($flushed !== $rewrite_version)
|
||||
|
||||
@@ -37,12 +37,6 @@ $_aksrt_special_templates = [
|
||||
'title' => '个人中心',
|
||||
'fallback' => '/profile/',
|
||||
],
|
||||
'messages' => [
|
||||
'file' => 'templates/pages/page-messages.php',
|
||||
'slug' => 'messages',
|
||||
'title' => '消息中心',
|
||||
'fallback' => '/messages/',
|
||||
],
|
||||
'submit' => [
|
||||
'file' => 'templates/pages/page-submit.php',
|
||||
'slug' => 'submit',
|
||||
@@ -135,7 +129,10 @@ function aksrt_resolve_page_template_file(string $template_file = '', string $po
|
||||
$post_slug = sanitize_title($post_slug);
|
||||
|
||||
if ($template_file !== '') {
|
||||
if ($template_file === 'templates/pages/page-sign.php') {
|
||||
if ($template_file === 'templates/pages/page-messages.php') {
|
||||
return '';
|
||||
}
|
||||
if ($template_file === 'templates/pages/page-sign.php') {
|
||||
return $template_file;
|
||||
}
|
||||
|
||||
@@ -223,6 +220,11 @@ function aksrt_get_user_home_template_path(): string {
|
||||
* Resolve the active special-page template path for the current request.
|
||||
*/
|
||||
function aksrt_get_current_special_template_path(): string {
|
||||
if (get_query_var('aksrt_message_view')) {
|
||||
$message_template = aksrt_locate_template_file('templates/pages/page-messages.php');
|
||||
return $message_template ?: '';
|
||||
}
|
||||
|
||||
if (get_query_var('aksrt_author_identifier')) {
|
||||
return aksrt_get_user_home_template_path();
|
||||
}
|
||||
|
||||
@@ -55,16 +55,19 @@ function aksrt_get_new_post_url(int $edit_id = 0): string {
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_messages_page_url')) :
|
||||
/**
|
||||
* 消息中心页面 URL(模板 templates/pages/page-messages.php)。
|
||||
*/
|
||||
function aksrt_get_messages_page_url(): string {
|
||||
/** 消息中心 URL。 */
|
||||
function aksrt_get_messages_page_url(string $view = 'unread'): string {
|
||||
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::message_enabled()) {
|
||||
return '';
|
||||
}
|
||||
return function_exists('aksrt_get_template_url')
|
||||
? aksrt_get_template_url('messages')
|
||||
: home_url('/messages/');
|
||||
$paths = [
|
||||
'unread' => '/message/',
|
||||
'posts' => '/message/posts/',
|
||||
'like' => '/message/like/',
|
||||
'system' => '/message/system/',
|
||||
];
|
||||
$view = sanitize_key($view);
|
||||
return home_url($paths[$view] ?? $paths['unread']);
|
||||
}
|
||||
endif;
|
||||
|
||||
|
||||
@@ -568,7 +568,7 @@ trait ScriptsPart1b {
|
||||
}
|
||||
|
||||
// 消息中心页
|
||||
if (is_page_template('templates/pages/page-messages.php')) {
|
||||
if (get_query_var('aksrt_message_view')) {
|
||||
$messages_css = get_template_directory() . '/assets/css/frontend/messages.css';
|
||||
if (file_exists($messages_css)) {
|
||||
wp_enqueue_style(
|
||||
|
||||
@@ -46,7 +46,6 @@ class Templates {
|
||||
'templates/pages/page-posts-navs.php' => __('AKSRT - 文章导航', 'aksrt'),
|
||||
'templates/pages/page-friends.php' => __('AKSRT - 友情链接', 'aksrt'),
|
||||
'templates/pages/page-ban-appeal.php' => __('AKSRT - 禁封申诉', 'aksrt'),
|
||||
'templates/pages/page-messages.php' => __('AKSRT - 消息中心', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+17
-71
@@ -16,33 +16,21 @@ class Message {
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('wp_ajax_aksrt_mark_message_read', [$this, 'ajax_mark_read']);
|
||||
add_action('wp_ajax_aksrt_get_unread_count', [$this, 'ajax_get_unread_count']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户未读消息数
|
||||
*/
|
||||
public function get_unread_count(int $user_id): int {
|
||||
if ($user_id <= 0) return 0;
|
||||
|
||||
// 优先从表中实时统计
|
||||
$table = $this->table();
|
||||
if ($table !== '') {
|
||||
global $wpdb;
|
||||
$count = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND is_read = 0",
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND is_read = 0 AND type NOT LIKE 'asset\\_%'",
|
||||
$user_id
|
||||
));
|
||||
// 同步到 user_meta 以兼容旧读取方
|
||||
update_user_meta($user_id, 'aksrt_unread_message_count', $count);
|
||||
return $count;
|
||||
}
|
||||
|
||||
// 降级:从 user_meta 读取
|
||||
$count = (int) get_user_meta($user_id, 'aksrt_unread_message_count', true);
|
||||
return max(0, $count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,12 +52,7 @@ class Message {
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
// 降级:仅更新计数
|
||||
$current = $this->get_unread_count($user_id);
|
||||
if ($current > 0) {
|
||||
update_user_meta($user_id, 'aksrt_unread_message_count', max(0, $current - 1));
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,8 +73,7 @@ class Message {
|
||||
);
|
||||
}
|
||||
|
||||
update_user_meta($user_id, 'aksrt_unread_message_count', 0);
|
||||
return true;
|
||||
return $table !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,22 +89,18 @@ class Message {
|
||||
if ($to_user_id <= 0 || empty($content)) return false;
|
||||
|
||||
$table = $this->table();
|
||||
if ($table !== '') {
|
||||
global $wpdb;
|
||||
$wpdb->insert($table, [
|
||||
'user_id' => $to_user_id,
|
||||
'type' => sanitize_key($type),
|
||||
'title' => sanitize_text_field((string) ($data['title'] ?? $content)),
|
||||
'description' => sanitize_textarea_field($content),
|
||||
'target_url' => esc_url_raw((string) ($data['target_url'] ?? '')),
|
||||
'is_read' => 0,
|
||||
'created_at' => current_time('mysql'),
|
||||
], ['%d', '%s', '%s', '%s', '%s', '%d', '%s']);
|
||||
}
|
||||
|
||||
// 同步更新 user_meta 未读计数
|
||||
$current = (int) get_user_meta($to_user_id, 'aksrt_unread_message_count', true);
|
||||
update_user_meta($to_user_id, 'aksrt_unread_message_count', max(0, $current) + 1);
|
||||
if ($table === '') return false;
|
||||
global $wpdb;
|
||||
$inserted = $wpdb->insert($table, [
|
||||
'user_id' => $to_user_id,
|
||||
'type' => sanitize_key($type),
|
||||
'title' => sanitize_text_field((string) ($data['title'] ?? $content)),
|
||||
'description' => sanitize_textarea_field($content),
|
||||
'target_url' => esc_url_raw((string) ($data['target_url'] ?? '')),
|
||||
'is_read' => 0,
|
||||
'created_at' => current_time('mysql'),
|
||||
], ['%d', '%s', '%s', '%s', '%s', '%d', '%s']);
|
||||
if ($inserted === false) return false;
|
||||
|
||||
// 触发通知钩子
|
||||
do_action('aksrt_notification_sent', $to_user_id, $type, $data);
|
||||
@@ -181,36 +159,4 @@ class Message {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX 标记已读
|
||||
*/
|
||||
public function ajax_mark_read(): void {
|
||||
check_ajax_referer('aksrt_message', '_nonce');
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
|
||||
}
|
||||
|
||||
$message_id = sanitize_text_field((string) ($_POST['message_id'] ?? ''));
|
||||
if ($this->mark_read($user_id, $message_id)) {
|
||||
wp_send_json_success(['unread' => $this->get_unread_count($user_id)]);
|
||||
}
|
||||
|
||||
wp_send_json_error(['message' => __('操作失败', 'aksrt')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX 获取未读数
|
||||
*/
|
||||
public function ajax_get_unread_count(): void {
|
||||
check_ajax_referer('aksrt_message', '_nonce');
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
|
||||
}
|
||||
|
||||
wp_send_json_success(['unread' => $this->get_unread_count($user_id)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,10 +286,6 @@ class Integration {
|
||||
'comment_reply' => true,
|
||||
'comment_approved' => true,
|
||||
'download_ready' => true,
|
||||
'asset_points_increase' => true,
|
||||
'asset_points_decrease' => true,
|
||||
'asset_balance_increase' => true,
|
||||
'asset_balance_decrease' => true,
|
||||
'pending_comments' => true,
|
||||
'upload_moderation' => true,
|
||||
],
|
||||
|
||||
@@ -69,21 +69,20 @@ final class NotificationPreferences {
|
||||
public static function category_for_type(string $notification_type): string {
|
||||
$type = sanitize_key($notification_type);
|
||||
if (
|
||||
in_array($type, ['posts', 'post', 'comment', 'favorite', 'pending_comments'], true)
|
||||
|| str_starts_with($type, 'post_')
|
||||
|| str_starts_with($type, 'comment_')
|
||||
|| str_starts_with($type, 'favorite_')
|
||||
) {
|
||||
return 'posts';
|
||||
}
|
||||
if (
|
||||
in_array($type, ['like', 'follow', 'followed', 'hot'], true)
|
||||
in_array($type, ['like', 'post_like', 'comment_like', 'follow', 'followed', 'hot'], true)
|
||||
|| str_starts_with($type, 'like_')
|
||||
|| str_starts_with($type, 'follow_')
|
||||
|| str_starts_with($type, 'followed_')
|
||||
) {
|
||||
return 'like';
|
||||
}
|
||||
if (
|
||||
in_array($type, ['posts', 'post', 'comment', 'favorite', 'post_favorite', 'pending_comments'], true)
|
||||
|| str_starts_with($type, 'comment_')
|
||||
|| str_starts_with($type, 'favorite_')
|
||||
) {
|
||||
return 'posts';
|
||||
}
|
||||
return 'system';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,275 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Name: 消息中心
|
||||
* Template Post Type: page
|
||||
*
|
||||
* 独立消息中心页面 — 两栏布局(左侧导航 + 右侧消息列表)
|
||||
* 展示未读、评论、资产、交易与系统通知。
|
||||
* Routed message center shell.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
get_header();
|
||||
\AKSRT\WidgetSystem\WidgetAreas::render('msg_top_fluid', 'fluid-widget-wrap');
|
||||
|
||||
if (!is_user_logged_in()) {
|
||||
$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(get_permalink());
|
||||
wp_safe_redirect($login_url);
|
||||
wp_safe_redirect(function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(aksrt_get_messages_page_url()));
|
||||
exit;
|
||||
}
|
||||
|
||||
get_header();
|
||||
\AKSRT\WidgetSystem\WidgetAreas::render('msg_top_fluid', 'fluid-widget-wrap');
|
||||
|
||||
$messages_enabled = !class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::message_enabled();
|
||||
|
||||
if (!$messages_enabled) :
|
||||
?>
|
||||
<main id="main-content" class="site-main content-shell" tabindex="-1">
|
||||
<div class="container messages-center messages-center--disabled">
|
||||
<div class="messages-empty">
|
||||
<span class="messages-empty__icon"><i class="fa fas fa-bell-slash"></i></span>
|
||||
<p class="messages-empty__text"><?php esc_html_e('站内消息功能未开启', 'aksrt'); ?></p>
|
||||
<p class="messages-empty__hint"><?php esc_html_e('请联系管理员在后台「用户 → 消息通知」中启用。', 'aksrt'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<?php
|
||||
\AKSRT\WidgetSystem\WidgetAreas::render('msg_bottom_fluid', 'fluid-widget-wrap');
|
||||
get_footer();
|
||||
return;
|
||||
endif;
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
$all_notifications = function_exists('aksrt_get_user_all_notifications')
|
||||
? aksrt_get_user_all_notifications($user_id)
|
||||
: [];
|
||||
|
||||
$current_type = isset($_GET['msg_type']) ? sanitize_key(wp_unslash((string) $_GET['msg_type'])) : 'unread';
|
||||
|
||||
// 通知类型分组。
|
||||
$type_groups = [
|
||||
'unread' => null, // special: !read
|
||||
'all' => null, // special: all
|
||||
'comment' => ['comment_reply', 'comment_approved', 'pending_comments'],
|
||||
'credits' => ['asset_points_increase', 'asset_points_decrease', 'asset_balance_increase', 'asset_balance_decrease'],
|
||||
'pay' => ['pay', 'order_paid', 'order_refund', 'shop_order', 'after_sale'],
|
||||
'vip' => ['vip', 'pay_vip', 'vip_expire', 'vip_upgrade'],
|
||||
'medal' => ['medal', 'badge', 'level_up'],
|
||||
'ban_appeal' => ['ban_appeal', 'user_ban', 'restriction'],
|
||||
'system' => ['notice', 'download_ready', 'upload_moderation', 'verify_email', 'auth_apply', 'auth_apply_reply'],
|
||||
];
|
||||
|
||||
$allowed_types = array_keys($type_groups);
|
||||
if (!in_array($current_type, $allowed_types, true)) {
|
||||
$mapped = 'all';
|
||||
foreach ($type_groups as $group => $types) {
|
||||
if (is_array($types) && in_array($current_type, $types, true)) {
|
||||
$mapped = $group;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$current_type = $mapped;
|
||||
}
|
||||
|
||||
$filtered = $all_notifications;
|
||||
if ($current_type === 'unread') {
|
||||
$filtered = array_filter($filtered, static fn($n) => empty($n['read']));
|
||||
} elseif ($current_type !== 'all') {
|
||||
$types = $type_groups[$current_type] ?? [];
|
||||
$filtered = array_filter($filtered, static function ($n) use ($types) {
|
||||
$t = (string) ($n['type'] ?? 'notice');
|
||||
return in_array($t, $types, true);
|
||||
});
|
||||
}
|
||||
$filtered = array_values($filtered);
|
||||
|
||||
$per_page = 20;
|
||||
$current_page = max(1, (int) ($_GET['msg_page'] ?? 1));
|
||||
$total = count($filtered);
|
||||
$total_pages = max(1, (int) ceil($total / $per_page));
|
||||
$offset = ($current_page - 1) * $per_page;
|
||||
$paged_items = array_slice($filtered, $offset, $per_page);
|
||||
|
||||
// 统计
|
||||
$type_counts = [
|
||||
'all' => count($all_notifications),
|
||||
'unread' => 0,
|
||||
'comment' => 0,
|
||||
'credits' => 0,
|
||||
'pay' => 0,
|
||||
'vip' => 0,
|
||||
'medal' => 0,
|
||||
'ban_appeal' => 0,
|
||||
'system' => 0,
|
||||
];
|
||||
foreach ($all_notifications as $n) {
|
||||
$t = (string) ($n['type'] ?? 'notice');
|
||||
if (empty($n['read'])) {
|
||||
$type_counts['unread']++;
|
||||
}
|
||||
foreach (['comment', 'credits', 'pay', 'vip', 'medal', 'ban_appeal', 'system'] as $group) {
|
||||
if (!empty($type_groups[$group]) && is_array($type_groups[$group]) && in_array($t, $type_groups[$group], true)) {
|
||||
$type_counts[$group]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$nav_items = [
|
||||
'unread' => __('未读消息', 'aksrt'),
|
||||
'all' => __('全部消息', 'aksrt'),
|
||||
'comment' => __('评论互动', 'aksrt'),
|
||||
'credits' => __('资产动态', 'aksrt'),
|
||||
'pay' => __('交易通知', 'aksrt'),
|
||||
'vip' => __('会员动态', 'aksrt'),
|
||||
'medal' => __('徽章等级', 'aksrt'),
|
||||
'ban_appeal' => __('禁封申诉', 'aksrt'),
|
||||
'system' => __('系统通知', 'aksrt'),
|
||||
];
|
||||
|
||||
$icon_map = [
|
||||
'comment_reply' => 'fa fas fa-comment-dots',
|
||||
'comment_approved' => 'fa fas fa-check-circle',
|
||||
'asset_points_increase' => 'fa fas fa-coins',
|
||||
'asset_points_decrease' => 'fa fas fa-coins',
|
||||
'asset_balance_increase' => 'fa fas fa-wallet',
|
||||
'asset_balance_decrease' => 'fa fas fa-wallet',
|
||||
'download_ready' => 'fa fas fa-download',
|
||||
'upload_moderation' => 'fa fas fa-image',
|
||||
'verify_email' => 'fa fas fa-envelope-open',
|
||||
'pending_comments' => 'fa fas fa-clock',
|
||||
'pay' => 'fa fas fa-file-alt',
|
||||
'order_paid' => 'fa fas fa-file-alt',
|
||||
'order_refund' => 'fa fas fa-undo',
|
||||
'after_sale' => 'fa fas fa-undo',
|
||||
'vip' => 'fa fas fa-certificate',
|
||||
'pay_vip' => 'fa fas fa-certificate',
|
||||
'medal' => 'fa fas fa-certificate',
|
||||
'badge' => 'fa fas fa-certificate',
|
||||
'ban_appeal' => 'fa fas fa-gavel',
|
||||
'user_ban' => 'fa fas fa-ban',
|
||||
'auth_apply' => 'fa fas fa-id-badge',
|
||||
'auth_apply_reply' => 'fa fas fa-id-badge',
|
||||
'notice' => 'fa fas fa-bell',
|
||||
];
|
||||
|
||||
$messages_base = function_exists('aksrt_get_messages_page_url') ? aksrt_get_messages_page_url() : get_permalink();
|
||||
if (!$messages_base) {
|
||||
$messages_base = get_permalink();
|
||||
}
|
||||
$current_view = aksrt_normalize_message_view((string) get_query_var('aksrt_message_view'));
|
||||
?>
|
||||
|
||||
<main id="main-content" class="site-main content-shell" tabindex="-1">
|
||||
<?php if (function_exists('aksrt_breadcrumbs')) { aksrt_breadcrumbs(); } ?>
|
||||
|
||||
<div class="container">
|
||||
<header class="messages-center__header">
|
||||
<h1 class="messages-center__title"><?php esc_html_e('消息中心', 'aksrt'); ?></h1>
|
||||
<p class="messages-center__subtitle"><?php esc_html_e('查看站内通知与互动提醒', 'aksrt'); ?></p>
|
||||
</header>
|
||||
|
||||
<div class="messages-center">
|
||||
<aside class="messages-center__nav" aria-label="<?php esc_attr_e('消息分类', 'aksrt'); ?>">
|
||||
<ul class="messages-nav__list">
|
||||
<?php foreach ($nav_items as $key => $label) :
|
||||
$count = (int) ($type_counts[$key] ?? 0);
|
||||
// 始终显示核心 tab;其余无数据时可隐藏(保留 unread/all/system)
|
||||
if (!in_array($key, ['unread', 'all', 'system'], true) && $count < 1) {
|
||||
continue;
|
||||
}
|
||||
$is_active = $current_type === $key;
|
||||
$tab_url = add_query_arg(['msg_type' => $key, 'msg_page' => 1], $messages_base);
|
||||
?>
|
||||
<li>
|
||||
<a href="<?php echo esc_url($tab_url); ?>"
|
||||
class="messages-nav__item<?php echo $is_active ? ' is-active' : ''; ?>"
|
||||
data-type="<?php echo esc_attr($key); ?>">
|
||||
<span><?php echo esc_html($label); ?></span>
|
||||
<?php if ($count > 0) : ?>
|
||||
<span class="messages-nav__badge" data-type="<?php echo esc_attr($key); ?>" data-count="<?php echo esc_attr((string) $count); ?>"><?php echo esc_html((string) $count); ?></span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<div class="messages-nav__actions">
|
||||
<button type="button" class="messages-nav__action-btn js-msg-mark-all-read"><?php esc_html_e('全部已读', 'aksrt'); ?></button>
|
||||
<button type="button" class="messages-nav__action-btn js-msg-clear-read"><?php esc_html_e('清除已读', 'aksrt'); ?></button>
|
||||
<?php if (!$messages_enabled) : ?>
|
||||
<div class="messages-center messages-center--disabled">
|
||||
<div class="messages-empty">
|
||||
<span class="messages-empty__icon"><i class="fa fas fa-bell-slash"></i></span>
|
||||
<p class="messages-empty__text"><?php esc_html_e('站内消息功能未开启', 'aksrt'); ?></p>
|
||||
<p class="messages-empty__hint"><?php esc_html_e('请联系管理员在后台「用户 → 消息通知」中启用。', 'aksrt'); ?></p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="messages-center__content">
|
||||
<?php if (!empty($paged_items)) : ?>
|
||||
<div class="messages-list">
|
||||
<?php foreach ($paged_items as $notice) :
|
||||
$n_type = (string) ($notice['type'] ?? 'notice');
|
||||
$n_title = (string) ($notice['title'] ?? '');
|
||||
$n_desc = (string) ($notice['desc'] ?? '');
|
||||
$n_url = (string) ($notice['url'] ?? '');
|
||||
$n_id = (string) ($notice['id'] ?? '');
|
||||
$is_read = !empty($notice['read']);
|
||||
$is_state = !empty($notice['stateful']);
|
||||
$created = (string) ($notice['created_at'] ?? '');
|
||||
$time_str = $is_state
|
||||
? __('状态提醒', 'aksrt')
|
||||
: ($created !== '' ? get_date_from_gmt($created, 'Y-m-d H:i') : '');
|
||||
$icon_cls = $icon_map[$n_type] ?? 'fa fas fa-bell';
|
||||
?>
|
||||
<article class="message-item<?php echo !$is_read ? ' is-unread' : ''; ?>" data-notification-id="<?php echo esc_attr($n_id); ?>">
|
||||
<span class="message-item__icon">
|
||||
<i class="<?php echo esc_attr($icon_cls); ?>" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div class="message-item__body">
|
||||
<div class="message-item__title">
|
||||
<?php echo esc_html($n_title); ?>
|
||||
<?php if (!$is_read && !$is_state) : ?>
|
||||
<span class="message-item__new-badge"><?php esc_html_e('新', 'aksrt'); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($n_desc !== '') : ?>
|
||||
<div class="message-item__desc"><?php echo esc_html($n_desc); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($time_str !== '') : ?>
|
||||
<div class="message-item__time"><?php echo esc_html($time_str); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="message-item__actions">
|
||||
<?php if ($n_url !== '') : ?>
|
||||
<a href="<?php echo esc_url($n_url); ?>" class="message-item__action-btn"><?php esc_html_e('查看', 'aksrt'); ?></a>
|
||||
<?php endif; ?>
|
||||
<?php if (!$is_state && !$is_read) : ?>
|
||||
<button type="button" class="message-item__action-btn js-msg-mark-read" data-id="<?php echo esc_attr($n_id); ?>"><?php esc_html_e('已读', 'aksrt'); ?></button>
|
||||
<?php endif; ?>
|
||||
<?php if (!$is_state) : ?>
|
||||
<button type="button" class="message-item__action-btn message-item__action-btn--danger js-msg-delete" data-id="<?php echo esc_attr($n_id); ?>"><?php esc_html_e('删除', 'aksrt'); ?></button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($total_pages > 1) : ?>
|
||||
<nav class="messages-pagination" aria-label="<?php esc_attr_e('消息分页', 'aksrt'); ?>">
|
||||
<?php for ($i = 1; $i <= $total_pages; $i++) :
|
||||
$page_url = add_query_arg(['msg_type' => $current_type, 'msg_page' => $i], $messages_base);
|
||||
?>
|
||||
<?php if ($i === $current_page) : ?>
|
||||
<span class="profile-status-chip is-active"><?php echo esc_html((string) $i); ?></span>
|
||||
<?php else : ?>
|
||||
<a href="<?php echo esc_url($page_url); ?>" class="profile-status-chip"><?php echo esc_html((string) $i); ?></a>
|
||||
<?php endif; ?>
|
||||
<?php endfor; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php else : ?>
|
||||
<div class="messages-empty">
|
||||
<span class="messages-empty__icon"><i class="fa fas fa-bell-slash"></i></span>
|
||||
<p class="messages-empty__text"><?php esc_html_e('暂无消息', 'aksrt'); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="messages-center" data-message-center data-view="<?php echo esc_attr($current_view); ?>">
|
||||
<aside class="messages-center__nav" aria-label="<?php esc_attr_e('消息分类', 'aksrt'); ?>" data-message-nav>
|
||||
<?php aksrt_render_message_center_nav($current_view, aksrt_get_message_center_counts($user_id)); ?>
|
||||
</aside>
|
||||
<section class="messages-center__content" data-message-content aria-live="polite" tabindex="-1">
|
||||
<?php aksrt_render_message_center_list($user_id, $current_view); ?>
|
||||
</section>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
|
||||
test('message center exposes only the four canonical parameter-free routes', () => {
|
||||
const routing = read('inc/modules/user-parts/routing.php');
|
||||
const urls = read('inc/modules/user-parts/urls.php');
|
||||
const registry = read('inc/modules/user-parts/template-registry.php');
|
||||
|
||||
assert.match(routing, /add_rewrite_rule\('\^message\/\?\$',[\s\S]*?aksrt_message_view=unread/);
|
||||
assert.match(routing, /add_rewrite_rule\('\^message\/\(posts\|like\|system\)\/\?\$'/);
|
||||
assert.doesNotMatch(routing, /message\/\(\[0-9]|aksrt_message_id/);
|
||||
for (const route of ['/message/', '/message/posts/', '/message/like/', '/message/system/']) {
|
||||
assert.ok(urls.includes(route), `${route} should be canonical`);
|
||||
}
|
||||
const messageUrlFunction = urls.slice(
|
||||
urls.indexOf('function aksrt_get_messages_page_url'),
|
||||
urls.indexOf('endif;', urls.indexOf('function aksrt_get_messages_page_url')),
|
||||
);
|
||||
assert.doesNotMatch(messageUrlFunction, /add_query_arg|\/messages\//);
|
||||
assert.doesNotMatch(registry, /'messages'\s*=>/);
|
||||
assert.match(registry, /page-messages\.php'\)\s*\{[\s\S]*?return ''/);
|
||||
assert.match(routing, /get_page_template_slug[\s\S]*?page-messages\.php[\s\S]*?set_404/);
|
||||
});
|
||||
|
||||
test('tabs, pagination, actions, and detail all replace DOM fragments', () => {
|
||||
const center = read('inc/modules/message-center.php');
|
||||
const script = read('assets/js/messages.js');
|
||||
const template = read('templates/pages/page-messages.php');
|
||||
|
||||
for (const label of ['未读消息', '文章评论', '点赞喜欢', '系统通知']) {
|
||||
assert.ok(center.includes(label));
|
||||
}
|
||||
assert.match(center, /wp_ajax_aksrt_load_message_center/);
|
||||
assert.match(center, /aksrt_render_message_center_detail/);
|
||||
assert.match(template, /data-message-content/);
|
||||
assert.match(script, /nav\.innerHTML = result\.data\.nav/);
|
||||
assert.match(script, /content\.innerHTML = result\.data\.content/);
|
||||
assert.match(script, /\.js-msg-detail/);
|
||||
assert.match(script, /data-message-back/);
|
||||
assert.doesNotMatch(script, /searchParams|msg_type|msg_page|location\.href\s*=/);
|
||||
assert.doesNotMatch(script, /pushState\([^\n]+detail/);
|
||||
});
|
||||
|
||||
test('asset notifications are absent and interaction notification hooks are present', () => {
|
||||
const notifications = read('inc/modules/user-notifications.php');
|
||||
const settings = read('src/Settings/Integration.php');
|
||||
const adminSettings = read('assets/admin/src/pages/content/NotificationCenterSettings.vue');
|
||||
const messageAdmin = read('assets/admin/src/pages/users/UsersMessages.vue');
|
||||
const combined = [notifications, settings, adminSettings, messageAdmin].join('\n');
|
||||
|
||||
for (const retired of [
|
||||
'asset_points_increase',
|
||||
'asset_points_decrease',
|
||||
'asset_balance_increase',
|
||||
'asset_balance_decrease',
|
||||
]) {
|
||||
assert.equal(combined.includes(retired), false, `${retired} should be absent`);
|
||||
}
|
||||
assert.doesNotMatch(notifications, /add_action\('aksrt_asset_transaction_committed'/);
|
||||
for (const hook of ['aksrt_post_favorited', 'aksrt_post_liked', 'aksrt_comment_liked', 'aksrt_user_followed']) {
|
||||
assert.ok(notifications.includes(`add_action('${hook}'`));
|
||||
}
|
||||
});
|
||||
|
||||
test('notification storage has no message-center legacy metadata fallback', () => {
|
||||
const notifications = read('inc/modules/user-notifications.php');
|
||||
const service = read('src/Messages/Message.php');
|
||||
const combined = `${notifications}\n${service}`;
|
||||
|
||||
assert.doesNotMatch(combined, /aksrt_notifications|aksrt_unread_message_count/);
|
||||
assert.doesNotMatch(combined, /legacy|兼容|降级/i);
|
||||
});
|
||||
Reference in New Issue
Block a user