diff --git a/assets/admin/src/schema/sections/permissions/index.js b/assets/admin/src/schema/sections/permissions/index.js index 2826560c..2eea9afc 100644 --- a/assets/admin/src/schema/sections/permissions/index.js +++ b/assets/admin/src/schema/sections/permissions/index.js @@ -84,6 +84,7 @@ export const permissionsSchemas = { { label: '资源下载', value: 'download' }, { label: '每日签到', value: 'signin' }, { label: '前台投稿', value: 'front_submit' }, + { label: '打赏用户', value: 'tip_user' }, ], }, ], diff --git a/assets/css/frontend/single-post/rewards.css b/assets/css/frontend/single-post/rewards.css index 8ed982b8..9e7291bf 100644 --- a/assets/css/frontend/single-post/rewards.css +++ b/assets/css/frontend/single-post/rewards.css @@ -42,14 +42,25 @@ cursor: pointer; font-size: 15px; font-weight: 600; - transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease; + transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease; +} + +.aksrt-reward__amount:hover { + border-color: var(--focus-color, var(--color-primary, #f04494)); + background: var(--color-primary-soft, rgba(240, 68, 148, 0.1)); + color: var(--focus-color, var(--color-primary, #f04494)); +} + +.aksrt-reward__amount:focus-visible { + outline: 0; + box-shadow: 0 0 0 3px var(--color-primary-soft, rgba(240, 68, 148, 0.2)); } -.aksrt-reward__amount:hover, .aksrt-reward__amount.is-active { border-color: var(--focus-color, var(--color-primary, #f04494)); background: var(--focus-color, var(--color-primary, #f04494)); color: #fff; + box-shadow: 0 3px 10px var(--color-primary-soft, rgba(240, 68, 148, 0.2)); } .aksrt-reward__currency { @@ -71,13 +82,43 @@ font-size: 13px; } -.aksrt-reward__input { +.aksrt-reward__custom-label { + flex: 0 0 auto; +} + +.aksrt-reward__input-wrap { + display: flex; width: min(180px, 58%); min-height: 40px; + align-items: center; padding: 0 12px; border: 1px solid var(--muted-border-color, var(--color-line-stronger, rgba(50, 50, 50, 0.12))); border-radius: var(--main-radius, 8px); background: var(--main-bg-color, #fff); + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.aksrt-reward__input-wrap:focus-within { + border-color: var(--focus-color, var(--color-primary, #f04494)); + box-shadow: 0 0 0 3px var(--color-primary-soft, rgba(240, 68, 148, 0.1)); +} + +.aksrt-reward__input-currency { + flex: 0 0 auto; + margin-right: 6px; + color: var(--muted-color, var(--color-ink-3, #777)); + font-size: 14px; + font-weight: 600; +} + +.aksrt-reward__input { + width: 100%; + min-width: 0; + min-height: 38px; + padding: 0; + border: 0; + outline: 0; + background: transparent; color: var(--main-color, var(--color-ink, #333)); text-align: right; -moz-appearance: textfield; @@ -89,9 +130,15 @@ -webkit-appearance: none; } -.aksrt-reward__input:focus { - border-color: var(--focus-color, var(--color-primary, #f04494)); - outline: 0; +.aksrt-reward__input::placeholder { + color: var(--muted-color, var(--color-ink-4, #999)); + opacity: 1; +} + +.aksrt-reward__input:disabled, +.aksrt-reward__amount:disabled { + cursor: wait; + opacity: 0.6; } .aksrt-reward__submit { @@ -104,11 +151,19 @@ } .aksrt-reward__message { - min-height: 20px; - margin-top: 8px; - color: var(--focus-color, var(--color-primary, #f04494)); - font-size: 12px; - text-align: center; + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.aksrt-reward__captcha { + margin: 0 0 14px; } .aksrt-modal-runtime--bottom.aksrt-reward-modal .aksrt-modal-runtime__dialog { diff --git a/assets/js/reward.js b/assets/js/reward.js index b366b074..3ffdafcc 100644 --- a/assets/js/reward.js +++ b/assets/js/reward.js @@ -8,6 +8,25 @@ var cfg = window.aksrtData || {}; var titleSequence = 0; + var toastId = 'aksrt-reward-toast'; + + function escapeHtml(value) { + return String(value == null ? '' : value).replace(/[&<>"']/g, function (character) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }[character]; + }); + } + + function notify(message, type, time) { + if (typeof window.notyf === 'function') { + window.notyf(escapeHtml(message), type || 'info', time, toastId); + } + } function rewardModal(target) { return target.closest('.aksrt-modal-runtime.aksrt-reward-modal'); @@ -24,6 +43,28 @@ return template.dataset.aksrtOperationKey; } + function runCaptcha(modal) { + var mount = modal.querySelector('[data-aksrt-captcha-config][data-scene="tip_user"]'); + if (!mount) return Promise.resolve({}); + if (!window.AKSRTCaptcha || typeof window.AKSRTCaptcha.run !== 'function') { + return Promise.reject(new Error('人机验证正在加载,请稍后重试')); + } + + return window.AKSRTCaptcha.run('tip_user', modal).then(function (payload) { + if (!payload) return null; + if (!payload.aksrt_captcha_proof) throw new Error('人机验证凭证未就绪,请重试'); + return payload; + }); + } + + function setSubmitting(modal, submit, submitting, succeeded) { + submit.disabled = submitting || succeeded; + submit.textContent = submitting ? '验证中…' : '余额赞赏'; + modal.querySelectorAll('[data-reward-amount], [data-aksrt-reward-custom]').forEach(function (control) { + control.disabled = submitting || succeeded; + }); + } + function openReward(trigger) { var runtime = window.AksrtModalRuntime; if (!runtime || !window.AksrtRequest) { @@ -96,17 +137,26 @@ var custom = modal.querySelector('[data-aksrt-reward-custom]'); var selected = modal.querySelector('[data-reward-amount].is-active'); var amount = custom && custom.value ? custom.value : (selected ? selected.dataset.rewardAmount : ''); + if (!amount) { + if (message) message.textContent = '请选择或输入赞赏金额'; + notify('请选择或输入赞赏金额', 'warning'); + return; + } + if (custom && custom.value && !custom.checkValidity()) { + custom.reportValidity(); + return; + } var key; try { key = operationKey(template); } catch (error) { if (message) message.textContent = error.message || '无法创建安全请求,请稍后重试'; + notify(error.message || '无法创建安全请求,请稍后重试', 'danger'); return; } - submit.disabled = true; - submit.textContent = '赞赏中…'; - if (message) message.textContent = ''; + setSubmitting(modal, submit, true, false); + if (message) message.textContent = '正在进行人机验证…'; var body = new URLSearchParams(); body.set('action', 'aksrt_submit_tip'); @@ -116,34 +166,70 @@ body.set('amount', amount); var succeeded = false; - window.fetch(cfg.ajaxUrl || '/wp-admin/admin-ajax.php', { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'Idempotency-Key': key, - }, - body: body.toString(), - }).then(function (response) { - return response.json(); + runCaptcha(modal).then(function (captchaPayload) { + if (!captchaPayload) { + if (window.AKSRTCaptcha && typeof window.AKSRTCaptcha.reset === 'function') { + window.AKSRTCaptcha.reset('tip_user', modal); + } + return { cancelled: true }; + } + if (runtime.getActive() !== modal) { + if (window.AKSRTCaptcha && typeof window.AKSRTCaptcha.reset === 'function') { + window.AKSRTCaptcha.reset('tip_user', modal); + } + return { cancelled: true }; + } + Object.keys(captchaPayload).forEach(function (name) { + var value = captchaPayload[name]; + if (value != null && typeof value !== 'object') body.set(name, String(value)); + }); + submit.textContent = '赞赏中…'; + if (message) message.textContent = '正在提交赞赏…'; + notify('正在提交赞赏…', 'load', ''); + return window.fetch(cfg.ajaxUrl || '/wp-admin/admin-ajax.php', { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'Idempotency-Key': key, + }, + body: body.toString(), + }).then(function (response) { + return response.json(); + }); }).then(function (response) { if (runtime.getActive() !== modal) return; + if (response && response.cancelled) { + if (message) message.textContent = '请完成人机验证后再试'; + notify('请完成人机验证后再试', 'warning', 5); + return; + } var text = response && response.data && response.data.message ? response.data.message : '赞赏失败'; if (response && response.success) { succeeded = true; delete template.dataset.aksrtOperationKey; if (message) message.textContent = text; + notify(text, 'success', 5); window.setTimeout(function () { if (runtime.getActive() === modal) runtime.close('success'); }, 900); - } else if (message) { - message.textContent = text; + } else { + if (message) message.textContent = text; + notify(text, 'danger', 5); + if (window.AKSRTCaptcha && typeof window.AKSRTCaptcha.reset === 'function') { + window.AKSRTCaptcha.reset('tip_user', modal); + } + } + }).catch(function (error) { + if (runtime.getActive() !== modal) return; + var errorMessage = error && error.message ? error.message : '网络错误,请稍后重试'; + if (message) message.textContent = errorMessage; + notify(errorMessage, 'danger', 5); + if (window.AKSRTCaptcha && typeof window.AKSRTCaptcha.reset === 'function') { + window.AKSRTCaptcha.reset('tip_user', modal); } - }).catch(function () { - if (runtime.getActive() === modal && message) message.textContent = '网络错误,请稍后重试'; }).finally(function () { - submit.disabled = succeeded; - submit.textContent = '余额赞赏'; + setSubmitting(modal, submit, false, succeeded); }); }); diff --git a/inc/helpers/aksrt-rewards.php b/inc/helpers/aksrt-rewards.php index 19ae0a41..93459e17 100644 --- a/inc/helpers/aksrt-rewards.php +++ b/inc/helpers/aksrt-rewards.php @@ -82,10 +82,16 @@ if (!function_exists('aksrt_render_rewards_template_for_user')) { - + + + +
-
+
__('操作太频繁,请稍后再试', 'aksrt')], 429); } + $captcha_result = aksrt_require_captcha('tip_user', wp_unslash($_POST)); + if ($captcha_result !== true) { + $captcha_error = is_wp_error($captcha_result) ? $captcha_result : null; + $captcha_data = $captcha_error ? $captcha_error->get_error_data() : null; + $captcha_status = is_array($captcha_data) ? (int) ($captcha_data['status'] ?? 400) : 400; + wp_send_json_error([ + 'code' => $captcha_error ? ($captcha_error->get_error_code() ?: 'aksrt_captcha_failed') : 'aksrt_captcha_failed', + 'message' => $captcha_error ? ($captcha_error->get_error_message() ?: __('人机验证失败,请重试', 'aksrt')) : __('人机验证失败,请重试', 'aksrt'), + ], $captcha_status > 0 ? $captcha_status : 400); + } set_transient($rate_key, $operation_key, 3); $result = \AKSRT\Ledger\LedgerService::get_instance()->transfer( diff --git a/inc/modules/captcha.php b/inc/modules/captcha.php index dd87188d..7c45d686 100644 --- a/inc/modules/captcha.php +++ b/inc/modules/captcha.php @@ -374,6 +374,7 @@ function aksrt_captcha_scenes(): array { 'download' => __('资源下载', 'aksrt'), 'signin' => __('每日签到', 'aksrt'), 'front_submit' => __('前台投稿', 'aksrt'), + 'tip_user' => __('打赏用户', 'aksrt'), ]; } diff --git a/src/Admin/Schema/SchemaPermissions.php b/src/Admin/Schema/SchemaPermissions.php index c8b53857..2a5aed58 100644 --- a/src/Admin/Schema/SchemaPermissions.php +++ b/src/Admin/Schema/SchemaPermissions.php @@ -80,6 +80,7 @@ class SchemaPermissions { ['label' => '资源下载', 'value' => 'download'], ['label' => '每日签到', 'value' => 'signin'], ['label' => '前台投稿', 'value' => 'front_submit'], + ['label' => '打赏用户', 'value' => 'tip_user'], ]], ]], ['key' => 'providers', 'title' => '服务商配置', 'dependsOn' => ['key' => 'enabled', 'value' => true], 'fields' => $provider_fields],