diff --git a/assets/admin/src/schema/sections/post-list/page.js b/assets/admin/src/schema/sections/post-list/page.js index bfcabd5e..f9b20515 100644 --- a/assets/admin/src/schema/sections/post-list/page.js +++ b/assets/admin/src/schema/sections/post-list/page.js @@ -149,8 +149,12 @@ export const pageSchema = { { key: 'post_copyright_enabled', label: '版权信息', type: 'switch', default: true }, { key: 'post_copyright_text', label: '版权默认文案', type: 'textarea', default: '', help: '留空则自动生成"本文由 XX 原创发布"' }, { key: 'post_button_toptext', label: '按钮上方文案', type: 'input', default: '喜欢就支持一下吧', help: '显示在点赞/打赏/分享按钮上方' }, - { key: 'post_rewards_s', label: '用户打赏', type: 'switch', default: false, help: '开启后用户可配置微信/支付宝收款码;无码则不显示按钮。' }, + { key: 'post_rewards_s', label: '用户打赏', type: 'switch', default: false, help: '开启后,登录用户可使用站内余额赞赏作者,金额直接进入作者余额。' }, { key: 'post_rewards_text', label: '打赏按钮文案', type: 'input', default: '赞赏', dependsOn: { key: 'post_rewards_s', value: true } }, + { key: 'post_rewards_amounts', label: '快捷金额', type: 'input', default: '5,10,20,50,100', help: '使用英文逗号分隔', dependsOn: { key: 'post_rewards_s', value: true } }, + { key: 'post_rewards_custom', label: '允许自定义金额', type: 'switch', default: true, dependsOn: { key: 'post_rewards_s', value: true } }, + { key: 'post_rewards_min', label: '最低金额', type: 'number', default: 1, min: 0.01, max: 1000, step: 0.01, dependsOn: { key: 'post_rewards_s', value: true } }, + { key: 'post_rewards_max', label: '最高金额', type: 'number', default: 1000, min: 1, max: 100000, step: 0.01, dependsOn: { key: 'post_rewards_s', value: true } }, { key: 'article_maxheight_kg', label: '正文限高', type: 'switch', default: false, help: '开启后长文默认折叠,可展开全文' }, { key: 'article_maxheight', label: '限高像素', type: 'number', default: 1000, min: 200, max: 5000, dependsOn: { key: 'article_maxheight_kg', value: true } }, ], diff --git a/assets/css/frontend/enhancements/components/base.css b/assets/css/frontend/enhancements/components/base.css index 9206b0cc..951c839f 100644 --- a/assets/css/frontend/enhancements/components/base.css +++ b/assets/css/frontend/enhancements/components/base.css @@ -197,3 +197,64 @@ background: var(--surface-3, #ECEEF1); } + + +.reward-modal__content > p { + margin: -4px 0 18px; +} + +.reward-amounts { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin: 18px 0; +} + +.reward-amount { + min-height: 42px; + border: 1px solid var(--color-line-soft, rgba(15, 23, 42, 0.1)); + border-radius: 10px; + background: var(--surface-2); + color: var(--color-ink-2); + cursor: pointer; + font-weight: 700; +} + +.reward-amount.is-active { + border-color: var(--color-primary); + background: var(--color-primary-soft); + color: var(--color-primary); +} + +.reward-custom { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; + color: var(--color-ink-3); + font-size: 13px; +} + +.reward-custom input { + width: 140px; + min-height: 40px; + border: 1px solid var(--color-line-soft, rgba(15, 23, 42, 0.1)); + border-radius: 9px; + background: var(--surface-1); + color: var(--color-ink); + padding: 0 12px; +} + +.reward-modal__submit { + width: 100%; + min-height: 42px; +} + +.reward-modal__message { + min-height: 20px; + margin-top: 10px; + color: var(--color-primary); + font-size: 12px; + text-align: center; +} diff --git a/assets/css/routes/user-center/shell.css b/assets/css/routes/user-center/shell.css index 9a8497dc..6d7d2f2f 100644 --- a/assets/css/routes/user-center/shell.css +++ b/assets/css/routes/user-center/shell.css @@ -380,3 +380,261 @@ body.aksrt-user-center .uc-content, padding-top: 18px; border-top: 1px solid var(--color-line-soft, rgba(0,0,0,0.06)); } + + +/* ==================== 侧栏账户仪表盘 ==================== */ +.uc-sidebar__section--stats { + padding: 8px 10px; +} + +.uc-sidebar__heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; + color: var(--color-ink, #172033); + font-size: 14px; + font-weight: 700; +} + +.uc-sidebar__heading > a { + display: inline-flex; + align-items: center; + gap: 5px; + color: var(--color-ink-4); + font-size: 12px; + font-weight: 500; + text-decoration: none; +} + +.uc-sidebar__heading > a:hover { + color: var(--color-primary); +} + +.uc-stats { + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0; +} + +.uc-stats__item { + position: relative; + gap: 2px; + padding: 8px 2px; + background: transparent; +} + +.uc-stats__item + .uc-stats__item::before { + content: ''; + position: absolute; + top: 12px; + bottom: 12px; + left: 0; + width: 1px; + background: var(--color-line-soft, rgba(15, 23, 42, 0.07)); +} + +.uc-stats__item:hover { + background: var(--surface-2); + box-shadow: none; +} + +.uc-stats__value { + color: var(--color-ink, #172033); + font-size: 17px; + letter-spacing: -0.02em; +} + +.uc-stats__label { + margin: 0; + font-size: 11px; +} + +.uc-order-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 4px; +} + +.uc-order-grid__item { + display: flex; + flex-direction: column; + align-items: center; + gap: 7px; + min-width: 0; + padding: 5px 0 2px; + color: var(--color-ink-3); + font-size: 11px; + text-align: center; + text-decoration: none; +} + +.uc-order-grid__item:hover { + color: var(--color-primary); +} + +.uc-order-grid__icon { + position: relative; + display: grid; + place-items: center; + width: 38px; + height: 38px; + border-radius: 13px; + background: color-mix(in srgb, var(--color-primary) 9%, var(--surface-2)); + color: var(--color-primary); + font-size: 16px; + transition: transform 0.2s ease, background-color 0.2s ease; +} + +.uc-order-grid__item:nth-child(2) .uc-order-grid__icon { color: #a56b10; background: rgba(245, 158, 11, 0.12); } +.uc-order-grid__item:nth-child(3) .uc-order-grid__icon { color: #15806c; background: rgba(16, 185, 129, 0.12); } +.uc-order-grid__item:nth-child(4) .uc-order-grid__icon { color: #7c4ed8; background: rgba(139, 92, 246, 0.12); } +.uc-order-grid__item:nth-child(5) .uc-order-grid__icon { color: #d04b65; background: rgba(244, 63, 94, 0.11); } + +.uc-order-grid__item:hover .uc-order-grid__icon { + transform: translateY(-2px); +} + +.uc-order-grid__icon small, +.uc-service-grid__icon small { + position: absolute; + top: -5px; + right: -7px; + display: grid; + place-items: center; + min-width: 17px; + height: 17px; + padding: 0 4px; + border: 2px solid var(--surface-1); + border-radius: 99px; + background: var(--color-error, #ef4444); + color: #fff; + font-size: 9px; + font-style: normal; + font-weight: 700; + line-height: 1; +} + +.uc-sidebar__pair { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.uc-summary-card, +.uc-center-card { + min-width: 0; + border: 1px solid var(--color-line-soft, rgba(15, 23, 42, 0.06)); + border-radius: var(--main-radius, var(--radius, 8px)); + background: var(--surface-1); + box-shadow: 0 1px 4px rgba(15, 23, 42, 0.04); + color: var(--color-ink-2); + text-decoration: none; +} + +.uc-summary-card { + padding: 14px; +} + +.uc-summary-card__head, +.uc-summary-card__body { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.uc-summary-card__head { + margin-bottom: 10px; + color: var(--color-ink-4); + font-size: 12px; +} + +.uc-summary-card__body strong { + overflow: hidden; + font-size: 18px; + font-variant-numeric: tabular-nums; + letter-spacing: -0.02em; + text-overflow: ellipsis; +} + +.uc-summary-card__body > i { + font-size: 18px; +} + +.uc-summary-card--wallet .uc-summary-card__body { color: #3978d6; } +.uc-summary-card--points .uc-summary-card__body { color: #ba7a0a; } + +.uc-center-card { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 9px; + padding: 13px 12px; +} + +.uc-center-card__icon { + display: grid; + place-items: center; + width: 34px; + height: 34px; + border-radius: 12px; + font-size: 15px; +} + +.uc-center-card strong, +.uc-center-card small { + display: block; + white-space: nowrap; +} + +.uc-center-card strong { font-size: 13px; } +.uc-center-card small { margin-top: 3px; color: var(--color-ink-4); font-size: 10px; } +.uc-center-card > i { color: var(--color-ink-4); font-size: 11px; } +.uc-center-card--creation .uc-center-card__icon { color: #6650c7; background: rgba(124, 92, 220, 0.12); } +.uc-center-card--promotion .uc-center-card__icon { color: #ce6b35; background: rgba(234, 112, 55, 0.12); } + +.uc-service-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.uc-service-grid__item { + display: flex; + flex-direction: column; + align-items: center; + gap: 7px; + min-width: 0; + padding: 4px 2px; + color: var(--color-ink-3); + font-size: 11px; + text-align: center; + text-decoration: none; +} + +.uc-service-grid__icon { + position: relative; + display: grid; + place-items: center; + width: 38px; + height: 38px; + border-radius: 14px; + background: var(--surface-2); + font-size: 16px; +} + +.uc-service-grid__item--cart .uc-service-grid__icon { color: #dd6941; background: rgba(238, 109, 61, 0.12); } +.uc-service-grid__item--level .uc-service-grid__icon { color: #9b6b0a; background: rgba(234, 179, 8, 0.13); } +.uc-service-grid__item--auth .uc-service-grid__icon { color: #287bc4; background: rgba(14, 165, 233, 0.12); } +.uc-service-grid__item--notice .uc-service-grid__icon { color: #d04b65; background: rgba(244, 63, 94, 0.11); } +.uc-service-grid__item--profile .uc-service-grid__icon { color: #6550c7; background: rgba(124, 92, 220, 0.12); } +.uc-service-grid__item--security .uc-service-grid__icon { color: #16816c; background: rgba(16, 185, 129, 0.12); } + +.uc-service-grid__item:hover { + color: var(--color-primary); +} + +@media (prefers-reduced-motion: reduce) { + .uc-order-grid__icon { transition: none; } +} diff --git a/assets/js/reward.js b/assets/js/reward.js index 113a45d0..c5a2b0bc 100644 --- a/assets/js/reward.js +++ b/assets/js/reward.js @@ -1,55 +1,114 @@ /** - * Reward (打赏) — per-user QR modal openers. + * On-site wallet tipping modal. * * @package AKSRT */ ;(function () { 'use strict'; + var cfg = window.aksrtData || {}; + function initReward() { + var activeTrigger = null; + function closeAll() { - document.querySelectorAll('.reward-modal.is-open').forEach(function (m) { - m.classList.remove('is-open'); - m.setAttribute('aria-hidden', 'true'); + document.querySelectorAll('.reward-modal.is-open').forEach(function (modal) { + modal.classList.remove('is-open'); + modal.setAttribute('aria-hidden', 'true'); }); document.body.style.overflow = ''; + if (activeTrigger) activeTrigger.focus(); + activeTrigger = null; } - function openForUser(userId) { - var id = String(userId || ''); - var modal = id - ? document.getElementById('reward-modal-' + id) - : document.querySelector('.reward-modal'); + function open(trigger) { + var userId = trigger.getAttribute('data-user-id') || ''; + var modal = document.getElementById('reward-modal-' + userId); if (!modal) return; closeAll(); + activeTrigger = trigger; + modal.dataset.postId = trigger.getAttribute('data-post-id') || modal.dataset.postId || '0'; modal.classList.add('is-open'); modal.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; + var first = modal.querySelector('[data-reward-amount], [data-aksrt-reward-custom]'); + if (first) first.focus(); } - document.querySelectorAll('[data-aksrt-reward-open]').forEach(function (el) { - el.addEventListener('click', function (e) { - e.preventDefault(); - openForUser(el.getAttribute('data-user-id') || ''); - }); - }); + document.addEventListener('click', function (event) { + var opener = event.target.closest('[data-aksrt-reward-open]'); + if (opener) { + event.preventDefault(); + open(opener); + return; + } - document.querySelectorAll('[data-aksrt-reward-close]').forEach(function (el) { - el.addEventListener('click', function (e) { - e.preventDefault(); + if (event.target.closest('[data-aksrt-reward-close]')) { + event.preventDefault(); closeAll(); + return; + } + + var amountButton = event.target.closest('[data-reward-amount]'); + if (amountButton) { + var modal = amountButton.closest('.reward-modal'); + modal.querySelectorAll('[data-reward-amount]').forEach(function (button) { + button.classList.toggle('is-active', button === amountButton); + }); + var custom = modal.querySelector('[data-aksrt-reward-custom]'); + if (custom) custom.value = ''; + return; + } + + var submit = event.target.closest('[data-aksrt-reward-submit]'); + if (!submit || submit.disabled) return; + var tipModal = submit.closest('.reward-modal'); + var message = tipModal.querySelector('[data-aksrt-reward-message]'); + var customInput = tipModal.querySelector('[data-aksrt-reward-custom]'); + var selected = tipModal.querySelector('[data-reward-amount].is-active'); + var amount = customInput && customInput.value ? customInput.value : (selected ? selected.dataset.rewardAmount : ''); + + submit.disabled = true; + submit.textContent = '赞赏中…'; + if (message) message.textContent = ''; + + var body = new URLSearchParams(); + body.set('action', 'aksrt_submit_tip'); + body.set('nonce', cfg.nonce || ''); + body.set('recipient_id', tipModal.dataset.userId || ''); + body.set('post_id', tipModal.dataset.postId || '0'); + body.set('amount', amount); + + fetch(cfg.ajaxUrl || '/wp-admin/admin-ajax.php', { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, + body: body.toString() + }).then(function (response) { + return response.json(); + }).then(function (response) { + var text = response && response.data && response.data.message ? response.data.message : '赞赏失败'; + if (response && response.success) { + if (message) message.textContent = text; + setTimeout(closeAll, 900); + } else if (message) { + message.textContent = text; + } + }).catch(function () { + if (message) message.textContent = '网络错误,请稍后重试'; + }).finally(function () { + submit.disabled = false; + submit.textContent = '余额赞赏'; }); }); - document.addEventListener('keydown', function (e) { - if (e.key === 'Escape') closeAll(); + document.addEventListener('keydown', function (event) { + if (event.key === 'Escape') closeAll(); }); } if (window.AksrtFrontend && typeof window.AksrtFrontend.register === 'function') { - window.AksrtFrontend.register('aksrt-reward', function () { - initReward(); - }); + window.AksrtFrontend.register('aksrt-reward', initReward); } else if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initReward); } else { diff --git a/assets/js/routes/user-center.js b/assets/js/routes/user-center.js index 4c1b84c9..2fdffcaa 100644 --- a/assets/js/routes/user-center.js +++ b/assets/js/routes/user-center.js @@ -121,80 +121,6 @@ /* ==================== 个人信息保存 ==================== */ - /* ==================== 打赏收款码上传 ==================== */ - function initRewardQrUpload() { - var form = document.getElementById('profile-form'); - if (!form) return; - var nonceInput = form.querySelector('input[name="_profile_nonce"]'); - var nonce = nonceInput ? nonceInput.value : ''; - - function setPreview(kind, url) { - var box = form.querySelector('[data-reward-preview="' + kind + '"]'); - if (!box) return; - if (url) { - box.innerHTML = ''; - } else { - box.innerHTML = '未上传'; - } - } - - form.querySelectorAll('[data-reward-upload]').forEach(function (btn) { - btn.addEventListener('click', function () { - var kind = btn.getAttribute('data-reward-upload'); - var fileInput = form.querySelector('input[data-reward-kind="' + kind + '"]'); - if (fileInput) fileInput.click(); - }); - }); - - form.querySelectorAll('[data-reward-clear]').forEach(function (btn) { - btn.addEventListener('click', function () { - var kind = btn.getAttribute('data-reward-clear'); - var hidden = form.querySelector('#rewards-' + kind); - var fileInput = form.querySelector('input[data-reward-kind="' + kind + '"]'); - if (hidden) hidden.value = ''; - if (fileInput) fileInput.value = ''; - setPreview(kind, ''); - }); - }); - - form.querySelectorAll('input[data-reward-kind]').forEach(function (input) { - input.addEventListener('change', function () { - var kind = input.getAttribute('data-reward-kind'); - var file = input.files && input.files[0]; - if (!file || !kind) return; - if (file.size > 2 * 1024 * 1024) { - showToast('图片不能超过 2MB'); - input.value = ''; - return; - } - var body = new FormData(); - body.append('action', 'aksrt_upload_reward_qr'); - body.append('_profile_nonce', nonce); - body.append('kind', kind); - body.append('file', file); - showToast(i18n.saving || '上传中…'); - fetch(ajaxUrl, { method: 'POST', body: body, credentials: 'same-origin' }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res && res.success && res.data) { - var hidden = form.querySelector('#rewards-' + kind); - if (hidden) hidden.value = String(res.data.id || ''); - setPreview(kind, res.data.url || ''); - showToast(res.data.message || '上传成功'); - } else { - showToast((res && res.data && res.data.message) || '上传失败'); - } - }) - .catch(function () { - showToast(i18n.networkError || '网络错误'); - }) - .finally(function () { - input.value = ''; - }); - }); - }); - } - function initProfileForm() { var form = document.getElementById('profile-form'); @@ -752,7 +678,6 @@ document.addEventListener('DOMContentLoaded', function() { initDashboardNavigation(); initProfileForm(); - initRewardQrUpload(); initUnfavorite(); initNotifications(); initSettings(); @@ -760,7 +685,6 @@ } else { initDashboardNavigation(); initProfileForm(); - initRewardQrUpload(); initUnfavorite(); initNotifications(); initSettings(); diff --git a/inc/helpers/aksrt-frontend-tags.php b/inc/helpers/aksrt-frontend-tags.php index 0629e4d5..f2d1463a 100644 --- a/inc/helpers/aksrt-frontend-tags.php +++ b/inc/helpers/aksrt-frontend-tags.php @@ -91,9 +91,12 @@ if (!function_exists('aksrt_resolve_single_post_display_settings')) : 'authordesc' => true, 'hide_time' => false, 'rewards_enabled' => false, - 'rewards_image' => '', 'rewards_text' => '', 'rewards_btn_text' => '赞赏', + 'rewards_amounts' => [5, 10, 20, 50, 100], + 'rewards_custom' => true, + 'rewards_min' => 1, + 'rewards_max' => 1000, 'maxheight_enabled' => false, 'maxheight' => 1000, 'copyright_enabled' => true, diff --git a/inc/helpers/aksrt-rewards.php b/inc/helpers/aksrt-rewards.php index a1823ff0..06f777e4 100644 --- a/inc/helpers/aksrt-rewards.php +++ b/inc/helpers/aksrt-rewards.php @@ -1,8 +1,6 @@ '', 'alipay' => '']; - if ($user_id <= 0) { - return $out; - } - foreach (['weixin' => 'aksrt_rewards_weixin', 'alipay' => 'aksrt_rewards_alipay'] as $key => $meta) { - $raw = get_user_meta($user_id, $meta, true); - if (is_numeric($raw) && (int) $raw > 0) { - $url = wp_get_attachment_image_url((int) $raw, 'full'); - $out[$key] = is_string($url) ? $url : ''; - } else { - $out[$key] = esc_url_raw((string) $raw); - } - } - return $out; - } -} - -if (!function_exists('aksrt_user_has_rewards_qr')) { - function aksrt_user_has_rewards_qr(int $user_id): bool { - $urls = aksrt_get_user_rewards_img_urls($user_id); - return $urls['weixin'] !== '' || $urls['alipay'] !== ''; + $settings = \AKSRT\Settings\Settings::get_post_page_settings(); + return !empty($settings['rewards_enabled']); } } if (!function_exists('aksrt_get_rewards_button')) { - function aksrt_get_rewards_button(int $user_id, string $class = 'but jb-blue ml10 pw-1em'): string { - if ($user_id <= 0 || !aksrt_rewards_enabled() || !aksrt_user_has_rewards_qr($user_id)) { + function aksrt_get_rewards_button(int $user_id, string $class = 'but jb-blue ml10 pw-1em', int $post_id = 0): string { + if ($user_id <= 0 || !aksrt_rewards_enabled()) { return ''; } - $label = '赞赏'; - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_post_page_settings')) { - $s = \AKSRT\Settings\Settings::get_post_page_settings(); - $label = (string) ($s['rewards_btn_text'] ?? $s['rewards_text'] ?? $label); - if ($label === '') { - $label = '赞赏'; - } - } - return ''; + $settings = \AKSRT\Settings\Settings::get_post_page_settings(); + $label = (string) ($settings['rewards_btn_text'] ?? __('赞赏', 'aksrt')); + return sprintf( + '', + esc_attr($class), + $user_id, + $post_id, + esc_html($label) + ); } } if (!function_exists('aksrt_render_rewards_modal_for_user')) { - function aksrt_render_rewards_modal_for_user(int $user_id): void { + function aksrt_render_rewards_modal_for_user(int $user_id, int $post_id = 0): void { static $done = []; - if ($user_id <= 0 || isset($done[$user_id])) { + $key = $user_id . ':' . $post_id; + if ($user_id <= 0 || isset($done[$key]) || !aksrt_rewards_enabled()) { return; } - if (!aksrt_rewards_enabled() || !aksrt_user_has_rewards_qr($user_id)) { - return; - } - $done[$user_id] = true; - $urls = aksrt_get_user_rewards_img_urls($user_id); - $label = '赞赏'; - if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_post_page_settings')) { - $s = \AKSRT\Settings\Settings::get_post_page_settings(); - $label = (string) ($s['rewards_text'] ?? $label); - if ($label === '') { - $label = __('赞赏', 'aksrt'); - } - } + $done[$key] = true; + $settings = \AKSRT\Settings\Settings::get_post_page_settings(); + $amounts = (array) ($settings['rewards_amounts'] ?? [5, 10, 20, 50, 100]); + $recipient = get_userdata($user_id); ?> -