fix(ui): nav CTA icon alignment and always show UC VIP menu

This commit is contained in:
2026-07-21 17:53:34 +08:00
parent 3e58eecbc5
commit deff20532f
8 changed files with 257 additions and 78 deletions
@@ -4,7 +4,7 @@ export const featuresSchema = {
icon: 'fas fa-user-cog',
description: '配置用户中心和个人主页的功能模块。',
defaults: {
user_center_tabs: 'profile,posts,comments,credits,favorites',
user_center_tabs: 'profile,posts,comments,favorites,vip,credits',
user_avatar_upload: true,
user_cover_upload: true,
user_social_links: true,
@@ -25,7 +25,7 @@ export const featuresSchema = {
title: '用户中心',
icon: 'fas fa-user-shield',
fields: [
{ key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,posts,comments,credits,favorites', help: '逗号分隔' },
{ key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,posts,comments,favorites,vip,credits', help: '逗号分隔。vip=我的会员(会员功能开启时也会自动注入)' },
{ key: 'user_avatar_upload', label: '头像上传', type: 'switch', default: true },
{ key: 'user_cover_upload', label: '封面上传', type: 'switch', default: true },
{ key: 'user_social_links', label: '社交链接', type: 'switch', default: true },
+109 -7
View File
@@ -8,44 +8,97 @@
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
min-width: 0;
}
.navbar-but-cluster .navbar-but,
.navbar-but-cluster .navbar-vip {
display: inline-flex;
align-items: center;
min-width: 0;
}
/* CTA button base: flex icon + text, never stack or clip glyphs */
.navbar-but-cluster .navbar-cta,
.navbar-but-cluster .btn-primary,
.navbar-but-cluster .but.btn-primary,
.navbar-but-cluster .navbar-newposts.but,
.navbar-but-cluster a.but {
display: inline-flex;
display: inline-flex !important;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 14px;
border-radius: 20px;
border-radius: var(--navbar-cta-radius, 20px);
background: var(--color-primary, #2f6df6);
color: #fff;
text-decoration: none;
border: 0;
font-size: 13px;
line-height: 1.4;
line-height: 1.2;
white-space: nowrap;
box-sizing: border-box;
vertical-align: middle;
}
.navbar-but-cluster .navbar-cta > i,
.navbar-but-cluster a.but > i,
.navbar-but-cluster .btn-primary > i {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1em;
height: 1em;
margin: 0 !important; /* kill mr3/ml10 utility drift on icons */
flex: 0 0 auto;
line-height: 1;
font-size: 1em;
}
.navbar-but-cluster .navbar-cta-text,
.navbar-but-cluster .navbar-cta > span:not(.badge) {
display: inline-block;
line-height: 1.2;
white-space: nowrap;
}
.navbar-but-cluster .payvip-icon.but {
.navbar-but-cluster .payvip-icon.but,
.navbar-but-cluster a.payvip-icon {
background: linear-gradient(90deg, #ff6b6b, #f04494);
color: #fff;
padding: 6px 12px;
border-radius: 20px;
border-radius: var(--navbar-cta-radius, 20px);
text-decoration: none;
font-size: 13px;
}
.navbar-but-cluster .ml10 {
margin-left: 0; /* spacing handled by cluster gap */
}
.navbar-but-cluster .theme-toggle-desktop,
.navbar-but-cluster .toggle-theme {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
padding: 0;
border-radius: 999px;
flex: 0 0 auto;
}
.navbar-but-cluster .theme-toggle-desktop > i,
.navbar-but-cluster .toggle-theme > i {
margin: 0 !important;
line-height: 1;
}
.newadd-btns.hover-show {
position: relative;
display: inline-block;
display: inline-flex;
align-items: center;
}
.newadd-btns .drop-newadd,
@@ -54,7 +107,7 @@
position: absolute;
top: calc(100% + 6px);
right: 0;
min-width: 140px;
min-width: 148px;
background: var(--main-bg-color, #fff);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
@@ -78,11 +131,43 @@
white-space: nowrap;
}
.newadd-btns__item > i {
width: 1em;
margin: 0;
text-align: center;
flex: 0 0 auto;
}
.newadd-btns__item:hover {
background: var(--surface-2, rgba(0, 0, 0, 0.04));
color: var(--focus-color, var(--color-primary));
}
/* Header VIP hover cards */
.navbar-vip.hover-show {
position: relative;
}
.navbar-vip .sub-vip-card {
display: none;
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 60;
width: min(420px, 90vw);
padding: 10px;
border-radius: 12px;
background: var(--main-bg-color, #fff);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.16);
gap: 10px;
flex-wrap: wrap;
}
.navbar-vip.hover-show:hover .sub-vip-card,
.navbar-vip.hover-show:focus-within .sub-vip-card {
display: flex;
}
/* float commerce */
.float-btn-shop-cart {
position: relative;
@@ -112,3 +197,20 @@
.float-btn.pay-vip {
color: #f5a623;
}
@media (max-width: 768px) {
.navbar-but-cluster .navbar-cta-text {
/* keep text on tablet; hide only on very narrow if needed */
}
}
@media (max-width: 520px) {
.navbar-but-cluster {
gap: 6px;
}
.navbar-but-cluster .navbar-cta,
.navbar-but-cluster a.but {
padding: 6px 10px;
font-size: 12px;
}
}
+67 -19
View File
@@ -65,7 +65,8 @@ if (!function_exists('aksrt_get_new_add_actions')) :
'type' => 'post',
'url' => $url,
'label' => __('写文章', 'aksrt'),
'icon' => 'fa fa-file-text-o',
// FA5 free solid names (avoid FA4 aliases like file-text-o).
'icon' => 'fas fa-file-alt',
];
} elseif ($type === 'topic') {
$url = function_exists('aksrt_get_forum_compose_url') ? aksrt_get_forum_compose_url() : '';
@@ -76,25 +77,21 @@ if (!function_exists('aksrt_get_new_add_actions')) :
'type' => 'topic',
'url' => $url,
'label' => __('发帖', 'aksrt'),
'icon' => 'fa fa-comments-o',
'icon' => 'fas fa-comments',
];
} elseif ($type === 'product') {
// Prefer front product edit page when registered.
$url = '';
if (function_exists('aksrt_get_product_edit_url')) {
$url = (string) aksrt_get_product_edit_url();
}
if ($url === '') {
// Fallback: product-edit page template lookup is optional.
$url = home_url('/?page_id=0');
// Use admin product CPT if available.
$url = admin_url('post-new.php?post_type=aksrt_product');
}
$out[] = [
'type' => 'product',
'url' => $url,
'label' => __('发布商品', 'aksrt'),
'icon' => 'fa fa-shopping-bag',
'icon' => 'fas fa-shopping-bag',
];
}
}
@@ -102,6 +99,49 @@ if (!function_exists('aksrt_get_new_add_actions')) :
}
endif;
if (!function_exists('aksrt_normalize_header_cta_label')) :
/**
* Normalize CTA label HTML: map FA4 classes and keep a single leading icon.
*/
function aksrt_normalize_header_cta_label(string $label, string $fallback_icon = 'fas fa-pen'): string {
$label = trim($label);
if ($label === '') {
return '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i> ' . esc_html__('发布', 'aksrt');
}
// Common FA4 → FA5 renames used in admin defaults.
$replacements = [
'fa-file-text-o' => 'fa-file-alt',
'fa-file-text' => 'fa-file-alt',
'fa-pencil-square-o' => 'fa-edit',
'fa-pencil' => 'fa-pen',
'fa-comments-o' => 'fa-comments',
'fa-clock-o' => 'fa-clock',
'fa-calendar-check-o'=> 'fa-calendar-check',
'fa-diamond' => 'fa-gem',
];
$label = strtr($label, $replacements);
// Ensure icon uses fas (FA5 solid) when only bare "fa " is present.
$label = preg_replace('/\bclass=(["\'])fa\s+/', 'class=$1fas ', $label) ?? $label;
$label = preg_replace('/<i\s+class=(["\'])(?!fas\b|far\b|fab\b)/', '<i class=$1fas ', $label) ?? $label;
// If label is plain text without icon markup, prefix default icon.
if (stripos($label, '<i') === false) {
$label = '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i> <span class="navbar-cta-text">' . esc_html(wp_strip_all_tags($label)) . '</span>';
} else {
// Wrap remaining text nodes for flex alignment without raw text siblings.
$label = preg_replace(
'/(<\/i>)\s*([^<].*)$/u',
'$1 <span class="navbar-cta-text">$2</span>',
$label
) ?? $label;
}
return $label;
}
endif;
if (!function_exists('aksrt_render_new_add_btns')) :
/**
* Multi-type publish button (zibll newadd-btns).
@@ -116,22 +156,26 @@ if (!function_exists('aksrt_render_new_add_btns')) :
if ($label === '') {
$label = __('发布', 'aksrt');
}
$label_html = aksrt_normalize_header_cta_label((string) $label, 'fas fa-pen');
$class = trim('newadd-btns ' . $class);
if (count($actions) === 1) {
$a = $actions[0];
return '<a rel="nofollow" class="' . esc_attr(trim($class . ' but')) . '" href="' . esc_url($a['url']) . '">'
. '<i class="' . esc_attr($a['icon']) . '" aria-hidden="true"></i> '
. wp_kses_post($label)
// Prefer configured label; icon already embedded after normalize.
return '<a rel="nofollow" class="' . esc_attr(trim($class . ' but navbar-cta')) . '" href="' . esc_url($a['url']) . '">'
. $label_html
. '</a>';
}
$html = '<span class="' . esc_attr($class) . ' hover-show">';
$html .= '<a href="javascript:;" class="but newadd-btns__toggle" rel="nofollow">' . wp_kses_post($label) . ' <i class="fa fa-caret-down" aria-hidden="true"></i></a>';
$html = '<span class="' . esc_attr($class . ' hover-show') . '">';
$html .= '<a href="javascript:;" class="but newadd-btns__toggle navbar-cta" rel="nofollow">'
. $label_html
. ' <i class="fas fa-caret-down" aria-hidden="true"></i></a>';
$html .= '<div class="hover-show-con dropdown-menu drop-newadd">';
foreach ($actions as $a) {
$html .= '<a rel="nofollow" class="newadd-btns__item" href="' . esc_url($a['url']) . '">';
$html .= '<i class="' . esc_attr($a['icon']) . '" aria-hidden="true"></i> ' . esc_html($a['label']);
$html .= '<i class="' . esc_attr($a['icon']) . '" aria-hidden="true"></i> '
. '<span>' . esc_html($a['label']) . '</span>';
$html .= '</a>';
}
$html .= '</div></span>';
@@ -152,12 +196,12 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
$class .= ' ' . $cfg['class'];
}
$radius = (int) ($cfg['radius'] ?? 20);
$style = $radius > 0 ? ' style="border-radius:' . $radius . 'px"' : '';
// Radius applied via CSS class variable to avoid style attribute overriding flex.
$radius_attr = $radius > 0 ? ' data-radius="' . esc_attr((string) $radius) . '" style="--navbar-cta-radius:' . $radius . 'px"' : '';
$label = (string) ($cfg['text'] ?? __('发布', 'aksrt'));
// Strip outer <i> if present in settings HTML text — keep simple.
$btn = aksrt_render_new_add_btns($cfg['types'] ?? ['post'], $class, $label);
if ($btn !== '') {
$html .= '<div class="navbar-form navbar-right navbar-but"' . $style . '>' . $btn . '</div>';
$html .= '<div class="navbar-form navbar-right navbar-but"' . $radius_attr . '>' . $btn . '</div>';
}
}
@@ -186,10 +230,14 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
}
}
$html .= '<div class="navbar-form navbar-right navbar-vip hover-show inline-block">';
$html .= '<a class="but jb-red radius payvip-icon ml10 ' . esc_attr($cls) . '" href="javascript:;"' . ($user_id > 0 ? ' vip-level="1"' : '') . '>';
$html .= '<i class="fa fa-diamond em12 mr3" aria-hidden="true"></i>' . esc_html__('开通会员', 'aksrt');
// FA5: diamond → gem. Keep label in span for flex alignment.
$html .= '<a class="but jb-red radius payvip-icon navbar-cta ml10 ' . esc_attr($cls) . '" href="javascript:;"'
. ($user_id > 0 ? ' vip-level="1"' : '') . '>';
$html .= '<i class="fas fa-gem" aria-hidden="true"></i>'
. '<span class="navbar-cta-text">' . esc_html__('开通会员', 'aksrt') . '</span>';
$html .= '</a>' . $hover . '</div>';
} }
}
}
return $html;
}
+6 -6
View File
@@ -54,14 +54,14 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
if ($signed) {
return '<button type="button" class="but block c-yellow aksrt-header-checkin is-done" disabled>'
. '<i class="fa fa-calendar-check-o" aria-hidden="true"></i> '
. '<i class="fas fa-calendar-check" aria-hidden="true"></i> '
. esc_html__('今日已签到', 'aksrt')
. '</button>'
. $details;
}
return '<button type="button" class="but block c-yellow aksrt-header-checkin" data-aksrt-checkin="1">'
. '<i class="fa fa-calendar-check-o" aria-hidden="true"></i> '
. '<i class="fas fa-calendar-check" aria-hidden="true"></i> '
. esc_html__('签到领取今日奖励', 'aksrt')
. '</button>'
. $details;
@@ -142,10 +142,10 @@ if (!function_exists('aksrt_header_payvip_bar')) :
}
$html = '<div class="mt10 em09 aksrt-header-payvip">';
$html .= '<span class="radius4 payvip-icon btn-block text-center vipbg-v2 level-' . (int) $level . '">';
$html .= '<i class="fa fa-diamond em12 mr6" aria-hidden="true"></i>';
$html .= '<i class="fas fa-gem" aria-hidden="true"></i>';
$html .= '<span>' . esc_html($name) . '</span>';
if ($exp_label !== '') {
$html .= '<span class="ml10 badg jb-yellow vip-expdate-tag"><i class="fa fa-clock-o mr3" aria-hidden="true"></i>'
$html .= '<span class="ml10 badg jb-yellow vip-expdate-tag"><i class="fas fa-clock" aria-hidden="true"></i>'
. esc_html($exp_label) . '</span>';
}
$html .= '</span></div>';
@@ -153,8 +153,8 @@ if (!function_exists('aksrt_header_payvip_bar')) :
}
return '<div class="mt10 em09 aksrt-header-payvip">'
. '<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;" vip-level="1">'
. '<i class="fa fa-diamond mr6" aria-hidden="true"></i>'
. '<a class="pay-vip but radius4 payvip-icon btn-block navbar-cta" href="javascript:;" vip-level="1">'
. '<i class="fas fa-gem" aria-hidden="true"></i>'
. esc_html__('开通会员 尊享会员权益', 'aksrt')
. '</a></div>';
}
+1 -1
View File
@@ -515,7 +515,7 @@ class SchemaUsers {
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'center', 'title' => '用户中心', 'fields' => [
['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,posts,comments,credits,favorites'],
['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,posts,comments,favorites,vip,credits', 'help' => '逗号分隔。vip=我的会员(会员功能开启时也会自动注入)'],
['key' => 'user_avatar_upload', 'label' => '头像上传', 'type' => 'switch', 'default' => true],
['key' => 'user_cover_upload', 'label' => '封面上传', 'type' => 'switch', 'default' => true],
['key' => 'user_social_links', 'label' => '社交链接', 'type' => 'switch', 'default' => true],
+1 -1
View File
@@ -57,7 +57,7 @@ trait VipServiceModal {
$equity = (string) ($levels[$level]['equity'] ?? '');
$cls = $user_id > 0 ? 'pay-vip' : 'signin-loader';
$html = '<div class="vip-card pointer level-' . $level . ' vip-theme' . $level . ' ' . esc_attr($cls) . '" vip-level="' . $level . '">';
$html .= '<div class="abs-center vip-baicon"><i class="fa fa-diamond" aria-hidden="true"></i></div>';
$html .= '<div class="abs-center vip-baicon"><i class="fas fa-gem" aria-hidden="true"></i></div>';
$html .= '<div class="relative">';
$html .= '<div class="vip-name font-bold">' . esc_html($name) . '</div>';
if ($desc !== '') {
+25 -1
View File
@@ -88,7 +88,9 @@ class UserRuntime {
? $raw_tabs
: preg_split('/[\s,]+/', (string) $raw_tabs, -1, PREG_SPLIT_NO_EMPTY);
$allowed_tabs = ['profile', 'posts', 'comments', 'favorites', 'credits'];
// Built-in switchable panels. VIP is always auto-injected when enabled
// by AKSRT\User\Center — keep it allowed for explicit config too.
$allowed_tabs = ['profile', 'posts', 'comments', 'favorites', 'credits', 'vip', 'settings'];
$normalized_tabs = ['profile'];
foreach ((array) $tabs as $tab) {
@@ -106,6 +108,28 @@ class UserRuntime {
$normalized_tabs[] = 'posts';
}
// Ensure VIP appears in configured tabs when membership is on, even if
// the stored option string is older and omits it.
$vip_on = !empty(Settings::get_payment_vip_settings()['enabled']);
if ($vip_on && !in_array('vip', $normalized_tabs, true)) {
// Prefer after credits, else after favorites, else append before settings.
$insert_at = count($normalized_tabs);
foreach (['credits', 'favorites', 'posts'] as $anchor) {
$idx = array_search($anchor, $normalized_tabs, true);
if ($idx !== false) {
$insert_at = (int) $idx + 1;
break;
}
}
array_splice($normalized_tabs, $insert_at, 0, ['vip']);
}
if (!$vip_on) {
$normalized_tabs = array_values(array_filter(
$normalized_tabs,
static fn(string $t): bool => $t !== 'vip'
));
}
return [
'tabs' => $normalized_tabs,
'avatar_upload' => !isset($settings['user_avatar_upload']) || !empty($settings['user_avatar_upload']),
+46 -41
View File
@@ -65,6 +65,12 @@ class Center {
'priority' => 40,
'type' => 'panel',
],
'vip' => [
'label' => __('我的会员', 'aksrt'),
'icon' => 'crown',
'priority' => 45,
'type' => 'panel',
],
'credits' => [
'label' => class_exists('\AKSRT\Settings\Settings')
? sprintf(__('我的%s', 'aksrt'), \AKSRT\Settings\Settings::get_currency_name())
@@ -73,12 +79,6 @@ class Center {
'priority' => 50,
'type' => 'panel',
],
'vip' => [
'label' => __('我的会员', 'aksrt'),
'icon' => 'crown',
'priority' => 45,
'type' => 'panel',
],
'notif' => [
'label' => __('消息通知', 'aksrt'),
'icon' => 'bell',
@@ -94,50 +94,55 @@ class Center {
];
$this->panels = [];
foreach ((array) ($settings['tabs'] ?? []) as $key) {
if ($key === 'credits') {
$credits_on = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled();
$balance_on = class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled();
if (!$credits_on && !$balance_on) {
continue;
}
if ($balance_on) {
$definitions['credits']['label'] = __('我的资产', 'aksrt');
$definitions['credits']['icon'] = 'wallet';
}
}
if (isset($definitions[$key])) {
$this->panels[$key] = $definitions[$key];
}
$configured = array_values(array_filter(array_map('sanitize_key', (array) ($settings['tabs'] ?? []))));
if ($configured === []) {
$configured = ['profile', 'posts', 'comments', 'favorites'];
}
// Ensure assets panel when wallet and/or credits enabled (even if tabs list omitted credits).
$credits_on = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled();
$balance_on = class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled();
if (!isset($this->panels['credits']) && ($credits_on || $balance_on)) {
$panel = $definitions['credits'];
if ($balance_on) {
$vip_on = class_exists('\AKSRT\Settings\Settings')
&& !empty(\AKSRT\Settings\Settings::get_payment_vip_settings()['enabled']);
// Follow configured order; always auto-include VIP/credits when feature on.
$order = $configured;
if ($vip_on && !in_array('vip', $order, true)) {
$insert_at = count($order);
foreach (['credits', 'favorites', 'posts'] as $anchor) {
$idx = array_search($anchor, $order, true);
if ($idx !== false) {
$insert_at = (int) $idx + 1;
break;
}
}
array_splice($order, $insert_at, 0, ['vip']);
}
if (($credits_on || $balance_on) && !in_array('credits', $order, true)) {
$order[] = 'credits';
}
if (!in_array('settings', $order, true)) {
$order[] = 'settings';
}
foreach ($order as $key) {
if (!isset($definitions[$key])) {
continue;
}
if ($key === 'credits' && !$credits_on && !$balance_on) {
continue;
}
if ($key === 'vip' && !$vip_on) {
continue;
}
$panel = $definitions[$key];
if ($key === 'credits' && $balance_on) {
$panel['label'] = __('我的资产', 'aksrt');
$panel['icon'] = 'wallet';
}
$this->panels['credits'] = $panel;
}
// Dedicated VIP tab when membership is enabled.
$vip_on = class_exists('\AKSRT\Settings\Settings')
&& !empty(\AKSRT\Settings\Settings::get_payment_vip_settings()['enabled']);
if ($vip_on && !isset($this->panels['vip'])) {
$this->panels['vip'] = $definitions['vip'];
} elseif (!$vip_on && isset($this->panels['vip'])) {
unset($this->panels['vip']);
}
if (!isset($this->panels['settings'])) {
$this->panels['settings'] = $definitions['settings'];
$this->panels[$key] = $panel;
}
// Notif is a link entry (messages page), not a dashboard panel body.
$this->panels['notif'] = $definitions['notif'];
// Keep stable order by priority.