fix(signin): align check-in feedback with reference theme

This commit is contained in:
kate522 committed 2026-10-29 17:06:25 +08:00
1 parent 1df2822f01
commit 8e9d3a7e49
7 files changed
+334 -65

No files matched your search

+56 -58
View File
@@ -11,6 +11,7 @@
}
var signinBusy = false;
var refreshSequence = 0;
function api(path, options) {
return window.AksrtRequest.request(path, options || {}).then(function (response) {
@@ -60,24 +61,17 @@
});
}
function setButtonState(state) {
currentButtons().forEach(function (button) {
var label = button.querySelector('[data-signin-label]');
var text = state === 'done'
? button.getAttribute('data-signin-done-label') || '已签到'
: (state === 'busy' ? '…' : button.getAttribute('data-signin-idle-label') || '签到');
if (label) label.textContent = text;
button.disabled = state !== 'idle';
button.classList.toggle('is-done', state === 'done');
});
}
// 签到成功后,入口按钮就地替换为「已签到」详情弹窗触发链接
// (参考主题:.initiate-checkin → details_link 的替换行为)。
function swapButtonsToDetailsLink() {
var url = window.aksrtData && window.aksrtData.signinDetailsUrl;
if (!url) return;
currentButtons().forEach(function (button) {
var label = button.querySelector('[data-signin-label]');
if (label) {
label.textContent = button.getAttribute('data-signin-done-label') || '已签到';
label.removeAttribute('data-signin-label');
}
var link = document.createElement('a');
link.href = 'javascript:;';
link.className = (button.className || '') + ' checkin-details-link';
@@ -88,12 +82,17 @@
link.setAttribute('data-aksrt-modal-mobile-bottom', 'true');
link.setAttribute('data-aksrt-modal-cache', 'false');
link.innerHTML = button.innerHTML;
if (link.classList.contains('uc-header__signin-btn')) link.classList.add('is-done');
button.replaceWith(link);
});
}
function refresh() {
if (signinBusy) return;
var sequence = ++refreshSequence;
api('/signin/today').then(function (result) {
// A status read started before a click must not overwrite its result.
if (signinBusy || sequence !== refreshSequence) return;
if (!result.ok) {
var message = (result.json && result.json.message) || '签到状态加载失败 (HTTP ' + result.status + ')';
setStatus(message);
@@ -104,17 +103,19 @@
var data = result.json;
if (!data.enabled) {
setStatus('签到功能未启用');
setButtonState('done');
currentButtons().forEach(function (button) {
button.disabled = true;
button.setAttribute('disabled', '');
button.setAttribute('aria-disabled', 'true');
});
return;
}
if (data.signed) {
setStatusHtml('已签到 <strong>' + data.streak + '</strong> 天 +' + data.reward_today);
setButtonState('done');
swapButtonsToDetailsLink();
return;
}
setStatusHtml('连续 <strong>' + data.streak + '</strong> 天,+' + data.reward_if_signin);
setButtonState('idle');
});
}
@@ -122,62 +123,59 @@
return document.querySelector('[data-signin-captcha]');
}
function doSignin(captchaPayload) {
notyf('正在签到…', 'load', '', 'aksrt-balance-signin-toast');
api('/signin', {
method: 'POST',
body: JSON.stringify({ captcha: captchaPayload || {} })
}).then(function (result) {
signinBusy = false;
if (!result.ok) {
notyf((result.json && result.json.message) || '签到失败', 'danger', 5, 'aksrt-balance-signin-toast');
setButtonState('idle');
return;
}
notyf(result.json.message || '签到成功', 'success', 5, 'aksrt-balance-signin-toast');
document.querySelectorAll('[data-credits-value]').forEach(function (element) {
element.textContent = Number(result.json.balance || 0).toLocaleString();
element.title = element.textContent;
});
setButtonState('done');
swapButtonsToDetailsLink();
refresh();
}).finally(function () {
if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset('signin', captchaMount());
});
}
freshButtons.forEach(function (button) {
button.addEventListener('click', function (event) {
event.preventDefault();
if (signinBusy || button.disabled) return;
signinBusy = true;
setButtonState('busy');
++refreshSequence;
var originalHtml = button.innerHTML;
button.disabled = true;
button.setAttribute('disabled', '');
button.setAttribute('aria-busy', 'true');
button.innerHTML = '<i class="loading mr6" aria-hidden="true"></i><text>请稍后</text>';
function restoreButton() {
button.innerHTML = originalHtml;
button.disabled = false;
button.removeAttribute('disabled');
button.removeAttribute('aria-busy');
}
var captchaRequired = !!window.aksrtData.captchaSignin;
var captcha = window.AKSRTCaptcha;
var mount = captchaMount();
if (captchaRequired && (!captcha || !mount)) {
signinBusy = false;
notyf('人机验证未正确加载,请刷新页面后重试', 'danger');
setButtonState('idle');
return;
}
if (!captchaRequired) {
doSignin({});
return;
}
captcha.run('signin', mount).then(function (payload) {
Promise.resolve().then(function () {
if (captchaRequired && (!captcha || !mount)) {
throw new Error('人机验证未正确加载,请刷新页面后重试');
}
return captchaRequired ? captcha.run('signin', mount) : {};
}).then(function (payload) {
if (payload === null) throw new Error('已取消验证');
doSignin(payload);
notyf('正在签到,请稍后...', 'load', '', 'aksrt-balance-signin-toast');
return api('/signin', {
method: 'POST',
body: JSON.stringify({ captcha: payload })
});
}).then(function (result) {
if (!result.ok) throw new Error(result.json.message || '签到失败');
restoreButton();
notyf(result.json.message || '签到成功', 'success', 5, 'aksrt-balance-signin-toast');
document.querySelectorAll('[data-credits-value]').forEach(function (element) {
element.textContent = Number(result.json.balance || 0).toLocaleString();
element.title = element.textContent;
});
setStatusHtml('已签到 <strong>' + result.json.streak + '</strong> 天 +' + result.json.reward);
swapButtonsToDetailsLink();
}).catch(function (error) {
restoreButton();
notyf((error && error.message) || '签到失败,请稍后重试', 'danger', 5, 'aksrt-balance-signin-toast');
}).finally(function () {
signinBusy = false;
captcha.reset('signin', mount);
notyf((error && error.message) || '人机验证加载中,请稍候再试', 'danger');
setButtonState('idle');
if (captcha && mount) captcha.reset('signin', mount);
});
});
});
+1 -3
View File
@@ -64,7 +64,7 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
);
}
return aksrt_get_user_checkin_details_link(
'but block c-yellow is-done',
'but block c-yellow',
'<i class="fa far fa-calendar-check" aria-hidden="true"></i> '
. esc_html__('今日已签到', 'aksrt')
);
@@ -75,7 +75,6 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
// instead of the UA button stylesheet (font/UI 1:1 with the reference).
if ($class !== null) {
return '<a class="' . esc_attr($class) . '" href="javascript:;" data-signin-btn'
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
. ' data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
. '<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
. '<span data-signin-label>' . esc_html__('签到', 'aksrt') . '</span>'
@@ -83,7 +82,6 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
}
return '<a class="but block c-yellow" href="javascript:;" data-signin-btn'
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
. ' data-signin-done-label="' . esc_attr__('今日已签到', 'aksrt') . '">'
. '<i class="fa far fa-calendar-check" aria-hidden="true"></i> '
. '<span data-signin-label>' . esc_html__('签到领取今日奖励', 'aksrt') . '</span>'
@@ -35,7 +35,7 @@ $currency = (string) ($currency_name ?? __('积分', 'aksrt'));
<?php if (!empty($signin_signed_today)) : ?>
<?php echo aksrt_get_user_checkin_details_link('uc-asset-card__signin muted-color px12', '<span>' . esc_html__('已签到', 'aksrt') . '</span><i class="fa fas fa-angle-right em12 ml3" aria-hidden="true"></i>'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php else : ?>
<button type="button" class="uc-asset-card__signin muted-color px12" data-signin-btn data-signin-idle-label="<?php esc_attr_e('签到领积分', 'aksrt'); ?>" data-signin-done-label="<?php esc_attr_e('已签到', 'aksrt'); ?>" disabled>
<button type="button" class="uc-asset-card__signin muted-color px12" data-signin-btn data-signin-done-label="<?php esc_attr_e('已签到', 'aksrt'); ?>">
<span data-signin-label><?php esc_html_e('签到领积分', 'aksrt'); ?></span><i class="fa fas fa-angle-right em12 ml3" aria-hidden="true"></i>
</button>
<?php endif; ?>
@@ -85,7 +85,6 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
type="button"
class="btn btn-sm uc-header__signin-btn"
data-signin-btn
data-signin-idle-label="<?php esc_attr_e('签到', 'aksrt'); ?>"
data-signin-done-label="<?php esc_attr_e('已签到', 'aksrt'); ?>"
>
<i class="fa fas fa-calendar-check" aria-hidden="true"></i>
-1
View File
@@ -338,7 +338,6 @@ class Signin {
</div>';
} else {
$checkin_btn = '<button type="button" class="but c-blue ml10 p2-10 radius" data-signin-btn'
. ' data-signin-idle-label="' . esc_attr__('立即签到', 'aksrt') . '"'
. ' data-signin-done-label="' . esc_attr__('今日已签到', 'aksrt') . '">'
. '<span data-signin-label>' . esc_html__('立即签到', 'aksrt') . '</span></button>';
}
@@ -115,11 +115,13 @@ test('sign-in waits for main before binding and invokes the signin captcha', asy
const button = {
dataset: {},
disabled: false,
attrs: {},
classList: { toggle() {} },
setAttribute(name, value) { this.attrs[name] = value; },
removeAttribute(name) { delete this.attrs[name]; },
addEventListener(name, handler) { listeners[name] = handler; },
querySelector(selector) { return selector === '[data-signin-label]' ? label : null; },
getAttribute(name) {
if (name === 'data-signin-idle-label') return '签到';
if (name === 'data-signin-done-label') return '已签到';
return '';
},
+273
View File
@@ -0,0 +1,273 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const vm = require('node:vm');
const root = path.resolve(__dirname, '..', '..');
const source = fs.readFileSync(path.join(root, 'assets/js/signin.js'), 'utf8');
class Element {
constructor(tag, html = '', className = '', labels = {}) {
this.tagName = tag.toUpperCase();
this._html = html;
this.className = className;
this.dataset = {};
this.attrs = {};
this.listeners = {};
this.replaced = null;
this.disabled = false;
this.title = '';
this._label = null;
this.labels = labels;
this.classList = {
contains: (name) => this.className.split(/\s+/).includes(name),
add: (name) => { if (!this.classList.contains(name)) this.className = (this.className + ' ' + name).trim(); },
};
this.parseLabel();
}
get innerHTML() { return this._html; }
set innerHTML(value) { this._html = value; this.parseLabel(); }
parseLabel() {
const match = this._html.match(/<span data-signin-label>(.*?)<\/span>/);
if (!match) { this._label = null; return; }
const parent = this;
let value = match[1];
let hasHook = true;
this._label = {
get textContent() { return value; },
set textContent(text) { value = text; parent._html = parent._html.replace(/<span data-signin-label>.*?<\/span>/, '<span' + (hasHook ? ' data-signin-label' : '') + '>' + text + '</span>'); },
removeAttribute(name) { if (name === 'data-signin-label') { hasHook = false; parent._html = parent._html.replace(' data-signin-label', ''); } },
};
}
querySelector(selector) { return selector === '[data-signin-label]' ? this._label : null; }
addEventListener(name, callback) { (this.listeners[name] ||= []).push(callback); }
getAttribute(name) { return this.attrs[name] ?? this.labels[name] ?? ''; }
setAttribute(name, value) { this.attrs[name] = String(value); }
removeAttribute(name) { delete this.attrs[name]; }
replaceWith(node) { this.replaced = node; }
click() { (this.listeners.click || []).forEach((callback) => callback({ preventDefault() {} })); }
}
function makeButton(className, label, icon = '<i class="fa fa-calendar-check"></i>', doneLabel = '已签到') {
return new Element('button', icon + '<span data-signin-label>' + label + '</span>', className, {
'data-signin-done-label': doneLabel,
});
}
function deferred() {
let resolve;
let reject;
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
return { promise, resolve, reject };
}
function setup({ buttons = [makeButton('btn uc-header__signin-btn', '签到')], captchaSignin = false, today = Promise.resolve({ ok: true, status: 200, json: { enabled: true, signed: false, streak: 1, reward_if_signin: 2 } }), signin = Promise.resolve({ ok: true, status: 200, json: { message: '签到成功,积分 +2', balance: 12 } }), captchaRun } = {}) {
const calls = [];
const notices = [];
const balance = { textContent: '10', title: '10' };
const status = { innerHTML: '', textContent: '' };
const modals = [];
const listeners = {};
const mount = { nodeType: 1 };
const doc = {
querySelectorAll(selector) {
if (selector === '[data-signin-btn]') return buttons.filter((button) => !button.replaced);
if (selector === '[data-credits-value]') return [balance];
if (selector === '[data-signin-status]') return [status];
return [];
},
querySelector(selector) { return selector === '[data-signin-captcha]' ? mount : null; },
createElement(tag) { return new Element(tag); },
};
const jq = { off() { return this; }, on(event, callback) { listeners[event] = callback; return this; } };
const window = {
aksrtData: { captchaSignin, signinDetailsUrl: '/wp-json/aksrt/v1/signin/details' },
AksrtModalRuntime: { open: (...args) => modals.push(args), openRemote: (...args) => modals.push(args) },
AksrtRequest: { request(endpoint, options) { calls.push({ endpoint, options }); return endpoint === '/signin/today' ? today : signin; } },
};
const captchaCalls = [];
if (captchaRun) window.AKSRTCaptcha = {
run(scene, scope) { captchaCalls.push(['run', scene, scope]); return captchaRun; },
reset(scene, scope) { captchaCalls.push(['reset', scene, scope]); },
};
vm.runInNewContext(source, { window, document: doc, jQuery: () => jq, notyf: (...args) => notices.push(args), Promise, JSON, Number, tbquire: (_deps, ready) => { ready(); } });
return { buttons, calls, notices, listeners, window, captchaCalls, mount, balance, status, modals };
}
const settle = () => new Promise((resolve) => setImmediate(resolve));
test('uses reference loading, success toast, and replaces all entry points with detail links', async () => {
const first = makeButton('img-badge jb-yellow', '签到', '<i class="fa far fa-calendar-check ml3 mr6"></i>');
const second = makeButton('btn uc-header__signin-btn', '签到', '<i class="fa fa-calendar-check"></i>', '今日已签到');
const success = deferred();
const h = setup({ buttons: [first, second], signin: success.promise });
await settle();
const originalOther = second.innerHTML;
first.click();
assert.equal(first.disabled, true);
assert.equal(first.innerHTML, '<i class="loading mr6" aria-hidden="true"></i><text>请稍后</text>');
assert.equal(first.getAttribute('aria-busy'), 'true');
assert.ok(Object.hasOwn(first.attrs, 'disabled'));
assert.equal(second.innerHTML, originalOther, 'non-clicked entries keep their content while the request runs');
assert.equal(second.disabled, false);
first.click();
second.click();
await settle();
assert.equal(h.calls.filter((call) => call.endpoint === '/signin').length, 1);
success.resolve({ ok: true, status: 200, json: { message: '签到成功,积分 +2', balance: 12, streak: 2, reward: 2 } });
await settle();
assert.deepEqual(h.notices.find((notice) => notice[1] === 'success')?.slice(0, 2), ['签到成功,积分 +2', 'success']);
for (const original of [first, second]) {
assert.equal(original.replaced?.getAttribute('data-aksrt-modal'), 'remote');
assert.equal(original.replaced?.getAttribute('data-aksrt-modal-url'), '/wp-json/aksrt/v1/signin/details');
assert.match(original.replaced.innerHTML, /已签到/);
assert.doesNotMatch(original.replaced.innerHTML, /data-signin-label/);
assert.match(original.replaced.className, /checkin-details-link/);
assert.match(original.replaced.innerHTML, /fa-calendar-check/);
assert.equal(Object.hasOwn(original.replaced.attrs, 'disabled'), false);
assert.equal(original.replaced.getAttribute('aria-busy'), '');
}
assert.doesNotMatch(first.replaced.className, /is-done/);
assert.match(second.replaced.innerHTML, /今日已签到/);
assert.equal(second.replaced.className.includes('uc-header__signin-btn'), true);
assert.equal(h.calls.filter((call) => call.endpoint === '/signin/today').length, 1, 'success does not issue another status request');
assert.equal(h.modals.length, 0, 'success does not open a modal');
assert.equal(h.balance.textContent, '12');
assert.equal(h.status.innerHTML, '已签到 <strong>2</strong> 天 +2');
});
test('keeps the profile done style on its replacement link', async () => {
const button = makeButton('btn uc-header__signin-btn', '签到');
const success = deferred();
const h = setup({ buttons: [button], signin: success.promise });
await settle();
button.click();
success.resolve({ ok: true, status: 200, json: { message: '签到成功', balance: 1 } });
await settle();
assert.match(button.replaced.className, /is-done/);
});
test('restores complete button markup after a rejected sign-in response', async () => {
const button = makeButton('img-badge jb-yellow', '签到', '<i class="fa far fa-calendar-check ml3 mr6"></i>');
const initial = button.innerHTML;
const h = setup({ buttons: [button], signin: Promise.resolve({ ok: false, status: 500, json: { message: '签到失败' } }) });
await settle();
button.click();
await settle();
assert.equal(button.innerHTML, initial);
assert.equal(button.disabled, false);
assert.equal(button.getAttribute('aria-busy'), '');
assert.equal(button.replaced, null);
});
test('preserves the existing main/captcha ordering and resets captcha after submission', async () => {
const button = makeButton('btn', '签到');
const success = deferred();
const captcha = deferred();
const h = setup({ buttons: [button], captchaSignin: true, signin: success.promise, captchaRun: captcha.promise });
await settle();
button.click();
await settle();
assert.deepEqual(h.captchaCalls[0]?.slice(0, 2), ['run', 'signin']);
assert.equal(h.calls.some((call) => call.endpoint === '/signin'), false);
captcha.resolve({ proof: 'ok' });
await settle();
assert.equal(h.calls.filter((call) => call.endpoint === '/signin').length, 1);
success.resolve({ ok: true, status: 200, json: { message: '签到成功', balance: 1 } });
await settle();
assert.deepEqual(h.captchaCalls.map((call) => call[0]), ['run', 'reset']);
});
test('allows retry after a rejected request', async () => {
const button = makeButton('btn', '签到');
const today = deferred();
const h = setup({ buttons: [button], today: today.promise, signin: Promise.reject(Object.assign(new Error('network down'), { status: 0 })) });
button.click();
await settle();
today.resolve({ ok: true, status: 200, json: { enabled: true, signed: false, streak: 2, reward_if_signin: 3 } });
await settle();
assert.equal(button.innerHTML.includes('请稍后'), false);
assert.equal(button.disabled, false);
assert.equal(button.replaced, null);
button.click();
await settle();
assert.equal(h.calls.filter((call) => call.endpoint === '/signin').length, 2);
});
test('keeps the loading button when a previous status read completes during submission', async () => {
const button = makeButton('img-badge jb-yellow', '签到');
const today = deferred();
const signin = deferred();
const h = setup({ buttons: [button], today: today.promise, signin: signin.promise });
button.click();
await settle();
today.resolve({ ok: true, status: 200, json: { enabled: true, signed: true, streak: 2, reward_today: 3 } });
await settle();
assert.equal(button.replaced, null);
assert.match(button.innerHTML, /loading mr6/);
assert.equal(button.disabled, true);
assert.equal(h.status.innerHTML, '');
signin.resolve({ ok: true, status: 200, json: { message: '签到成功', balance: 1, streak: 2, reward: 3 } });
await settle();
assert.ok(button.replaced);
});
test('ignores an old today response after successful sign-in', async () => {
const button = makeButton('btn', '签到');
const today = deferred();
const signin = deferred();
const h = setup({ buttons: [button], today: today.promise, signin: signin.promise });
button.click();
await settle();
signin.resolve({ ok: true, status: 200, json: { message: '签到成功', balance: 1, streak: 3, reward: 2 } });
await settle();
today.resolve({ ok: true, status: 200, json: { enabled: true, signed: false, streak: 2, reward_if_signin: 3 } });
await settle();
assert.ok(button.replaced, 'success replaced the action with the details link');
assert.match(button.replaced.innerHTML, /已签到/);
assert.equal(h.status.innerHTML, '已签到 <strong>3</strong> 天 +2');
});
test('leaves a disabled sign-in feature disabled without turning it into a signed state', async () => {
const button = makeButton('btn', '签到');
const h = setup({ buttons: [button], today: Promise.resolve({ ok: true, status: 200, json: { enabled: false, signed: false } }) });
await settle();
assert.equal(button.disabled, true);
assert.equal(button.replaced, null);
assert.doesNotMatch(button.className, /is-done/);
assert.equal(button.innerHTML.includes('已签到'), false);
});
test('does not send the request when captcha is absent or cancelled and keeps original markup', async () => {
const missingButton = makeButton('btn', '签到');
const missing = setup({ buttons: [missingButton], captchaSignin: true });
missing.window.AKSRTCaptcha = undefined;
// No mount is needed in the harness to verify the missing provider path.
missingButton.click();
await settle();
assert.equal(missingButton.disabled, false);
assert.equal(missing.calls.some((call) => call.endpoint === '/signin'), false);
const cancelledButton = makeButton('btn', '签到');
const cancelled = setup({ buttons: [cancelledButton], captchaSignin: true, captchaRun: Promise.resolve(null) });
const original = cancelledButton.innerHTML;
cancelledButton.click();
await settle();
assert.equal(cancelledButton.innerHTML, original);
assert.equal(cancelledButton.disabled, false);
assert.equal(cancelled.calls.some((call) => call.endpoint === '/signin'), false);
});
test('does not bind the same button twice through repeated auto_fun scans', async () => {
const button = makeButton('btn', '签到');
const h = setup({ buttons: [button] });
await settle();
h.listeners['auto_fun.aksrt-signin']();
assert.equal(button.listeners.click.length, 1);
button.click();
await settle();
assert.equal(h.calls.filter((call) => call.endpoint === '/signin').length, 1);
});