Merge pull request 'Worktree prd user center sidebar vip entry' (#371) from worktree-prd-user-center-sidebar-vip-entry into main
Reviewed-on: CYL/wordpress-theme-aksrt#371
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# Plan: 用户中心侧栏 VIP 入口卡片
|
||||
|
||||
## Architectural Decisions
|
||||
|
||||
Durable decisions that apply across all phases:
|
||||
|
||||
- **现有会员导航契约**:完整点击卡片复用现有用户中心 `tab=vip` URL;不打开购买弹窗,也不新增客户端标签切换逻辑。
|
||||
- **现有配置决定可见性**:仅当现有 VIP 配置中至少一个等级启用时渲染入口;不新增设置项、管理员例外或不可用占位。
|
||||
- **现有数据决定展示状态**:复用既有 VIP 等级、过期等级和到期状态读取能力及文案语义;不增加数据字段、迁移或新的会员状态计算。
|
||||
- **侧栏组合层决定顺序**:侧栏组合器将独立 VIP 入口渲染器放在统计信息与账户资产之间;渲染器封装卡片状态和输出。
|
||||
- **用户中心样式保持局部**:会员横卡样式限定在用户中心侧栏范围,沿用现有响应式侧栏抽屉,不新增移动端内容区入口。
|
||||
- **单一会员入口位置**:删除资产页中仅作导航用途的 VIP 入口,保留独立“我的会员”页面及其购买、续费、升级、兑换和支付能力。
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — 会员入口可从侧栏进入
|
||||
|
||||
**User stories**: 1、2、3、4、7
|
||||
|
||||
### What to build
|
||||
|
||||
在统计信息和账户资产之间接入独立的服务端 VIP 入口渲染器。入口根据现有配置和当前用户会员状态,展示未开通引导、有效会员等级与既有到期状态,或过期状态;整个卡片通过现有用户中心 URL 进入“我的会员”页面。此阶段先交付完整行为与基础结构,视觉增强和资产页去重留到下一阶段。
|
||||
|
||||
### Done when
|
||||
|
||||
- [x] `inc/helpers/user-center-renderers/profile-sidebar.php` 在 `sidebar-stats.php` 后、`sidebar-commerce.php` 前加载独立的侧栏 VIP 入口渲染器。
|
||||
- [x] 新的侧栏 VIP 入口渲染器复用现有 VIP 配置、会员等级、过期等级和到期状态,并在没有启用任何 VIP 等级时不输出 HTML。
|
||||
- [x] [agent:test-auditor] 人工检查渲染器的四种服务端输出:未开通显示权益引导;有效会员显示等级和既有状态文案;过期会员显示过期状态;VIP 未启用时无入口,并确认未引入新的精确日期格式。
|
||||
- [x] 新入口的完整可点击区域使用 `aksrt_get_user_center_tab_url('vip')` 生成目标地址,人工检查所得 URL 可进入 `tab=vip`。
|
||||
- [x] 运行 `php tools/audit-admin-contracts.php`,命令退出码为 0 且报告为 `errors=0`。
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — 会员横卡完成视觉落地并消除重复入口
|
||||
|
||||
**User stories**: 5、6
|
||||
|
||||
### What to build
|
||||
|
||||
为侧栏 VIP 入口增加与账户资产双卡明显区分的金色或会员感渐变视觉、会员图标、动态状态文本、右侧箭头及交互反馈,并确保其随既有侧栏在移动端抽屉中正确展示。同时删除资产页内容区中重复的 VIP 导航卡,保持余额、积分、签到和记录功能不变。
|
||||
|
||||
### Done when
|
||||
|
||||
- [x] `assets/css/routes/user-center/shell.css` 包含限定在用户中心侧栏的 VIP 卡样式,覆盖会员渐变、图标、状态文本、右侧箭头、完整卡片悬停/聚焦状态和窄屏文本约束。
|
||||
- [x] `inc/helpers/user-center-renderers/profile-credits.php` 不再输出 `uc-vip-assets-entry` 导航卡,文件中原有余额、积分、签到和资产记录组合逻辑仍被保留。
|
||||
- [ ] [agent:test-auditor] 人工在桌面宽度验收“统计信息 → VIP 入口 → 账户资产”的顺序、整卡跳转和 VIP 卡与余额/积分双卡的视觉区分。
|
||||
- [ ] [agent:test-auditor] 人工在既有移动侧栏抽屉验收相同顺序、整卡可点击性、状态文本无溢出,并确认页面内容区没有额外复制 VIP 入口。
|
||||
- [x] 运行 `php tools/audit-admin-contracts.php`,命令退出码为 0 且报告为 `errors=0`。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 修改“我的会员”页面的布局、文案、套餐卡、权益内容或购买流程。
|
||||
- 修改支付弹窗、支付渠道、续费、升级、兑换、卡密兑换或订单逻辑。
|
||||
- 新增、修改或迁移会员数据、会员状态算法、到期日期格式、数据库表或 REST API。
|
||||
- 新增后台会员配置、侧栏开关、管理员例外显示规则或运营统计。
|
||||
- 记录 VIP 入口的曝光、点击或转化数据。
|
||||
- 在移动端内容区额外置顶或复制 VIP 入口。
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。
|
||||
@@ -0,0 +1,95 @@
|
||||
# 用户中心侧栏 VIP 入口卡片
|
||||
|
||||
## Problem Statement
|
||||
|
||||
用户中心侧栏当前在统计信息卡片后直接展示账户资产卡片,会员入口仅存在于资产页内容区。会员入口缺少侧栏层级的持续曝光,并且与参考主题的“统计信息 → VIP 入口 → 账户资产”信息架构不一致;同时,用户中心内重复出现会员入口会造成导航冗余。
|
||||
|
||||
## Current State
|
||||
|
||||
- 用户中心侧栏按“统计信息、账户资产、创作/收益、服务、设置”的顺序组织内容。
|
||||
- 已存在独立的“我的会员”用户中心页面,并使用现有 VIP 等级、会员状态和到期状态能力展示会员信息及承接续费、升级、开通等操作。
|
||||
- 资产页内容区当前已有一个进入“我的会员”页面的 VIP 入口。
|
||||
- 会员功能可由后台 VIP 等级开关控制;未启用任何 VIP 等级时,会员中心不应成为可用入口。
|
||||
- 移动端用户中心侧栏会收纳至既有抽屉中。
|
||||
|
||||
## Solution
|
||||
|
||||
在用户中心侧栏中,统计信息卡片下方、账户资产卡片上方新增一个醒目的 VIP 入口横卡;整体顺序调整为“统计信息 → VIP 入口 → 账户资产”。
|
||||
|
||||
入口采用参考主题的会员横卡视觉方向:金色或会员感渐变背景、会员图标、动态状态文案及右侧箭头。卡片为完整可点击区域,点击后以现有用户中心 URL 进入“我的会员”页面。
|
||||
|
||||
入口仅在至少一个 VIP 等级启用时展示。未启用会员功能时,不渲染入口,也不提供不可用状态或管理员例外。
|
||||
|
||||
卡片根据当前登录用户的既有会员状态展示:
|
||||
|
||||
- 未开通会员:展示开通会员及会员权益引导文案。
|
||||
- 当前有效会员:展示当前会员等级和既有有效期状态文案,例如“VIP1 · 还有 30 天到期”。
|
||||
- 已过期会员:展示当前或最近会员等级及既有“已过期”等状态文案。
|
||||
|
||||
状态文案沿用现有会员状态语义,不新增精确到期日期格式或新的会员状态计算规则。
|
||||
|
||||
移动端不额外创建内容区入口;卡片随现有侧栏收纳至抽屉,并保持与桌面端相同的顺序。
|
||||
|
||||
为消除重复入口,移除账户资产页内容区中现有的 VIP 入口卡片。独立“我的会员”页面、会员套餐展示、购买弹窗、支付流程和会员状态逻辑保持不变。
|
||||
|
||||
## User Stories
|
||||
|
||||
1. 作为未开通会员的登录用户,我可以在用户中心侧栏统计信息下方看到会员入口,了解开通会员可获得权益,并可进入我的会员页面。
|
||||
2. 作为有效会员,我可以在侧栏入口直接看到当前会员等级和既有到期状态提示,并进入我的会员页面进行续费或升级。
|
||||
3. 作为已过期会员,我可以在侧栏入口看到已过期状态,并进入我的会员页面重新开通或续费。
|
||||
4. 作为普通用户,当站点没有启用任何 VIP 等级时,我不会看到失效或不可访问的会员入口。
|
||||
5. 作为移动端用户,我可以在用户中心侧栏抽屉中按“统计信息、VIP 入口、账户资产”的顺序找到会员入口。
|
||||
6. 作为用户,我在账户资产页不再看到重复的会员入口,但原有资产余额、积分、签到及记录功能不受影响。
|
||||
7. 作为用户,我点击 VIP 横卡后会通过现有可分享、可刷新、可返回的用户中心链接进入“我的会员”页面。
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
### New Modules
|
||||
|
||||
- **侧栏 VIP 入口渲染器**:基于既有用户中心渲染上下文,在统计信息与账户资产之间输出一个完整可点击的会员横卡;负责会员功能启用判断、状态展示数据和目标链接的组合。
|
||||
- **侧栏 VIP 入口样式模块**:为会员横卡定义桌面端与抽屉端一致的会员视觉、图标、状态文本、箭头、悬停和窄屏布局;不改变现有统计卡或资产卡的布局职责。
|
||||
|
||||
### Architectural Decisions
|
||||
|
||||
- 复用既有 VIP 启用判断、会员等级解析和到期状态文本;不复制或重新定义会员状态业务逻辑。
|
||||
- 复用现有“我的会员”用户中心目标页和 `tab=vip` 导航方式;不采用未开通即打开购买弹窗的分流行为。
|
||||
- 侧栏顺序由侧栏组合层负责,VIP 入口置于统计信息渲染之后、账户资产渲染之前。
|
||||
- 删除资产页内容区仅作为导航入口的现有 VIP 卡片;不删除独立会员页面及其中的套餐、权益、续费、升级、兑换和支付能力。
|
||||
- 不引入新设置项;可见性完全由现有 VIP 等级启用配置决定。
|
||||
|
||||
### Schema Changes
|
||||
|
||||
无。复用已有会员配置、用户会员状态和到期状态数据。
|
||||
|
||||
### API Contracts
|
||||
|
||||
无。入口仅使用既有用户中心页面路由和现有服务端渲染数据。
|
||||
|
||||
### Navigation
|
||||
|
||||
- 桌面端:用户中心侧栏展示“统计信息 → VIP 入口 → 账户资产”。
|
||||
- 移动端:上述顺序在既有用户中心侧栏抽屉中保持一致。
|
||||
- 点击 VIP 入口:进入现有用户中心“我的会员”目标页(`tab=vip`)。
|
||||
- 账户资产页:移除其内容区内已有的 VIP 导航入口,避免重复。
|
||||
|
||||
## Testing Decisions
|
||||
|
||||
采用静态契约审计与人工响应式验收。
|
||||
|
||||
- 运行现有管理员契约审计,确保本次模板与样式调整没有引入管理端契约回归。
|
||||
- 人工验收未开通会员:卡片显示权益引导文案,点击进入我的会员页面。
|
||||
- 人工验收有效会员:卡片显示会员等级及既有剩余有效期状态文案。
|
||||
- 人工验收过期会员:卡片显示既有过期状态文案,跳转仍可用。
|
||||
- 人工验收未启用任何 VIP 等级:侧栏不显示 VIP 入口。
|
||||
- 人工验收桌面端顺序:统计信息、VIP 入口、账户资产依次显示,会员横卡视觉与资产双卡区分明显。
|
||||
- 人工验收移动端:入口随侧栏进入抽屉,顺序、可点击性和窄屏样式正确。
|
||||
- 人工验收资产页:原 VIP 入口已移除,余额、积分及其他资产功能仍正常。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 修改“我的会员”页面的布局、文案、套餐卡、权益内容或购买流程。
|
||||
- 修改支付弹窗、支付渠道、续费、升级、兑换、卡密兑换或订单逻辑。
|
||||
- 新增、修改或迁移会员数据、会员状态算法、到期日期格式、数据库表或 REST API。
|
||||
- 新增后台会员配置、侧栏开关、管理员例外显示规则或运营统计。
|
||||
- 记录 VIP 入口的曝光、点击或转化数据。
|
||||
- 在移动端内容区额外置顶或复制 VIP 入口。
|
||||
@@ -450,6 +450,123 @@ body.aksrt-user-center .uc-content,
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* VIP 入口:琥珀会员券,与下方中性资产卡形成主次层级 */
|
||||
.uc-sidebar .uc-sidebar-vip {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 13px 14px;
|
||||
border: 1px solid rgba(255, 232, 177, 0.34);
|
||||
border-radius: var(--main-radius, var(--radius, 8px));
|
||||
background:
|
||||
radial-gradient(circle at 88% 12%, rgba(255, 239, 190, 0.2), transparent 34%),
|
||||
linear-gradient(122deg, #4a2b13 0%, #7c4a1c 48%, #a86d28 100%);
|
||||
box-shadow: 0 7px 18px rgba(91, 50, 15, 0.18);
|
||||
color: #fff8e8;
|
||||
text-decoration: none;
|
||||
isolation: isolate;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.uc-sidebar .uc-sidebar-vip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: -80%;
|
||||
left: 58%;
|
||||
width: 34px;
|
||||
height: 260%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 246, 211, 0.14), transparent);
|
||||
transform: rotate(24deg);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.uc-sidebar-vip__icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border: 1px solid rgba(255, 240, 197, 0.46);
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 244, 211, 0.12);
|
||||
box-shadow: inset 0 0 0 4px rgba(255, 244, 211, 0.05);
|
||||
color: #ffd982;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.uc-sidebar-vip__content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.uc-sidebar-vip__content strong,
|
||||
.uc-sidebar-vip__content small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.uc-sidebar-vip__content strong {
|
||||
color: #fffaf0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.uc-sidebar-vip__content small {
|
||||
margin-top: 3px;
|
||||
color: rgba(255, 244, 216, 0.76);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.uc-sidebar-vip__arrow {
|
||||
color: rgba(255, 244, 216, 0.72);
|
||||
font-size: 12px;
|
||||
transition: transform 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.uc-sidebar .uc-sidebar-vip:hover {
|
||||
border-color: rgba(255, 235, 180, 0.58);
|
||||
box-shadow: 0 10px 24px rgba(91, 50, 15, 0.25);
|
||||
color: #fff8e8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.uc-sidebar .uc-sidebar-vip:hover .uc-sidebar-vip__arrow {
|
||||
color: #fff4d8;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.uc-sidebar .uc-sidebar-vip:focus-visible {
|
||||
outline: 3px solid rgba(244, 180, 67, 0.42);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.uc-sidebar .uc-sidebar-vip {
|
||||
margin-right: 12px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.uc-sidebar .uc-sidebar-vip,
|
||||
.uc-sidebar-vip__arrow {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.uc-sidebar .uc-sidebar-vip:hover,
|
||||
.uc-sidebar .uc-sidebar-vip:hover .uc-sidebar-vip__arrow {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.uc-service-grid__icon small {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
|
||||
@@ -38,17 +38,6 @@ if ($show_wallet || $show_credits) :
|
||||
<?php
|
||||
endif;
|
||||
|
||||
if (!empty($show_vip)) {
|
||||
// Slim entry to dedicated VIP tab (full shop lives under tab=vip).
|
||||
$vip_url = function_exists('aksrt_get_user_center_tab_url')
|
||||
? aksrt_get_user_center_tab_url('vip')
|
||||
: '#';
|
||||
echo '<div class="zib-widget flex ac jsb padding-15 mb15 uc-vip-assets-entry">';
|
||||
echo '<div class="flex ac gap10"><i class="fas fa-crown c-yellow"></i><div><div class="font-bold">' . esc_html__('我的会员', 'aksrt') . '</div>';
|
||||
echo '<div class="muted-2-color em09">' . esc_html__('查看权益、续费与升级', 'aksrt') . '</div></div></div>';
|
||||
echo '<a class="but jb-yellow radius" href="' . esc_url($vip_url) . '">' . esc_html__('进入会员中心', 'aksrt') . '</a>';
|
||||
echo '</div>';
|
||||
}
|
||||
if ($show_credits) {
|
||||
require $dir . '/credits-signin.php';
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ $context = is_array($args['context'] ?? null) ? $args['context'] : [];
|
||||
extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
|
||||
|
||||
require __DIR__ . '/sidebar-stats.php';
|
||||
require __DIR__ . '/sidebar-vip.php';
|
||||
require __DIR__ . '/sidebar-commerce.php';
|
||||
require __DIR__ . '/sidebar-creator.php';
|
||||
require __DIR__ . '/sidebar-nav.php';
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* User center sidebar: VIP entry.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
$vip_settings = is_array($vip_settings ?? null) ? $vip_settings : [];
|
||||
$vip_levels = is_array($vip_settings['levels'] ?? null) ? $vip_settings['levels'] : [];
|
||||
$vip_enabled = !empty($vip_settings['enabled'])
|
||||
&& (!empty($vip_levels[1]['enabled']) || !empty($vip_levels[2]['enabled']));
|
||||
if (!$vip_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = (int) ($user_id ?? get_current_user_id());
|
||||
$vip_level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0;
|
||||
$expired_level = (int) get_user_meta($user_id, 'aksrt_vip_level_expired', true);
|
||||
$expires = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
|
||||
$vip_url = aksrt_get_user_center_tab_url('vip');
|
||||
|
||||
$title = __('开通会员', 'aksrt');
|
||||
$status = (string) ($vip_settings['desc'] ?? '');
|
||||
if ($status === '') {
|
||||
$status = __('尊享会员权益', 'aksrt');
|
||||
}
|
||||
|
||||
if ($vip_level > 0) {
|
||||
$title = (string) ($vip_levels[$vip_level]['name'] ?? ('VIP' . $vip_level));
|
||||
if ($expires <= 0) {
|
||||
$status = __('永久会员', 'aksrt');
|
||||
} else {
|
||||
$remaining_days = max(1, (int) ceil(($expires - time()) / DAY_IN_SECONDS));
|
||||
$status = sprintf(
|
||||
/* translators: %d: remaining membership days */
|
||||
__('还有 %d 天到期', 'aksrt'),
|
||||
$remaining_days
|
||||
);
|
||||
}
|
||||
} elseif ($expired_level > 0) {
|
||||
$title = (string) ($vip_levels[$expired_level]['name'] ?? ('VIP' . $expired_level));
|
||||
$status = __('已过期', 'aksrt');
|
||||
}
|
||||
?>
|
||||
<a class="uc-sidebar-vip" href="<?php echo esc_url($vip_url); ?>">
|
||||
<span class="uc-sidebar-vip__icon"><i class="fas fa-crown" aria-hidden="true"></i></span>
|
||||
<span class="uc-sidebar-vip__content">
|
||||
<strong><?php echo esc_html($title); ?></strong>
|
||||
<small><?php echo esc_html($status); ?></small>
|
||||
</span>
|
||||
<i class="fas fa-angle-right uc-sidebar-vip__arrow" aria-hidden="true"></i>
|
||||
</a>
|
||||
Reference in New Issue
Block a user