fix(frontend): 卡密兑换通知接入全局 notyf Toast 固定 id 流

- credits.js initCardPass 从 setTip 内联提示迁移为固定 id 三态 notyf 流
  (aksrt-card-pass-toast:load → success/danger 原位更新),
  成功后延迟 800ms 刷新以保证成功 Toast 可见,对齐其余资产操作。
- credits-wallet.php 删除 data-aksrt-card-tip 死元素;credits.css 删除
  .uc-card-pass__tip 死样式。
- 契约:toast-contract 将卡密兑换纳入固定 id 流清单并禁止 tip 回潮;
  card-pass-universal-redeem 新增 Toast 流断言(27/27 通过)。
- 验证:容器内真实抓取 /user/balance/ 确认弹窗正常渲染且无 tip 残留。
This commit is contained in:
2026-09-17 19:28:47 +08:00
parent 64ed47165d
commit 1889c9f048
5 changed files with 28 additions and 26 deletions
@@ -849,13 +849,6 @@ body.uc-modal-open {
justify-content: flex-end;
margin-top: 2px;
}
.uc-card-pass__tip {
margin: 8px 0 0;
font-size: 12px;
color: #059669;
min-height: 1.2em;
}
.uc-card-pass__tip.is-error { color: #b91c1c; }
+6 -13
View File
@@ -286,16 +286,9 @@
});
Array.prototype.forEach.call(roots, function (root) {
var tip = root.querySelector('[data-aksrt-card-tip]');
var busy = false;
resetOperationKeyOnChange(root);
function setTip(text, isError) {
if (!tip) return;
tip.textContent = text || '';
tip.classList.toggle('is-error', !!isError);
}
var btn = root.querySelector('[data-aksrt-card-redeem]');
if (!btn) return;
@@ -308,14 +301,14 @@
var card = cardInput ? String(cardInput.value || '').trim() : '';
// 卡号为选填:是否需要卡号由后端按卡密类型判定并返回错误提示。
if (!password) {
setTip('请输入卡密', true);
notyf('请输入卡密', 'danger');
if (pwInput) pwInput.focus();
return;
}
busy = true;
btn.disabled = true;
setTip('正在兑换…');
notyf('正在兑换…', 'load', '', 'aksrt-card-pass-toast');
var body = { password: password, type: type };
if (card) body.card = card;
@@ -328,7 +321,7 @@
busy = false;
btn.disabled = false;
if (!r.ok || (r.json && r.json.code && !r.json.success)) {
setTip((r.json && r.json.message) || '兑换失败', true);
notyf((r.json && r.json.message) || '兑换失败', 'danger', 5, 'aksrt-card-pass-toast');
return;
}
var data = (r.json && r.json.data) ? r.json.data : r.json;
@@ -338,12 +331,12 @@
if (redeemedType === 'vip' && window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === 'function') {
window.AksrtModalRuntime.invalidate(['vip', 'orders']);
}
setTip((data && data.message) || '兑换成功,正在刷新…');
window.location.reload();
notyf((data && data.message) || '兑换成功,正在刷新…', 'success', 5, 'aksrt-card-pass-toast');
window.setTimeout(function () { window.location.reload(); }, 800);
}).catch(function () {
busy = false;
btn.disabled = false;
setTip('网络错误,请重试', true);
notyf('网络错误,请重试', 'danger', 5, 'aksrt-card-pass-toast');
});
});
});
@@ -125,7 +125,6 @@ $mark = (string) ($charge_view['currency_mark'] ?? '元');
<?php esc_html_e('立即兑换', 'aksrt'); ?>
</button>
</div>
<p class="uc-card-pass__tip" data-aksrt-card-tip></p>
</div>
</div>
<?php endif; ?>
@@ -69,9 +69,25 @@ test('前台唯一兑换入口:页内联块与死文件已删除,JS 不再
assert.doesNotMatch(usersSchemaJs, /asset_points_card_/);
assert.doesNotMatch(chargeCard, /asset_points_card_/);
assert.doesNotMatch(chargeCard, /'pass_charge_desc'/);
// 失去宿主的内联块样式不回潮(弹窗仍在用 __input/__btn/__tip 子元素样式)。
// 失去宿主的内联块样式不回潮(弹窗仍在用 __input/__btn 子元素样式;
// 通知走全局 notyf Toast,__tip 内联提示样式已随迁移删除)。
assert.doesNotMatch(creditsCss, /^\.uc-card-pass\s*\{/m);
assert.doesNotMatch(creditsCss, /uc-card-pass__title|uc-card-pass__desc|\.uc-assets-col > \.uc-card-pass/);
assert.doesNotMatch(creditsCss, /uc-card-pass__title|uc-card-pass__desc|uc-card-pass__tip|\.uc-assets-col > \.uc-card-pass/);
});
test('兑换通知走全局 notyf Toast 固定 id 流,无内联 tip 残留', () => {
assert.match(creditsJs, /notyf\('正在兑换…', 'load', '', 'aksrt-card-pass-toast'\)/);
assert.match(creditsJs, /notyf\([^;]+?'danger', 5, 'aksrt-card-pass-toast'\)/);
assert.match(creditsJs, /notyf\([^;]+?'success', 5, 'aksrt-card-pass-toast'\)/);
// 初始化函数内不得再有 setTip 与 tip 元素查询(initCardPass 段)。
const from = creditsJs.indexOf('function initCardPass(');
const to = creditsJs.indexOf('function initBalanceCharge(', from + 1);
const slice = creditsJs.slice(from, to);
assert.ok(from !== -1 && to !== -1);
assert.doesNotMatch(slice, /setTip|data-aksrt-card-tip/);
assert.doesNotMatch(wallet, /data-aksrt-card-tip/);
// 成功后延迟刷新,保证成功 Toast 可见(与其余资产操作一致)。
assert.match(slice, /notyf\([^;]+?'success', 5, 'aksrt-card-pass-toast'\);[\s\S]{0,120}window\.setTimeout\(function \(\) \{ window\.location\.reload\(\); \}, 800\)/);
});
test('前端:VIP 缓存失效按后端识别类型', () => {
+4 -3
View File
@@ -223,6 +223,7 @@ test('balance center actions use direct fixed-id notyf flows', () => {
const flows = [
[signinController, 'aksrt-balance-signin-toast'],
[section('initCardPass', 'initBalanceCharge'), 'aksrt-card-pass-toast'],
[section('initBalanceCharge', 'initTransfer'), 'aksrt-balance-charge-toast'],
[section('initTransfer', 'initPointsExchange'), 'aksrt-balance-transfer-toast'],
[section('initPointsExchange', 'initWithdraw'), 'aksrt-balance-points-toast'],
@@ -233,7 +234,7 @@ test('balance center actions use direct fixed-id notyf flows', () => {
assert.match(source, new RegExp("notyf\\([^;]+?'load'[^;]+?'" + id + "'\\);"));
assert.match(source, new RegExp("notyf\\([^;]+?'danger'[^;]+?'" + id + "'\\);"));
assert.match(source, new RegExp("notyf\\([^;]+?'success'[^;]+?'" + id + "'\\);"));
assert.doesNotMatch(source, /setTip|data-(?:signin|aksrt-(?:charge|transfer|points-exchange|withdraw))-tip/);
assert.doesNotMatch(source, /setTip|data-(?:signin|aksrt-(?:card|charge|transfer|points-exchange|withdraw))-tip/);
});
assert.match(credits, /notyf\('等待支付结果…', 'load', '', 'aksrt-balance-charge-payment-toast'\)/);
@@ -261,8 +262,8 @@ test('balance center actions use direct fixed-id notyf flows', () => {
assert.match(context, /'signin_signed_today'\s*=>\s*\$signin_signed_today/);
assert.match(shell, /\.uc-header__actions\s*\{[^}]*display:\s*flex;[^}]*gap:\s*6px;/s);
assert.match(shell, /\.uc-header__message-btn\s*\{[^}]*position:\s*relative;/s);
assert.doesNotMatch(renderers, /data-(?:signin|aksrt-(?:charge|transfer|points-exchange|withdraw))-tip/);
assert.doesNotMatch(css, /\.uc-(?:modal|balance-card)__tip/);
assert.doesNotMatch(renderers, /data-(?:signin|aksrt-(?:card|charge|transfer|points-exchange|withdraw))-tip/);
assert.doesNotMatch(css, /\.uc-(?:card-pass|modal|balance-card)__tip/);
});
test('VIP purchase and points exchange use direct fixed-id notyf flows', () => {