feat(vip): skeleton loading state and faster modal/site startup
The VIP purchase/renew modal opened with a single "loading..." line, which reads as a hang while /pay/vip/modal round-trips. Every frontend pageview also paid an option write inside wp_head, and the header user-card CSS was enqueued even for guests who never see the card. pay-vip.js now builds a full-layout skeleton (user box, level tabs, the two-column package panel) from vipSkeleton(), reusing the global aksrt-modal-runtime__skeleton line/block classes; the VIP-specific shapes live in the conditionally loaded vip-modal.css, and the 640px two-column threshold matches .pay-vip-form-row. The modal cache TTL goes 15s -> 60s (purchase, card-pass redeem and points exchange already invalidate the vip tag), and hover/focus on a .pay-vip trigger now prefetches the payload through the same cacheKey and flight openRemote uses, so a warm click opens instantly. The bottom note and the whole help_text pipeline behind it are removed; the admin-editable pay_user_vip_desc still renders at the top of the modal, and the recharge/gateway help_text keys are untouched. get_catalog() reads points and wallet balance through one new LedgerService::points_and_balance() call. The StatsTracker counter write moves to shutdown so wp_head only does the visitor check and setcookie, and header-userbox.css is enqueued for logged-in users only. Verification: node --test across the suite (628 tests, including the 7 new contract assertions in tests/widget-system/vip-modal-skeleton.test.cjs) and php -l on every changed PHP file. No visual acceptance performed, per request.
This commit is contained in:
1 parent
319ec2f47f
commit
cb76b95b01
9 files changed
+376
-13
No files matched your search
@@ -115,7 +115,6 @@ trait VipServiceModal {
|
||||
'prefer_level' => $prefer_level,
|
||||
'catalog' => [
|
||||
'desc' => (string) ($catalog['desc'] ?? ''),
|
||||
'help_text' => (string) ($catalog['help_text'] ?? ''),
|
||||
'more' => (string) ($vip['more'] ?? ''),
|
||||
'currency_name' => (string) ($catalog['currency_name'] ?? ''),
|
||||
'level_packages' => $groups,
|
||||
|
||||
@@ -232,21 +232,19 @@ trait VipServicePurchase {
|
||||
}
|
||||
}
|
||||
|
||||
$points_balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, \AKSRT\Ledger\AssetType::POINTS) : 0;
|
||||
$wallet_balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id) : '0.00';
|
||||
$balances = \AKSRT\Ledger\LedgerService::get_instance()->points_and_balance($user_id);
|
||||
$methods = $this->available_payment_methods();
|
||||
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
|
||||
$catalog = [
|
||||
'enabled' => !empty($vip['enabled']),
|
||||
'desc' => (string) ($vip['desc'] ?? ''),
|
||||
'help_text' => (string) ($vip['help_text'] ?? ''),
|
||||
'user_level' => $level,
|
||||
'user_expires' => $expires,
|
||||
'user_is_permanent' => $is_permanent,
|
||||
'user_expires_label' => (string) ($member['expires_label'] ?? ''),
|
||||
'user_points' => $points_balance,
|
||||
'wallet_balance' => $wallet_balance,
|
||||
'user_points' => $balances['points'],
|
||||
'wallet_balance' => $balances['balance'],
|
||||
'currency_name' => $currency_name,
|
||||
'renew_enabled' => $renew_enabled,
|
||||
'upgrade_enabled' => $upgrade_enabled,
|
||||
|
||||
@@ -63,7 +63,10 @@ trait ScriptsPart1b {
|
||||
};
|
||||
|
||||
// 顶部导航用户卡片(legacy header-userbox 模块样式)。
|
||||
$enqueue_module('header-userbox');
|
||||
// 仅登录用户渲染用户卡(aksrt-header-userbox.php 的渲染条件),访客页面不加载。
|
||||
if (is_user_logged_in()) {
|
||||
$enqueue_module('header-userbox');
|
||||
}
|
||||
|
||||
if (!is_user_logged_in() || (bool) get_query_var('aksrt_sign')) {
|
||||
$auth_css = get_template_directory() . '/assets/css/routes/sign.css';
|
||||
|
||||
@@ -30,6 +30,9 @@ class StatsTracker {
|
||||
|
||||
/**
|
||||
* 追踪访问
|
||||
*
|
||||
* 访客判定与 setcookie 必须留在 wp_head(响应头尚未发送);
|
||||
* 计数读写改挂 shutdown,避免每次前台请求都在渲染关键路径上写库。
|
||||
*/
|
||||
public function track_visit(): void {
|
||||
// 排除管理员、爬虫、AJAX请求
|
||||
@@ -41,6 +44,12 @@ class StatsTracker {
|
||||
return;
|
||||
}
|
||||
|
||||
static $scheduled = false;
|
||||
if ($scheduled) {
|
||||
return;
|
||||
}
|
||||
$scheduled = true;
|
||||
|
||||
$today = current_time('Y-m-d');
|
||||
$option_key = 'aksrt_daily_stats_' . $today;
|
||||
|
||||
@@ -48,6 +57,19 @@ class StatsTracker {
|
||||
$visitor_cookie = 'aksrt_visitor_' . $today;
|
||||
$is_unique = empty($_COOKIE[$visitor_cookie]);
|
||||
|
||||
if ($is_unique) {
|
||||
setcookie($visitor_cookie, '1', time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
|
||||
}
|
||||
|
||||
add_action('shutdown', static function () use ($option_key, $is_unique): void {
|
||||
self::get_instance()->record_stats($option_key, $is_unique);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计数落库(shutdown 阶段执行;请求中断时本次统计丢弃)。
|
||||
*/
|
||||
public function record_stats(string $option_key, bool $is_unique): void {
|
||||
// 获取或初始化今日统计
|
||||
$stats = get_option($option_key, ['visitors' => 0, 'pageviews' => 0]);
|
||||
|
||||
@@ -59,7 +81,6 @@ class StatsTracker {
|
||||
$stats['pageviews']++;
|
||||
if ($is_unique) {
|
||||
$stats['visitors']++;
|
||||
setcookie($visitor_cookie, '1', time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
|
||||
}
|
||||
|
||||
update_option($option_key, $stats, false);
|
||||
|
||||
@@ -63,6 +63,46 @@ final class LedgerService {
|
||||
return AssetAmount::from_minor(AssetType::BALANCE, $this->balance($user_id, AssetType::BALANCE))->decimal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read points and cash balance together (VIP modal catalog).
|
||||
*
|
||||
* The meta adapter serves both assets from user meta; the asset-account
|
||||
* fallback answers both rows with a single query.
|
||||
*
|
||||
* @return array{points:int,balance:string}
|
||||
*/
|
||||
public function points_and_balance(int $user_id): array {
|
||||
if ($user_id <= 0) {
|
||||
return ['points' => 0, 'balance' => '0.00'];
|
||||
}
|
||||
if (class_exists('\AKSRT\Credits\MetaAssetService')) {
|
||||
$meta = \AKSRT\Credits\MetaAssetService::get_instance();
|
||||
return [
|
||||
'points' => $meta->balance_points($user_id),
|
||||
'balance' => $meta->balance_decimal($user_id),
|
||||
];
|
||||
}
|
||||
global $wpdb;
|
||||
$table = $this->table('asset_account');
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT asset, balance_minor FROM {$table} WHERE user_id = %d AND asset IN (%s, %s)",
|
||||
$user_id,
|
||||
AssetType::POINTS->value,
|
||||
AssetType::BALANCE->value
|
||||
), ARRAY_A);
|
||||
$minor = ['points' => 0, 'balance' => 0];
|
||||
foreach ((array) $rows as $row) {
|
||||
$asset = (string) ($row['asset'] ?? '');
|
||||
if (isset($minor[$asset])) {
|
||||
$minor[$asset] = max(0, (int) ($row['balance_minor'] ?? 0));
|
||||
}
|
||||
}
|
||||
return [
|
||||
'points' => $minor['points'],
|
||||
'balance' => AssetAmount::from_minor(AssetType::BALANCE, $minor['balance'])->decimal(),
|
||||
];
|
||||
}
|
||||
|
||||
public function format_balance(int $minor): string {
|
||||
return AssetAmount::from_minor(AssetType::BALANCE, max(0, $minor))->formatted();
|
||||
}
|
||||
|
||||
@@ -219,7 +219,6 @@ class PaymentRuntime {
|
||||
'badge_text' => $badge,
|
||||
'desc' => $desc,
|
||||
'more' => wp_kses($more, $allowed_equity + ['div' => ['class' => true], 'p' => ['class' => true]]),
|
||||
'help_text' => $desc !== '' ? $desc : __('会员权益以免扣下载和身份展示为主,开通后立即生效。', 'aksrt'),
|
||||
'levels' => [
|
||||
1 => [
|
||||
'enabled' => $vip1_enabled,
|
||||
|
||||
Reference in new issue
Block a user