feat: 优化页面 UI 以及支持个人中心 练习足迹

This commit is contained in:
richarjiang
2026-09-08 11:26:46 +08:00
parent 87d946adb5
commit 301f9ae385
9 changed files with 347 additions and 411 deletions

View File

@@ -10,7 +10,6 @@
<template v-else-if="detail">
<view class="hero">
<view class="hero-grain" />
<view class="hero-inner">
<view class="hero-avatar">
<image v-if="detail.user.avatarUrl" class="avatar-img" :src="detail.user.avatarUrl" mode="aspectFill" />
@@ -19,12 +18,15 @@
</view>
</view>
<view class="hero-copy">
<text class="hero-kicker">STUDIO LEDGER</text>
<text class="hero-name">{{ detail.user.nickname || '未知用户' }}</text>
<text class="hero-phone">{{ detail.user.phone || '未绑定手机' }}</text>
<text class="hero-openid" @tap="copyOpenid">{{ detail.user.openid }}</text>
</view>
</view>
<view v-if="detail.user.openid" class="member-id" @tap="copyOpenid">
<text class="member-id-label">微信标识</text>
<text class="member-id-value">{{ detail.user.openid }}</text>
<text class="member-id-copy">复制</text>
</view>
<view class="time-pair">
<view class="time-cell">
<text class="time-label">注册时间</text>
@@ -38,14 +40,14 @@
</view>
</view>
<view class="section">
<view class="section section--practice">
<text class="section-label">上课情况</text>
<view class="stats-strip">
<view class="stat">
<text class="stat-num">{{ detail.stats.totalBookings }}</text>
<text class="stat-name">累计预约</text>
</view>
<view class="stat">
<view class="stat stat--completed">
<text class="stat-num">{{ detail.stats.completedBookings }}</text>
<text class="stat-name">已完成</text>
</view>
@@ -53,19 +55,24 @@
<text class="stat-num">{{ detail.stats.cancelledBookings }}</text>
<text class="stat-name">已取消</text>
</view>
<view class="stat">
<text class="stat-num">{{ detail.stats.noShowBookings }}</text>
<text class="stat-name">未到课</text>
</view>
</view>
<text class="noshow-hint">未到 {{ detail.stats.noShowBookings }} </text>
</view>
<view class="section">
<text class="section-label">会员卡</text>
<view class="section-heading">
<text class="section-label">会员卡</text>
<text class="section-note">{{ detail.memberships.length }} </text>
</view>
<view v-if="detail.memberships.length" class="card-list">
<view
v-for="card in detail.memberships"
:key="card.id"
class="mship"
>
<view class="mship-strip" :class="getCardGradientClass(card.cardType.type)" />
<view class="mship-head">
<view class="mship-titles">
<text class="mship-name">{{ card.cardType.name }}</text>
@@ -77,7 +84,11 @@
</view>
<view v-if="card.remainingTimes !== null" class="mship-times">
<text class="mship-times-num">{{ card.remainingTimes }}</text>
<text class="mship-times-unit">剩余</text>
<text class="mship-times-unit">可用</text>
</view>
<view v-else class="mship-duration">
<text class="mship-duration-title">有效期内使用</text>
<text class="mship-times-unit">不限次数</text>
</view>
<view v-if="card.remainingTimes !== null && getMembershipTotalTimes(card)" class="progress">
<view class="progress-bar">
@@ -103,11 +114,14 @@
</view>
<view class="section section--last">
<text class="section-label">即将上课</text>
<view class="section-heading">
<text class="section-label">即将上课</text>
<text v-if="detail.upcomingBookings.length" class="section-note">{{ detail.upcomingBookings.length }} 节待上</text>
</view>
<view v-if="detail.upcomingBookings.length" class="upcoming-list">
<view v-for="item in detail.upcomingBookings" :key="item.id" class="upcoming-row">
<view class="upcoming-time">
<text class="upcoming-date">{{ item.date.slice(5) }}</text>
<text class="upcoming-date">{{ item.date.slice(5, 10).replace('-', ' / ') }}</text>
<text class="upcoming-hour">{{ item.startTime.slice(0, 5) }}{{ item.endTime.slice(0, 5) }}</text>
</view>
<view class="upcoming-meta">
@@ -147,7 +161,6 @@ import { getSystemLayout } from '../../utils/system'
import {
formatDateTimeFull,
getCardTypeLabel,
getCardGradientClass,
getMembershipProgressWidth,
getMembershipUsedTimes,
getMembershipTotalTimes,
@@ -238,443 +251,150 @@ onShow(() => {
<style lang="scss" scoped>
.page {
--ink: #514943;
--muted: #8b817b;
--line: #eee8e3;
--sage: #617d73;
min-height: 100vh;
background: $bg-page;
padding-bottom: 180rpx;
box-sizing: border-box;
background: #fbf9f6;
color: var(--ink);
padding-bottom: calc(152rpx + env(safe-area-inset-bottom));
}
.skeleton-wrap {
padding: 24rpx;
}
.skeleton-hero,
.skeleton-block {
border-radius: 24rpx;
background: linear-gradient(90deg, #efe8df 25%, #f7f2ea 50%, #efe8df 75%);
.skeleton-wrap { padding: 28rpx 32rpx; }
.skeleton-hero, .skeleton-block {
border-radius: 28rpx;
background: linear-gradient(90deg, #f0eae5 25%, #faf7f3 50%, #f0eae5 75%);
background-size: 400% 100%;
animation: shimmer 1.4s infinite;
}
.skeleton-hero { height: 360rpx; margin-bottom: 24rpx; }
.skeleton-block { height: 180rpx; margin-bottom: 20rpx; }
.skeleton-hero { height: 300rpx; margin-bottom: 28rpx; }
.skeleton-block { height: 180rpx; margin-bottom: 28rpx; }
.hero {
margin: 20rpx 24rpx 8rpx;
border-radius: 28rpx;
overflow: hidden;
background: linear-gradient(160deg, #2c241c 0%, #4a4035 58%, #6b5a48 100%);
position: relative;
box-shadow: 0 18rpx 40rpx rgba(44, 36, 28, 0.22);
margin: 28rpx 32rpx 0;
padding: 32rpx;
border-radius: 32rpx;
background: #f3eae5;
}
.hero-grain {
position: absolute;
inset: 0;
background-image:
radial-gradient(circle at 18% 20%, rgba(169, 191, 204, 0.18), transparent 36%),
radial-gradient(circle at 90% 80%, rgba(232, 168, 124, 0.16), transparent 32%);
}
.hero-inner {
position: relative;
display: flex;
gap: 24rpx;
padding: 36rpx 32rpx 20rpx;
}
.hero-inner { display: flex; align-items: center; gap: 24rpx; }
.hero-avatar {
width: 128rpx;
height: 128rpx;
border-radius: 28rpx;
width: 112rpx;
height: 112rpx;
border-radius: 50%;
overflow: hidden;
border: 3rpx solid rgba(255, 248, 240, 0.35);
border: 6rpx solid #fcf8f4;
flex-shrink: 0;
}
.avatar-img { width: 100%; height: 100%; }
.avatar-fallback {
width: 100%;
height: 100%;
background: #7ba5be;
display: flex;
align-items: center;
justify-content: center;
width: 100%; height: 100%; background: #e0cec4;
display: flex; align-items: center; justify-content: center;
}
.avatar-letter {
font-size: 48rpx;
font-weight: 700;
color: #fff8f0;
font-family: 'Songti SC', 'STSong', serif;
font-size: 44rpx; font-weight: 400; color: #735e53;
}
.hero-copy {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
.hero-copy { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 12rpx; }
.hero-name { font-size: 38rpx; font-weight: 500; line-height: 1.4; overflow-wrap: anywhere; }
.hero-phone { font-size: 26rpx; color: #84746b; letter-spacing: 1rpx; }
.member-id {
display: flex; align-items: center; gap: 14rpx;
margin-top: 24rpx; min-height: 48rpx;
font-size: 20rpx; color: #88786f;
}
.hero-kicker {
font-size: 18rpx;
letter-spacing: 4rpx;
color: rgba(200, 216, 228, 0.72);
}
.hero-name {
font-size: 40rpx;
font-weight: 700;
color: #fff8f0;
font-family: 'Songti SC', 'Noto Serif SC', Georgia, serif;
}
.hero-phone,
.hero-openid {
font-size: 22rpx;
color: rgba(255, 248, 240, 0.68);
}
.hero-openid {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.member-id-label { flex-shrink: 0; }
.member-id-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.member-id-copy { flex-shrink: 0; color: #715a4e; padding: 8rpx 0 8rpx 8rpx; }
.time-pair {
position: relative;
margin: 8rpx 20rpx 20rpx;
background: rgba(255, 248, 240, 0.08);
border: 1rpx solid rgba(255, 248, 240, 0.1);
border-radius: 18rpx;
display: flex;
padding: 18rpx 8rpx;
}
.time-cell {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
}
.time-rule {
width: 1rpx;
background: rgba(255, 248, 240, 0.16);
}
.time-label {
font-size: 20rpx;
color: rgba(255, 248, 240, 0.5);
letter-spacing: 2rpx;
}
.time-value {
font-size: 22rpx;
color: #fff8f0;
font-family: 'DIN Alternate', 'Helvetica Neue', sans-serif;
}
.section {
padding: 28rpx 24rpx 0;
}
.section--last {
padding-bottom: 24rpx;
}
.section-label {
display: block;
font-size: 22rpx;
letter-spacing: 4rpx;
color: $text-hint;
margin-bottom: 16rpx;
display: flex; gap: 20rpx;
margin-top: 20rpx; padding-top: 24rpx;
border-top: 1rpx solid #e4d8d0;
}
.time-cell { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10rpx; }
.time-rule { width: 1rpx; background: #e4d8d0; }
.time-label { font-size: 21rpx; color: #88786f; }
.time-value { font-size: 22rpx; color: #6d5c52; line-height: 1.5; font-variant-numeric: tabular-nums; }
.section { padding: 36rpx 32rpx 0; }
.section--last { padding-bottom: 24rpx; }
.section-label { display: block; font-size: 28rpx; font-weight: 500; margin-bottom: 20rpx; }
.section-heading { display: flex; justify-content: space-between; align-items: baseline; gap: 16rpx; }
.section-note { font-size: 22rpx; color: var(--muted); }
.stats-strip {
background: $bg-card;
border-radius: 20rpx;
display: flex;
padding: 28rpx 12rpx;
border: 1rpx solid rgba(180, 160, 130, 0.12);
display: flex; padding: 26rpx 0;
border-radius: 24rpx; background: #ffffff;
}
.stat {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.stat-num {
font-size: 40rpx;
font-weight: 800;
color: $text-primary;
font-family: 'DIN Alternate', 'Helvetica Neue', sans-serif;
}
.stat-name {
font-size: 22rpx;
color: $text-hint;
}
.noshow-hint {
display: block;
margin-top: 12rpx;
font-size: 22rpx;
color: $text-hint;
}
.card-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.mship {
background: $bg-card;
border-radius: 20rpx;
overflow: hidden;
border: 1rpx solid rgba(180, 160, 130, 0.12);
}
.mship-strip {
height: 8rpx;
}
.gradient--times { background: linear-gradient(90deg, #7ba5be, #a9bfcc); }
.gradient--duration { background: linear-gradient(90deg, #7A9E7E, #b7cbb8); }
.gradient--trial { background: linear-gradient(90deg, #C47A7A, #e8b4b4); }
.mship-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 24rpx 24rpx 8rpx;
}
.mship-titles {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.mship-name {
font-size: 30rpx;
font-weight: 700;
color: $text-primary;
}
.mship-type {
font-size: 22rpx;
color: $text-hint;
flex: 1; min-width: 0; display: flex; flex-direction: column;
align-items: center; gap: 12rpx; border-right: 1rpx solid var(--line);
&:last-child { border-right: none; }
}
.stat-num { font-size: 40rpx; font-weight: 400; line-height: 1.1; font-family: 'DIN Alternate', 'Avenir Next', sans-serif; font-variant-numeric: tabular-nums; }
.stat-name { font-size: 22rpx; color: var(--muted); }
.stat--completed { .stat-num, .stat-name { color: var(--sage); } }
.card-list { display: flex; flex-direction: column; gap: 20rpx; }
.mship { padding: 28rpx; background: #fff; border-radius: 28rpx; border: 1rpx solid var(--line); }
.mship-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 20rpx; }
.mship-titles { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8rpx; }
.mship-name { font-size: 29rpx; font-weight: 500; line-height: 1.5; overflow-wrap: anywhere; }
.mship-type { font-size: 21rpx; color: var(--muted); }
.mship-status {
padding: 4rpx 12rpx;
border-radius: 8rpx;
background: rgba($success-color, 0.14);
&--expired,
&--used_up { background: rgba($text-hint, 0.14); }
flex-shrink: 0; padding: 6rpx 16rpx; border-radius: 999rpx;
background: #edf2ee; color: #617d73; line-height: 1.3;
&--expired, &--used_up { background: #f2efec; color: #8b817b; }
}
.mship-status-text {
font-size: 20rpx;
color: $text-secondary;
}
.mship-times {
padding: 4rpx 24rpx 0;
display: flex;
align-items: baseline;
gap: 8rpx;
}
.mship-times-num {
font-size: 48rpx;
font-weight: 800;
color: $text-primary;
font-family: 'DIN Alternate', 'Helvetica Neue', sans-serif;
}
.mship-times-unit {
font-size: 22rpx;
color: $text-hint;
}
.progress {
padding: 12rpx 24rpx 0;
}
.progress-bar {
height: 8rpx;
border-radius: 8rpx;
background: rgba(180, 160, 130, 0.16);
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 8rpx;
background: $accent-color;
}
.progress-text {
display: block;
margin-top: 8rpx;
font-size: 20rpx;
color: $text-hint;
}
.mship-status-text { font-size: 21rpx; }
.mship-times { margin-top: 22rpx; display: flex; align-items: baseline; gap: 10rpx; }
.mship-times-num { font-size: 52rpx; font-weight: 400; font-family: 'DIN Alternate', 'Avenir Next', sans-serif; line-height: 1.2; }
.mship-times-unit { font-size: 22rpx; color: var(--muted); }
.mship-duration { display: flex; align-items: baseline; flex-wrap: wrap; gap: 12rpx; margin-top: 24rpx; }
.mship-duration-title { font-size: 28rpx; color: #617d73; }
.progress { margin-top: 18rpx; }
.progress-bar { height: 6rpx; border-radius: 6rpx; background: #f0efea; overflow: hidden; }
.progress-fill { height: 100%; border-radius: 6rpx; background: #a5b8ab; }
.progress-text { display: block; margin-top: 10rpx; font-size: 20rpx; color: var(--muted); }
.mship-dates {
padding: 16rpx 24rpx 24rpx;
display: flex;
justify-content: space-between;
font-size: 22rpx;
color: $text-secondary;
margin-top: 22rpx; padding-top: 18rpx; border-top: 1rpx solid #f3efeb;
display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8rpx 20rpx;
font-size: 21rpx; color: var(--muted); line-height: 1.5;
}
.empty-card {
background: $bg-card;
border-radius: 20rpx;
padding: 48rpx 32rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
border: 1rpx dashed rgba(180, 160, 130, 0.28);
}
.empty-card-title {
font-size: 30rpx;
font-weight: 700;
color: $text-primary;
}
.empty-card-sub {
font-size: 24rpx;
color: $text-hint;
}
.empty-card-btn {
margin-top: 12rpx;
padding: 12rpx 32rpx;
border-radius: 999rpx;
background: $brand-color;
}
.empty-card-btn-text {
font-size: 24rpx;
color: $primary-dark;
font-weight: 600;
}
.upcoming-list {
background: $bg-card;
border-radius: 20rpx;
overflow: hidden;
border: 1rpx solid rgba(180, 160, 130, 0.12);
background: #f4f2ed; border-radius: 28rpx; padding: 36rpx 28rpx;
display: flex; flex-direction: column; align-items: flex-start; gap: 12rpx;
}
.empty-card-title { font-size: 28rpx; font-weight: 500; }
.empty-card-sub { font-size: 23rpx; color: var(--muted); line-height: 1.7; }
.empty-card-btn { margin-top: 10rpx; padding: 14rpx 28rpx; border-radius: 999rpx; background: #e4ebe4; }
.empty-card-btn-text { font-size: 24rpx; color: #526e62; }
.upcoming-list { background: #fff; border-radius: 28rpx; padding: 0 28rpx; }
.upcoming-row {
display: flex;
justify-content: space-between;
padding: 24rpx;
border-bottom: 1rpx solid rgba(180, 160, 130, 0.1);
display: flex; align-items: center; justify-content: space-between; gap: 24rpx;
padding: 26rpx 0; border-bottom: 1rpx solid var(--line);
&:last-child { border-bottom: none; }
}
.upcoming-time {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.upcoming-date {
font-size: 26rpx;
font-weight: 700;
color: $text-primary;
}
.upcoming-hour {
font-size: 24rpx;
color: $accent-color;
font-family: 'DIN Alternate', 'Helvetica Neue', sans-serif;
}
.upcoming-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6rpx;
}
.upcoming-card {
font-size: 24rpx;
color: $text-secondary;
}
.upcoming-status {
font-size: 20rpx;
color: $text-hint;
}
.upcoming-empty {
padding: 32rpx;
background: $bg-card;
border-radius: 20rpx;
}
.upcoming-empty-text {
font-size: 24rpx;
color: $text-hint;
}
.upcoming-time { flex-shrink: 0; display: flex; flex-direction: column; gap: 10rpx; }
.upcoming-date { font-size: 28rpx; font-weight: 500; }
.upcoming-hour { font-size: 23rpx; color: var(--muted); font-variant-numeric: tabular-nums; }
.upcoming-meta { min-width: 0; display: flex; flex-direction: column; align-items: flex-end; gap: 10rpx; }
.upcoming-card { font-size: 24rpx; text-align: right; overflow-wrap: anywhere; }
.upcoming-status { font-size: 21rpx; color: var(--sage); }
.upcoming-empty { padding: 32rpx 28rpx; background: #f4f2ed; border-radius: 24rpx; }
.upcoming-empty-text { font-size: 24rpx; color: var(--muted); }
.dock {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
gap: 16rpx;
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
background: rgba(250, 248, 245, 0.94);
border-top: 1rpx solid rgba(180, 160, 130, 0.14);
}
.dock-btn {
flex: 1;
height: 88rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
}
.dock-btn--ghost {
background: #fff;
border: 2rpx solid $brand-color;
}
.dock-btn--solid {
background: $brand-color;
}
.dock-btn--disabled {
opacity: 0.38;
}
.dock-btn-text {
font-size: 28rpx;
font-weight: 700;
color: $brand-color;
}
.dock-btn-text--solid {
color: #fff8f0;
position: fixed; z-index: 10; left: 0; right: 0; bottom: 0;
display: flex; gap: 20rpx;
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
background: #fbf9f6; border-top: 1rpx solid var(--line);
}
.dock-btn { flex: 1; height: 88rpx; border-radius: 999rpx; display: flex; align-items: center; justify-content: center; }
.dock-btn--ghost { background: #f0eae4; }
.dock-btn--solid { flex: 1.35; background: #6b8276; }
.dock-btn--disabled { background: #d9dfd8; .dock-btn-text--solid { color: #677467; } }
.dock-btn-text { font-size: 28rpx; font-weight: 500; color: #78675c; }
.dock-btn-text--solid { color: #fff; }
</style>