feat(sidebar): show user info with three-state UI (guest/member/vip)

Sidebar screen now primarily displays user-related information
and derives three render states from loginStatus + currentUser.isVip:

- guest (notLoggedIn): welcome panel + Sign in / Sign up entries
- member (logged in, no VIP): user info card + Get VIP CTA + Log out
- vip (logged in, VIP): user info card with VIP badge + Manage VIP CTA
  + Log out

Adds UserView.isVip (default false), patches user-machine.toView()
to populate it, and introduces three presentational components
(GuestPanel, UserInfoCard, VipCta) co-located under src/app/sidebar.

Preserves existing UserInit + logout-redirect behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-15 17:52:52 +08:00
parent 1449acd6c9
commit fd5dfe2715
9 changed files with 466 additions and 21 deletions
@@ -47,3 +47,229 @@
align-items: flex-start;
gap: var(--spacing-lg, 16px);
}
/* ============================================================
* 用户信息区
* ============================================================ */
.profileArea {
padding: 0 var(--spacing-md, 12px);
display: flex;
flex-direction: column;
align-items: stretch;
gap: var(--spacing-lg, 16px);
width: 100%;
}
.userInfoCard {
display: flex;
align-items: center;
gap: var(--spacing-md, 12px);
padding: var(--spacing-lg, 16px);
background: var(--color-settings-card-background, #ffffff);
border-radius: var(--radius-lg, 12px);
color: #000;
width: 100%;
box-sizing: border-box;
}
.userInfoAvatar {
border-radius: var(--radius-full, 999px);
object-fit: cover;
flex-shrink: 0;
}
.userInfoAvatarFallback {
width: 64px;
height: 64px;
border-radius: var(--radius-full, 999px);
background: var(--color-settings-row-background, #f2f2f2);
color: #000;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xl, 20px);
font-weight: 600;
flex-shrink: 0;
}
.userInfoText {
display: flex;
flex-direction: column;
gap: var(--spacing-xs, 4px);
flex: 1 1 auto;
min-width: 0;
}
.userInfoNameRow {
display: flex;
align-items: center;
gap: var(--spacing-sm, 8px);
min-width: 0;
}
.userInfoName {
font-size: var(--font-size-lg, 16px);
font-weight: 600;
color: #000;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.userInfoEmail {
font-size: var(--font-size-sm, 14px);
color: var(--color-text-secondary, #666);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.userInfoCoinRow {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs, 4px);
font-size: var(--font-size-sm, 14px);
color: var(--color-text-secondary, #666);
}
.vipBadge {
display: inline-flex;
align-items: center;
padding: 2px var(--spacing-sm, 8px);
border-radius: var(--radius-full, 999px);
background: var(--color-accent, #7c5cff);
color: var(--color-text-primary, #ffffff);
font-size: var(--font-size-xs, 12px);
font-weight: 600;
letter-spacing: 0.5px;
flex-shrink: 0;
}
.vipCta {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-md, 12px) var(--spacing-lg, 16px);
border-radius: var(--radius-full, 999px);
background: linear-gradient(
90deg,
var(--color-button-gradient-start, #7c5cff),
var(--color-button-gradient-end, #b48bff)
);
color: var(--color-text-primary, #ffffff);
font-size: var(--font-size-lg, 16px);
font-weight: 600;
border: 0;
cursor: pointer;
width: 100%;
text-decoration: none;
}
.vipCta:hover {
opacity: 0.9;
}
.vipCta:focus-visible {
outline: 2px solid var(--color-accent, #7c5cff);
outline-offset: 2px;
}
/* ============================================================
* 未登录态 GuestPanel
* ============================================================ */
.guestPanel {
display: flex;
flex-direction: column;
gap: var(--spacing-md, 12px);
padding: var(--spacing-lg, 16px);
background: var(--color-settings-card-background, #ffffff);
border-radius: var(--radius-lg, 12px);
color: #000;
width: 100%;
box-sizing: border-box;
}
.guestTitle {
font-size: var(--font-size-xl, 20px);
font-weight: 600;
color: #000;
margin: 0;
}
.guestSubtitle {
font-size: var(--font-size-sm, 14px);
color: var(--color-text-secondary, #666);
margin: 0;
}
.guestActions {
display: flex;
flex-direction: column;
gap: var(--spacing-sm, 8px);
margin-top: var(--spacing-sm, 8px);
}
.guestPrimaryLink {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-md, 12px) var(--spacing-lg, 16px);
border-radius: var(--radius-full, 999px);
background: linear-gradient(
90deg,
var(--color-button-gradient-start, #7c5cff),
var(--color-button-gradient-end, #b48bff)
);
color: var(--color-text-primary, #ffffff);
font-size: var(--font-size-lg, 16px);
font-weight: 600;
text-decoration: none;
border: 0;
}
.guestPrimaryLink:hover {
opacity: 0.9;
}
.guestSecondaryLink {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-md, 12px) var(--spacing-lg, 16px);
border-radius: var(--radius-full, 999px);
background: transparent;
color: #000;
font-size: var(--font-size-lg, 16px);
font-weight: 600;
text-decoration: none;
border: 1px solid var(--color-auth-border, #d0d0d0);
}
.guestSecondaryLink:hover {
background: var(--color-settings-row-background, #f2f2f2);
}
/* ============================================================
* 骨架占位
* ============================================================ */
.skeleton {
display: block;
background: var(--color-settings-row-background, #e5e5e5);
border-radius: var(--radius-md, 8px);
}
.skeletonMd {
width: 60%;
height: 18px;
}
.skeletonSm {
width: 80%;
height: 14px;
}
.skeletonXs {
width: 40%;
height: 14px;
}