feat: 优化首页、个人中心页面 UI
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
<view class="about-section">
|
<view class="about-section">
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
<view>
|
<view>
|
||||||
<text class="section-eyebrow">Teacher Spotlight</text>
|
|
||||||
<text class="section-title">老师介绍</text>
|
<text class="section-title">老师介绍</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="section-more" @tap="goToDetail">查看详情</text>
|
<text class="section-more" @tap="goToDetail">查看详情</text>
|
||||||
@@ -12,9 +11,7 @@
|
|||||||
<view class="teacher-main">
|
<view class="teacher-main">
|
||||||
<view class="cover-wrap">
|
<view class="cover-wrap">
|
||||||
<image class="teacher-cover" :src="teacher.avatar" mode="aspectFill" />
|
<image class="teacher-cover" :src="teacher.avatar" mode="aspectFill" />
|
||||||
<view class="cover-badge">
|
|
||||||
<text class="cover-badge-text">{{ teacher.badges[0] }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="teacher-content">
|
<view class="teacher-content">
|
||||||
@@ -22,28 +19,20 @@
|
|||||||
<view>
|
<view>
|
||||||
<view class="name-row">
|
<view class="name-row">
|
||||||
<text class="teacher-name">{{ teacher.name }}</text>
|
<text class="teacher-name">{{ teacher.name }}</text>
|
||||||
<text class="teacher-tag">{{ teacher.badges[1] }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
<text class="teacher-title">{{ teacher.title }}</text>
|
<text class="teacher-title">{{ teacher.title }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="specialty-row">
|
<view class="specialty-row">
|
||||||
<text v-for="item in teacher.specialties" :key="item" class="specialty-pill">{{ item }}</text>
|
<text class="specialty-text">{{ teacher.specialties.join(' · ') }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<text class="teacher-intro">{{ teacher.intro }}</text>
|
<text class="teacher-intro">{{ teacher.intro }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="credential-box">
|
|
||||||
<text class="credential-label">认证背景</text>
|
|
||||||
<view v-for="item in teacher.certifications" :key="item" class="credential-row">
|
|
||||||
<view class="credential-dot" />
|
|
||||||
<text class="credential-text">{{ item }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="action-row">
|
<view class="action-row">
|
||||||
<view class="secondary-btn" @tap.stop="goToDetail">
|
<view class="secondary-btn" @tap.stop="goToDetail">
|
||||||
<text class="secondary-btn-text">查看详情</text>
|
<text class="secondary-btn-text">查看详情</text>
|
||||||
@@ -71,228 +60,25 @@ function goToBooking() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.about-section {
|
.about-section { margin: 36rpx 32rpx 0; }
|
||||||
padding: 20rpx 24rpx 0;
|
.section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20rpx; }
|
||||||
}
|
.section-title { font-size: 30rpx; font-weight: 500; color: #514943; }
|
||||||
|
.section-more { font-size: 23rpx; color: #8b817b; padding: 8rpx 0; }
|
||||||
.section-header {
|
.teacher-card { padding: 26rpx; background: #f2ebe5; border-radius: 28rpx; }
|
||||||
display: flex;
|
.teacher-main { display: flex; align-items: flex-start; gap: 24rpx; }
|
||||||
align-items: flex-end;
|
.cover-wrap { width: 164rpx; height: 230rpx; border-radius: 18rpx; overflow: hidden; background: #e1d4c8; flex-shrink: 0; }
|
||||||
justify-content: space-between;
|
.teacher-cover { width: 100%; height: 100%; }
|
||||||
margin-bottom: 18rpx;
|
.teacher-content { flex: 1; min-width: 0; }
|
||||||
}
|
.name-row { margin-bottom: 8rpx; }
|
||||||
|
.teacher-name { font-size: 34rpx; font-weight: 500; color: #514943; }
|
||||||
.section-eyebrow {
|
.teacher-title { font-size: 22rpx; color: #8b817b; line-height: 1.5; }
|
||||||
display: block;
|
.specialty-row { margin: 14rpx 0 10rpx; }
|
||||||
font-size: 20rpx;
|
.specialty-text { font-size: 21rpx; color: #617d73; line-height: 1.6; }
|
||||||
letter-spacing: 3rpx;
|
.teacher-intro { font-size: 22rpx; line-height: 1.7; color: #786b61; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
text-transform: uppercase;
|
.action-row { display: flex; gap: 16rpx; margin-top: 24rpx; }
|
||||||
color: #b99b8c;
|
.secondary-btn, .primary-btn { flex: 1; height: 68rpx; border-radius: 999rpx; display: flex; align-items: center; justify-content: center; }
|
||||||
margin-bottom: 8rpx;
|
.secondary-btn { background: #fbf7f3; }
|
||||||
}
|
.primary-btn { background: #6b8276; }
|
||||||
|
.secondary-btn-text { font-size: 24rpx; color: #78675c; }
|
||||||
.section-title {
|
.primary-btn-text { font-size: 24rpx; font-weight: 400; color: #fff; }
|
||||||
font-size: 34rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #2f2723;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-more {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #c36d52;
|
|
||||||
padding: 10rpx 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.teacher-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 18rpx;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at top right, rgba(226, 198, 179, 0.42), transparent 32%),
|
|
||||||
linear-gradient(135deg, #fffdfb 0%, #f8f2ee 46%, #f2e8e2 100%);
|
|
||||||
border-radius: 30rpx;
|
|
||||||
padding: 22rpx;
|
|
||||||
box-shadow: 0 18rpx 38rpx rgba(126, 98, 84, 0.09);
|
|
||||||
}
|
|
||||||
|
|
||||||
.teacher-main {
|
|
||||||
display: flex;
|
|
||||||
gap: 22rpx;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cover-wrap {
|
|
||||||
position: relative;
|
|
||||||
width: 188rpx;
|
|
||||||
height: 248rpx;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: #eadfd7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.teacher-cover {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cover-badge {
|
|
||||||
position: absolute;
|
|
||||||
left: 14rpx;
|
|
||||||
bottom: 14rpx;
|
|
||||||
border-radius: 999rpx;
|
|
||||||
padding: 8rpx 16rpx;
|
|
||||||
background: rgba(41, 34, 30, 0.74);
|
|
||||||
backdrop-filter: blur(12rpx);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cover-badge-text {
|
|
||||||
font-size: 20rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff7f3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.teacher-content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.teacher-heading {
|
|
||||||
margin-bottom: 14rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10rpx;
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.teacher-name {
|
|
||||||
font-size: 38rpx;
|
|
||||||
line-height: 1;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #2e2521;
|
|
||||||
}
|
|
||||||
|
|
||||||
.teacher-tag {
|
|
||||||
font-size: 18rpx;
|
|
||||||
line-height: 1;
|
|
||||||
color: #a85d44;
|
|
||||||
background: rgba(221, 150, 118, 0.18);
|
|
||||||
border-radius: 999rpx;
|
|
||||||
padding: 8rpx 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.teacher-title {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #7f6659;
|
|
||||||
}
|
|
||||||
|
|
||||||
.specialty-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10rpx;
|
|
||||||
margin-bottom: 14rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.specialty-pill {
|
|
||||||
padding: 8rpx 16rpx;
|
|
||||||
border-radius: 999rpx;
|
|
||||||
background: rgba(92, 126, 151, 0.12);
|
|
||||||
color: #5a7a8b;
|
|
||||||
font-size: 20rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.teacher-intro {
|
|
||||||
font-size: 22rpx;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: #5e5048;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-box {
|
|
||||||
padding: 16rpx 18rpx;
|
|
||||||
border-radius: 18rpx;
|
|
||||||
background: rgba(255, 255, 255, 0.72);
|
|
||||||
border: 1rpx solid rgba(190, 161, 145, 0.22);
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 18rpx;
|
|
||||||
letter-spacing: 2rpx;
|
|
||||||
color: #b19486;
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-text {
|
|
||||||
font-size: 20rpx;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: #6a5a51;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-row + .credential-row {
|
|
||||||
margin-top: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-dot {
|
|
||||||
width: 12rpx;
|
|
||||||
height: 12rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-top: 12rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: #6d4037;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-btn,
|
|
||||||
.primary-btn {
|
|
||||||
height: 72rpx;
|
|
||||||
border-radius: 999rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-btn {
|
|
||||||
width: 148rpx;
|
|
||||||
border: 1rpx solid rgba(139, 113, 99, 0.24);
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary-btn {
|
|
||||||
flex: 1;
|
|
||||||
background: linear-gradient(135deg, #ff7654 0%, #ff4d38 100%);
|
|
||||||
box-shadow: 0 14rpx 24rpx rgba(255, 92, 69, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-btn-text {
|
|
||||||
font-size: 24rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #684d40;
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary-btn-text {
|
|
||||||
font-size: 26rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #ffffff;
|
|
||||||
letter-spacing: 1rpx;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -42,9 +42,6 @@
|
|||||||
<!-- Single membership -->
|
<!-- Single membership -->
|
||||||
<view v-if="memberships.length === 1" class="card-single">
|
<view v-if="memberships.length === 1" class="card-single">
|
||||||
<view class="card-item selected">
|
<view class="card-item selected">
|
||||||
<view class="card-icon-wrap">
|
|
||||||
<text class="card-icon">💳</text>
|
|
||||||
</view>
|
|
||||||
<view class="card-info">
|
<view class="card-info">
|
||||||
<text class="card-name">{{ memberships[0].cardType.name }}</text>
|
<text class="card-name">{{ memberships[0].cardType.name }}</text>
|
||||||
<text class="card-remain" v-if="memberships[0].remainingTimes !== null">
|
<text class="card-remain" v-if="memberships[0].remainingTimes !== null">
|
||||||
@@ -61,7 +58,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Multiple memberships picker -->
|
<!-- Multiple memberships picker -->
|
||||||
<view v-else-if="memberships.length > 1" class="card-picker-wrap">
|
<scroll-view v-else-if="memberships.length > 1" class="card-picker-wrap" scroll-y>
|
||||||
<view
|
<view
|
||||||
v-for="m in memberships"
|
v-for="m in memberships"
|
||||||
:key="m.id"
|
:key="m.id"
|
||||||
@@ -69,9 +66,6 @@
|
|||||||
:class="{ selected: selectedMembershipId === m.id }"
|
:class="{ selected: selectedMembershipId === m.id }"
|
||||||
@tap="selectedMembershipId = m.id"
|
@tap="selectedMembershipId = m.id"
|
||||||
>
|
>
|
||||||
<view class="card-icon-wrap">
|
|
||||||
<text class="card-icon">💳</text>
|
|
||||||
</view>
|
|
||||||
<view class="card-info">
|
<view class="card-info">
|
||||||
<text class="card-name">{{ m.cardType.name }}</text>
|
<text class="card-name">{{ m.cardType.name }}</text>
|
||||||
<text class="card-remain" v-if="m.remainingTimes !== null">
|
<text class="card-remain" v-if="m.remainingTimes !== null">
|
||||||
@@ -85,7 +79,7 @@
|
|||||||
<text class="check-icon">✓</text>
|
<text class="check-icon">✓</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</scroll-view>
|
||||||
|
|
||||||
<!-- No memberships fallback (should not normally appear) -->
|
<!-- No memberships fallback (should not normally appear) -->
|
||||||
<view v-else class="no-card-tip">
|
<view v-else class="no-card-tip">
|
||||||
@@ -110,6 +104,8 @@
|
|||||||
<button
|
<button
|
||||||
class="btn-confirm"
|
class="btn-confirm"
|
||||||
:class="{ disabled: !selectedMembershipId }"
|
:class="{ disabled: !selectedMembershipId }"
|
||||||
|
:disabled="!selectedMembershipId || requestingSubscribe"
|
||||||
|
:loading="requestingSubscribe"
|
||||||
@tap="handleConfirm"
|
@tap="handleConfirm"
|
||||||
>
|
>
|
||||||
<text class="btn-confirm-text">确认预约</text>
|
<text class="btn-confirm-text">确认预约</text>
|
||||||
@@ -143,8 +139,8 @@ const requestingSubscribe = ref(false)
|
|||||||
watch(
|
watch(
|
||||||
[() => props.visible, () => props.memberships],
|
[() => props.visible, () => props.memberships],
|
||||||
([visible, memberships]) => {
|
([visible, memberships]) => {
|
||||||
if (visible && memberships.length > 0) {
|
if (visible) {
|
||||||
selectedMembershipId.value = memberships[0].id
|
selectedMembershipId.value = memberships[0]?.id ?? ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@@ -187,261 +183,45 @@ function handleMaskTap() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.popup-mask {
|
.popup-mask { position: fixed; inset: 0; background: rgba(56, 48, 42, 0.32); z-index: 1000; display: flex; align-items: flex-end; justify-content: center; }
|
||||||
position: fixed;
|
.popup-panel { width: 100%; box-sizing: border-box; background: #fbf9f6; border-radius: 36rpx 36rpx 0 0; padding: 28rpx 32rpx calc(24rpx + env(safe-area-inset-bottom)); }
|
||||||
top: 0;
|
.popup-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
|
||||||
left: 0;
|
.popup-title { font-size: 32rpx; font-weight: 500; color: #514943; }
|
||||||
right: 0;
|
.close-btn { width: 60rpx; height: 60rpx; display: flex; align-items: center; justify-content: center; }
|
||||||
bottom: 0;
|
.close-icon { font-size: 26rpx; color: #8b817b; }
|
||||||
background: rgba(0, 0, 0, 0.45);
|
.info-section { padding: 24rpx; background: #f1e8e1; border-radius: 24rpx; display: flex; flex-direction: column; gap: 14rpx; }
|
||||||
z-index: 1000;
|
.info-row { display: flex; align-items: center; gap: 20rpx; }
|
||||||
display: flex;
|
.info-label { font-size: 24rpx; color: #8b7b70; width: 64rpx; flex-shrink: 0; }
|
||||||
align-items: flex-end;
|
.info-value { font-size: 26rpx; color: #6f5c50; font-weight: 400; }
|
||||||
justify-content: center;
|
.divider { height: 24rpx; }
|
||||||
}
|
.card-section { display: flex; flex-direction: column; gap: 16rpx; }
|
||||||
|
.section-label { font-size: 26rpx; color: #514943; font-weight: 500; }
|
||||||
.popup-panel {
|
.card-picker-wrap { height: 250rpx; }
|
||||||
width: 100%;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 32rpx 32rpx 0 0;
|
|
||||||
padding: 32rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 32rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-title {
|
|
||||||
font-size: 34rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1a1a1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
width: 56rpx;
|
|
||||||
height: 56rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: $primary-selected-bg;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-icon {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Info rows */
|
|
||||||
.info-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20rpx;
|
|
||||||
margin-bottom: 28rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #999;
|
|
||||||
width: 80rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #222;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
height: 1rpx;
|
|
||||||
background: $primary-border;
|
|
||||||
margin: 8rpx 0 28rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card selection */
|
|
||||||
.card-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16rpx;
|
|
||||||
margin-bottom: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-label-row {
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-label {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #666;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-single,
|
|
||||||
.card-picker-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-item {
|
.card-item {
|
||||||
display: flex;
|
display: flex; align-items: center; padding: 22rpx 24rpx; border-radius: 24rpx;
|
||||||
flex-direction: row;
|
border: 2rpx solid #eee8e3; background: #fff; gap: 20rpx; box-sizing: border-box;
|
||||||
align-items: center;
|
&.selected { border-color: #a5b8ab; background: #f0f4ee; }
|
||||||
padding: 24rpx 20rpx;
|
|
||||||
border-radius: 16rpx;
|
|
||||||
border: 2rpx solid $primary-border;
|
|
||||||
background: $primary-bg;
|
|
||||||
gap: 20rpx;
|
|
||||||
transition: border-color 0.15s, background 0.15s;
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
border-color: $primary-dark;
|
|
||||||
background: $primary-selected-bg;
|
|
||||||
}
|
}
|
||||||
}
|
.card-picker-wrap .card-item { margin-bottom: 12rpx; }
|
||||||
|
.card-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8rpx; }
|
||||||
.card-icon-wrap {
|
.card-name { font-size: 26rpx; font-weight: 400; color: #514943; overflow-wrap: anywhere; }
|
||||||
width: 60rpx;
|
.card-remain { font-size: 22rpx; color: #8b817b; }
|
||||||
height: 60rpx;
|
.check-mark { width: 36rpx; height: 36rpx; border-radius: 50%; background: #6b8276; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||||
border-radius: 14rpx;
|
.check-icon { font-size: 22rpx; color: #fff; }
|
||||||
background: linear-gradient(135deg, $primary-color, $primary-dark);
|
.no-card-tip { padding: 24rpx; text-align: center; }
|
||||||
display: flex;
|
.no-card-text { font-size: 24rpx; color: #8b817b; }
|
||||||
align-items: center;
|
.deduction-tip { padding: 18rpx 4rpx; }
|
||||||
justify-content: center;
|
.deduction-text { font-size: 22rpx; color: #8b817b; line-height: 1.6; }
|
||||||
flex-shrink: 0;
|
.action-row { display: flex; gap: 20rpx; margin-top: 12rpx; }
|
||||||
}
|
.btn-outline { flex: 1; height: 88rpx; border-radius: 999rpx; background: #f0eae4; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.btn-outline-text { font-size: 28rpx; color: #78675c; font-weight: 400; }
|
||||||
.card-icon {
|
|
||||||
font-size: 32rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-info {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-name {
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-remain {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-mark {
|
|
||||||
width: 44rpx;
|
|
||||||
height: 44rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: $primary-dark;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-icon {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-card-tip {
|
|
||||||
padding: 24rpx;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-card-text {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #bbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Deduction tip */
|
|
||||||
.deduction-tip {
|
|
||||||
background: $primary-selected-bg;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
padding: 16rpx 20rpx;
|
|
||||||
margin-bottom: 28rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deduction-text {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: $primary-dark;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Action buttons */
|
|
||||||
.action-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 20rpx;
|
|
||||||
margin-top: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline {
|
|
||||||
flex: 1;
|
|
||||||
height: 88rpx;
|
|
||||||
border-radius: 44rpx;
|
|
||||||
border: 2rpx solid $primary-border;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline-text {
|
|
||||||
font-size: 30rpx;
|
|
||||||
color: #666;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-confirm {
|
.btn-confirm {
|
||||||
flex: 2;
|
flex: 2; height: 88rpx; padding: 0; margin: 0; border: none; line-height: 1;
|
||||||
height: 88rpx;
|
border-radius: 999rpx; background: #6b8276;
|
||||||
border-radius: 44rpx;
|
display: flex; align-items: center; justify-content: center;
|
||||||
background: linear-gradient(135deg, $primary-color, $primary-dark);
|
&::after { border: none; }
|
||||||
display: flex;
|
&:active { background: #597264; }
|
||||||
align-items: center;
|
&.disabled { background: #d9dfd8; }
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
background: $primary-border;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-confirm-text {
|
|
||||||
font-size: 30rpx;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 1rpx;
|
|
||||||
|
|
||||||
.disabled & {
|
|
||||||
color: #bbb;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.btn-confirm-text { font-size: 28rpx; color: #fff; font-weight: 500; .disabled & { color: #677467; } }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<!-- Section header -->
|
<!-- Section header -->
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
<text class="section-title">会员卡</text>
|
<text class="section-title">会员卡</text>
|
||||||
<text class="section-action" @tap="goToAllCards">全部</text>
|
<text class="section-action" @tap="goToAllCards">全部 ›</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Loading skeleton -->
|
<!-- Loading skeleton -->
|
||||||
@@ -39,8 +39,7 @@
|
|||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<view class="cover-deco cover-deco--1" />
|
<text class="cover-label">{{ card.type === CardTypeCategory.TRIAL ? '体验' : card.type === CardTypeCategory.DURATION ? '期限卡' : '次卡' }}</text>
|
||||||
<view class="cover-deco cover-deco--2" />
|
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -146,272 +145,36 @@ function goToAllCards() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.card-shop {
|
.card-shop { margin: 36rpx 32rpx 0; }
|
||||||
background: #ffffff;
|
.section-header { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; margin-bottom: 20rpx; }
|
||||||
margin: 16rpx 24rpx 0;
|
.section-title { font-size: 30rpx; font-weight: 500; color: #514943; }
|
||||||
padding-bottom: 20rpx;
|
.section-action { font-size: 23rpx; color: #8b817b; padding: 8rpx 0; }
|
||||||
border-radius: 20rpx;
|
.card-list { padding: 0 24rpx; border-radius: 28rpx; background: #fff; border: 1rpx solid #eee8e3; }
|
||||||
overflow: hidden;
|
.card-row { display: flex; align-items: center; gap: 20rpx; padding: 26rpx 0; border-bottom: 1rpx solid #f0ebe6; &:last-child { border: none; } }
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
.card-cover { width: 132rpx; height: 148rpx; border-radius: 16rpx; overflow: hidden; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: #f0e6de; }
|
||||||
}
|
.card-cover-img { width: 100%; height: 100%; }
|
||||||
|
.cover--times { background: #f0e6de; }
|
||||||
/* ── Section header ── */
|
.cover--duration { background: #e9ede5; }
|
||||||
.section-header {
|
.cover--trial { background: #f2e4e0; }
|
||||||
display: flex;
|
.cover-label { font-family: 'Songti SC', 'STSong', serif; font-size: 28rpx; color: #857062; letter-spacing: 3rpx; }
|
||||||
align-items: center;
|
.card-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 14rpx; }
|
||||||
justify-content: space-between;
|
.info-top { display: flex; flex-direction: column; gap: 8rpx; }
|
||||||
padding: 32rpx 32rpx 20rpx;
|
.card-name-row { display: flex; align-items: baseline; gap: 8rpx; }
|
||||||
}
|
.card-name { min-width: 0; font-size: 27rpx; font-weight: 400; color: #514943; line-height: 1.5; overflow-wrap: anywhere; }
|
||||||
|
.renew-tag { flex-shrink: 0; font-size: 18rpx; color: #617d73; background: #edf2ec; padding: 3rpx 8rpx; border-radius: 6rpx; }
|
||||||
.section-title {
|
.card-validity { font-size: 21rpx; color: #8b817b; }
|
||||||
font-size: 36rpx;
|
.info-bottom { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8rpx 16rpx; }
|
||||||
font-weight: 700;
|
.card-times { display: flex; align-items: baseline; gap: 4rpx; }
|
||||||
color: #222;
|
.card-times-value { font-size: 24rpx; color: #6f655e; }
|
||||||
}
|
.card-times-unit { font-size: 20rpx; color: #8b817b; }
|
||||||
|
.price-row { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8rpx; }
|
||||||
.section-action {
|
.price-current { font-size: 30rpx; font-weight: 500; color: #8b6c5b; font-variant-numeric: tabular-nums; }
|
||||||
font-size: 26rpx;
|
.price-original { font-size: 20rpx; color: #a59b93; text-decoration: line-through; }
|
||||||
color: #999;
|
.card-arrow { font-size: 32rpx; color: #b2a79e; flex-shrink: 0; }
|
||||||
padding: 8rpx 24rpx;
|
.skeleton-card-cover { width: 132rpx; height: 148rpx; border-radius: 16rpx; flex-shrink: 0; }
|
||||||
border: 1rpx solid #ddd;
|
.skeleton-info { flex: 1; display: flex; flex-direction: column; gap: 16rpx; }
|
||||||
border-radius: 24rpx;
|
.skeleton-line { height: 24rpx; width: 70%; border-radius: 8rpx; &--sub { width: 50%; } }
|
||||||
}
|
.skeleton-line, .skeleton-card-cover { background: linear-gradient(90deg, #f0eae5 25%, #faf7f3 50%, #f0eae5 75%); background-size: 400% 100%; animation: shimmer 1.4s infinite; }
|
||||||
|
.empty-state { padding: 48rpx 24rpx; border-radius: 24rpx; background: #f3efea; text-align: center; }
|
||||||
/* ── Card list ── */
|
.empty-text { font-size: 24rpx; color: #8b817b; }
|
||||||
.card-list {
|
|
||||||
padding: 0 32rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20rpx;
|
|
||||||
padding: 16rpx 0;
|
|
||||||
border-bottom: 1rpx solid rgba($brand-color, 0.08);
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ══════════════════════════════════════════════════════════
|
|
||||||
CARD COVER — Clean minimal design
|
|
||||||
══════════════════════════════════════════════════════════ */
|
|
||||||
.card-cover {
|
|
||||||
width: 200rpx;
|
|
||||||
height: 130rpx;
|
|
||||||
border-radius: 16rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-cover-img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Decorative circles */
|
|
||||||
.cover-deco {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
&--1 {
|
|
||||||
width: 100rpx;
|
|
||||||
height: 100rpx;
|
|
||||||
top: -30rpx;
|
|
||||||
right: -20rpx;
|
|
||||||
background: rgba(255, 255, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--2 {
|
|
||||||
width: 70rpx;
|
|
||||||
height: 70rpx;
|
|
||||||
bottom: -20rpx;
|
|
||||||
left: -10rpx;
|
|
||||||
background: rgba(255, 255, 255, 0.25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card cover backgrounds */
|
|
||||||
.cover--times {
|
|
||||||
background: linear-gradient(135deg, #E8D5C4 0%, #D4BFA8 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cover--duration {
|
|
||||||
background: linear-gradient(135deg, #D8C8DC 0%, #C4AECB 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cover--trial {
|
|
||||||
background: linear-gradient(135deg, #C8D8D2 0%, #A9C4BC 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Card info — matches card-cover height ── */
|
|
||||||
.card-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
height: 130rpx;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-top {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-bottom {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-name-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10rpx;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-name {
|
|
||||||
min-width: 0;
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: $text-primary;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
letter-spacing: 0.5rpx;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.renew-tag {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 18rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #5f7a6e;
|
|
||||||
background: rgba(143, 168, 154, 0.2);
|
|
||||||
padding: 2rpx 10rpx;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
line-height: 1.4;
|
|
||||||
letter-spacing: 1rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-validity {
|
|
||||||
font-size: 23rpx;
|
|
||||||
color: $text-secondary;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-times {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 4rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-times-value {
|
|
||||||
font-size: 34rpx;
|
|
||||||
font-weight: 800;
|
|
||||||
color: $brand-color;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-times-unit {
|
|
||||||
font-size: 20rpx;
|
|
||||||
color: $text-secondary;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 6rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-current {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 800;
|
|
||||||
color: $brand-color;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-original {
|
|
||||||
font-size: 20rpx;
|
|
||||||
color: $text-hint;
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Arrow */
|
|
||||||
.card-arrow {
|
|
||||||
font-size: 44rpx;
|
|
||||||
color: $text-hint;
|
|
||||||
flex-shrink: 0;
|
|
||||||
transform: scaleX(0.5);
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Skeleton ── */
|
|
||||||
.skeleton-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20rpx;
|
|
||||||
padding: 20rpx 0;
|
|
||||||
border-bottom: 1rpx solid rgba($brand-color, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-card-cover {
|
|
||||||
width: 240rpx;
|
|
||||||
height: 130rpx;
|
|
||||||
border-radius: 16rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
|
|
||||||
background-size: 400% 100%;
|
|
||||||
animation: shimmer 1.4s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-info {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-line {
|
|
||||||
height: 24rpx;
|
|
||||||
border-radius: 6rpx;
|
|
||||||
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
|
|
||||||
background-size: 400% 100%;
|
|
||||||
animation: shimmer 1.4s infinite;
|
|
||||||
|
|
||||||
&--title {
|
|
||||||
width: 60%;
|
|
||||||
height: 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--sub {
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--price {
|
|
||||||
width: 45%;
|
|
||||||
height: 36rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Empty state ── */
|
|
||||||
.empty-state {
|
|
||||||
padding: 80rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: $text-hint;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
>
|
>
|
||||||
<text class="weekday">{{ item.isToday ? '今天' : item.weekday }}</text>
|
<text class="weekday">{{ item.isToday ? '今天' : item.weekday }}</text>
|
||||||
<text class="day">{{ getDayNumber(item.date) }}</text>
|
<text class="day">{{ getDayNumber(item.date) }}</text>
|
||||||
<text class="month">{{ getMonthNumber(item.date) }}月</text>
|
<text v-if="variant !== 'soft' || getDayNumber(item.date) === '1'" class="month">{{ getMonthNumber(item.date) }}月</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@@ -28,7 +28,7 @@ import { getDateRange } from '../utils/format'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: string
|
modelValue: string
|
||||||
variant?: 'default' | 'booking'
|
variant?: 'default' | 'booking' | 'soft'
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -168,4 +168,24 @@ const variant = computed(() => props.variant ?? 'default')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-selector.date-selector--soft {
|
||||||
|
padding: 8rpx 0 24rpx;
|
||||||
|
background: #fbf9f6;
|
||||||
|
border-bottom: none;
|
||||||
|
.track { padding: 0 32rpx; gap: 12rpx; }
|
||||||
|
.date-item {
|
||||||
|
min-width: 0; width: 86rpx; height: 116rpx; padding: 0;
|
||||||
|
box-sizing: border-box; border-radius: 24rpx;
|
||||||
|
background: transparent; gap: 12rpx;
|
||||||
|
.weekday { font-size: 22rpx; color: #8b817b; }
|
||||||
|
.day { font-size: 34rpx; font-weight: 400; color: #514943; }
|
||||||
|
&.active {
|
||||||
|
background: #eee3dc;
|
||||||
|
.weekday, .day { color: #705b4f; }
|
||||||
|
.day { font-weight: 500; }
|
||||||
|
}
|
||||||
|
&.today:not(.active) .weekday { color: #617d73; font-weight: 400; }
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,11 +3,8 @@
|
|||||||
<!-- Section header -->
|
<!-- Section header -->
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
<view class="header-left">
|
<view class="header-left">
|
||||||
<view class="flash-icon-wrap">
|
|
||||||
<view class="flash-icon-clock" />
|
|
||||||
</view>
|
|
||||||
<text class="section-title">限时秒杀</text>
|
<text class="section-title">限时秒杀</text>
|
||||||
<view v-if="hasOngoing" class="live-dot" />
|
<text v-if="hasOngoing" class="live-note">进行中</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -155,284 +152,31 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.flash-sale-section {
|
.flash-sale-section { margin: 36rpx 32rpx 0; }
|
||||||
background: #fff;
|
.section-header { margin-bottom: 20rpx; }
|
||||||
margin: 16rpx 24rpx 0;
|
.header-left { display: flex; align-items: baseline; justify-content: space-between; gap: 16rpx; }
|
||||||
padding-bottom: 24rpx;
|
.section-title { font-size: 30rpx; font-weight: 500; color: #514943; }
|
||||||
border-radius: 20rpx;
|
.live-note { font-size: 22rpx; color: #9b7b66; }
|
||||||
overflow: hidden;
|
.flash-scroll { width: 100%; white-space: nowrap; }
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
.flash-card-list { display: inline-flex; gap: 20rpx; }
|
||||||
}
|
.flash-card { width: 400rpx; border-radius: 28rpx; overflow: hidden; border: 1rpx solid #e7ded5; flex-shrink: 0; display: inline-flex; flex-direction: column; white-space: normal; background: #fff; }
|
||||||
|
.card-top { padding: 20rpx 24rpx; background: #f1e6de; display: flex; flex-direction: column; gap: 12rpx; }
|
||||||
/* ── Section header ── */
|
.card--upcoming .card-top { background: #eaf0e8; }
|
||||||
.section-header {
|
.card--soldout .card-top, .card--ended .card-top { background: #eeeae5; }
|
||||||
display: flex;
|
.phase-badge-text { font-size: 22rpx; color: #7c6656; }
|
||||||
align-items: center;
|
.countdown-row, .countdown-blocks { display: flex; align-items: baseline; gap: 8rpx; }
|
||||||
justify-content: space-between;
|
.countdown-label { font-size: 20rpx; color: #8b817b; }
|
||||||
padding: 28rpx 32rpx 16rpx;
|
.cd-block, .cd-sep { font-size: 24rpx; color: #7c6656; font-variant-numeric: tabular-nums; }
|
||||||
}
|
.card-body { padding: 24rpx; display: flex; flex-direction: column; gap: 10rpx; }
|
||||||
|
.card-title { font-size: 28rpx; font-weight: 500; color: #514943; line-height: 1.5; overflow-wrap: anywhere; }
|
||||||
.header-left {
|
.card-type-name { font-size: 22rpx; color: #8b817b; }
|
||||||
display: flex;
|
.price-area { display: flex; align-items: baseline; flex-wrap: wrap; gap: 12rpx; margin-top: 8rpx; }
|
||||||
align-items: center;
|
.flash-price-row { display: flex; align-items: baseline; gap: 4rpx; }
|
||||||
gap: 12rpx;
|
.flash-currency { font-size: 22rpx; color: #8b6c5b; }
|
||||||
}
|
.flash-price { font-size: 38rpx; font-weight: 500; color: #8b6c5b; }
|
||||||
|
.original-price { font-size: 21rpx; color: #a59b93; text-decoration: line-through; }
|
||||||
.flash-icon-wrap {
|
.stock-area { margin-top: 12rpx; display: flex; flex-direction: column; gap: 10rpx; }
|
||||||
width: 44rpx;
|
.stock-bar { height: 6rpx; background: #f3efea; border-radius: 6rpx; overflow: hidden; }
|
||||||
height: 44rpx;
|
.stock-fill { height: 100%; border-radius: 6rpx; background: #bea28e; }
|
||||||
border-radius: 12rpx;
|
.stock-text { font-size: 20rpx; color: #8b817b; }
|
||||||
background: linear-gradient(135deg, #D4A59A, #C08B7E);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CSS-drawn clock icon */
|
|
||||||
.flash-icon-clock {
|
|
||||||
width: 24rpx;
|
|
||||||
height: 24rpx;
|
|
||||||
border: 3rpx solid #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 2rpx;
|
|
||||||
height: 9rpx;
|
|
||||||
background: #fff;
|
|
||||||
transform-origin: bottom center;
|
|
||||||
transform: translate(-50%, -100%) rotate(0deg);
|
|
||||||
border-radius: 1rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 2rpx;
|
|
||||||
height: 7rpx;
|
|
||||||
background: #fff;
|
|
||||||
transform-origin: bottom center;
|
|
||||||
transform: translate(-50%, -100%) rotate(90deg);
|
|
||||||
border-radius: 1rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-dot {
|
|
||||||
width: 12rpx;
|
|
||||||
height: 12rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #C08B7E;
|
|
||||||
animation: pulse 1.5s ease infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 1; transform: scale(1); }
|
|
||||||
50% { opacity: 0.4; transform: scale(0.8); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Horizontal scroll ── */
|
|
||||||
.flash-scroll {
|
|
||||||
padding-left: 32rpx;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-card-list {
|
|
||||||
display: inline-flex;
|
|
||||||
gap: 20rpx;
|
|
||||||
padding-right: 32rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Flash card ── */
|
|
||||||
.flash-card {
|
|
||||||
width: 340rpx;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-shadow: 0 4rpx 20rpx rgba(192, 139, 126, 0.18);
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card--ongoing { box-shadow: 0 6rpx 28rpx rgba(192, 139, 126, 0.28); }
|
|
||||||
.card--upcoming { opacity: 0.95; }
|
|
||||||
.card--soldout { opacity: 0.7; }
|
|
||||||
.card--ended { opacity: 0.5; }
|
|
||||||
|
|
||||||
/* Card top gradient — warm blush tones */
|
|
||||||
.card-top {
|
|
||||||
padding: 20rpx 20rpx 16rpx;
|
|
||||||
background: linear-gradient(135deg, #D4A59A 0%, #C9948A 40%, #B5836E 100%);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card--upcoming .card-top {
|
|
||||||
background: linear-gradient(135deg, #8FA89A 0%, #7BA5A0 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card--soldout .card-top,
|
|
||||||
.card--ended .card-top {
|
|
||||||
background: linear-gradient(135deg, #C4BAB0, #AEA49A);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Phase badge */
|
|
||||||
.phase-badge {
|
|
||||||
align-self: flex-start;
|
|
||||||
padding: 4rpx 14rpx;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge--ongoing { background: rgba(255, 255, 255, 0.3); }
|
|
||||||
.badge--upcoming { background: rgba(255, 255, 255, 0.25); }
|
|
||||||
.badge--inactive { background: rgba(0, 0, 0, 0.1); }
|
|
||||||
|
|
||||||
.phase-badge-text {
|
|
||||||
font-size: 20rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Countdown */
|
|
||||||
.countdown-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown-label {
|
|
||||||
font-size: 20rpx;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown-blocks {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-block {
|
|
||||||
background: rgba(255, 255, 255, 0.25);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 22rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 4rpx 8rpx;
|
|
||||||
border-radius: 6rpx;
|
|
||||||
font-family: 'DIN Alternate', monospace;
|
|
||||||
min-width: 36rpx;
|
|
||||||
text-align: center;
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-sep {
|
|
||||||
color: rgba(255, 255, 255, 0.75);
|
|
||||||
font-size: 20rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card body */
|
|
||||||
.card-body {
|
|
||||||
padding: 20rpx;
|
|
||||||
background: #fff;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: $text-primary;
|
|
||||||
line-height: 1.3;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-type-name {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: $text-hint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Price area */
|
|
||||||
.price-area {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 12rpx;
|
|
||||||
margin-top: 4rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-price-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-currency {
|
|
||||||
font-size: 22rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #B5725E;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-price {
|
|
||||||
font-size: 40rpx;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #B5725E;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.original-price {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #ccc;
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stock area */
|
|
||||||
.stock-area {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6rpx;
|
|
||||||
margin-top: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stock-bar {
|
|
||||||
height: 10rpx;
|
|
||||||
background: #f5f0ed;
|
|
||||||
border-radius: 5rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stock-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, #D4A59A, #C08B7E);
|
|
||||||
border-radius: 5rpx;
|
|
||||||
transition: width 0.3s;
|
|
||||||
|
|
||||||
&--hot {
|
|
||||||
animation: stockPulse 2s ease infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes stockPulse {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.7; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.stock-text {
|
|
||||||
font-size: 20rpx;
|
|
||||||
color: $text-hint;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<view class="practice">
|
<view class="practice">
|
||||||
<view class="practice__heading">
|
<view class="practice__heading">
|
||||||
<view>
|
<view>
|
||||||
<text class="practice__eyebrow">YOUR PRACTICE</text>
|
|
||||||
<view class="practice__title">练习足迹<text class="practice__period">最近 30 天</text></view>
|
<view class="practice__title">练习足迹<text class="practice__period">最近 30 天</text></view>
|
||||||
</view>
|
</view>
|
||||||
<view class="practice__summary"><text class="practice__number">{{ activity ? total : '—' }}</text><text>节课</text></view>
|
<view class="practice__summary"><text class="practice__number">{{ activity ? total : '—' }}</text><text>节课</text></view>
|
||||||
@@ -55,10 +54,10 @@ const cells = computed(() => activity.value?.days ?? Array.from({ length: 30 },
|
|||||||
const total = computed(() => cells.value.reduce((sum, day) => sum + day.count, 0))
|
const total = computed(() => cells.value.reduce((sum, day) => sum + day.count, 0))
|
||||||
const activeDays = computed(() => cells.value.filter(day => day.count > 0).length)
|
const activeDays = computed(() => cells.value.filter(day => day.count > 0).length)
|
||||||
const detail = computed(() => {
|
const detail = computed(() => {
|
||||||
if (!activity.value) return '每一次练习,都在积蓄力量'
|
if (!activity.value) return '正在整理练习记录'
|
||||||
const day = activity.value.days.find(item => item.date === selected.value)
|
const day = activity.value.days.find(item => item.date === selected.value)
|
||||||
if (day) return `${formatDate(day.date)} · ${day.count ? `已完成 ${day.count} 节课` : '暂无已完成课程'}`
|
if (day) return `${formatDate(day.date)} · ${day.count ? `已完成 ${day.count} 节课` : '暂无已完成课程'}`
|
||||||
return total.value ? `已练习 ${activeDays.value} 天 · 点选查看` : '从下一次练习,点亮第一格'
|
return total.value ? `已练习 ${activeDays.value} 天 · 点选查看` : '近 30 天暂无已完成课程'
|
||||||
})
|
})
|
||||||
function formatDate(date: string) {
|
function formatDate(date: string) {
|
||||||
return `${Number(date.slice(5, 7))}月${Number(date.slice(8, 10))}日`
|
return `${Number(date.slice(5, 7))}月${Number(date.slice(8, 10))}日`
|
||||||
@@ -89,21 +88,19 @@ onUnmounted(() => { disposed = true })
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.practice {
|
.practice {
|
||||||
margin: 28rpx $spacing-lg 0;
|
margin: 24rpx 32rpx 0;
|
||||||
padding: 28rpx;
|
padding: 28rpx;
|
||||||
border: 1rpx solid #e2eae8;
|
border: 1rpx solid #eee8e3;
|
||||||
border-radius: 28rpx;
|
border-radius: 28rpx;
|
||||||
background: linear-gradient(135deg, #fffefd, #f5f9f8);
|
background: #fff;
|
||||||
box-shadow: 0 8rpx 28rpx rgba(56, 88, 88, 0.035);
|
color: #514943;
|
||||||
color: #355b60;
|
|
||||||
|
|
||||||
&__heading, &__range, &__footer, &__legend { display: flex; align-items: center; justify-content: space-between; }
|
&__heading, &__range, &__footer, &__legend { display: flex; align-items: center; justify-content: space-between; }
|
||||||
&__eyebrow { font-family: Georgia, serif; font-size: 17rpx; letter-spacing: 3rpx; color: #75908f; }
|
&__title { font-size: 28rpx; font-weight: 500; }
|
||||||
&__title { margin-top: 8rpx; font-size: 30rpx; font-weight: 600; letter-spacing: 2rpx; }
|
&__period { margin-left: 16rpx; font-size: 21rpx; font-weight: 400; letter-spacing: 0; color: #8b817b; }
|
||||||
&__period { margin-left: 16rpx; font-size: 21rpx; font-weight: 400; letter-spacing: 0; color: #788b88; }
|
&__summary { display: flex; align-items: baseline; gap: 8rpx; font-size: 21rpx; color: #8b817b; }
|
||||||
&__summary { display: flex; align-items: baseline; gap: 8rpx; font-size: 21rpx; color: #788b88; }
|
&__number { font-size: 44rpx; font-weight: 400; line-height: 1; color: #617d73; font-variant-numeric: tabular-nums; }
|
||||||
&__number { font-family: Georgia, serif; font-size: 56rpx; line-height: 1; color: #355b60; font-variant-numeric: tabular-nums; }
|
&__range { margin: 24rpx 0 12rpx; font-size: 20rpx; color: #8b817b; }
|
||||||
&__range { margin: 24rpx 0 12rpx; font-size: 20rpx; color: #788b88; }
|
|
||||||
&__grid { display: grid; grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 9rpx; }
|
&__grid { display: grid; grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 9rpx; }
|
||||||
&__cell {
|
&__cell {
|
||||||
width: 100%; height: 44rpx; min-width: 0; margin: 0; padding: 0;
|
width: 100%; height: 44rpx; min-width: 0; margin: 0; padding: 0;
|
||||||
@@ -111,19 +108,19 @@ onUnmounted(() => { disposed = true })
|
|||||||
border-radius: 8rpx; border: 2rpx solid transparent; box-sizing: border-box;
|
border-radius: 8rpx; border: 2rpx solid transparent; box-sizing: border-box;
|
||||||
font-size: 19rpx; line-height: 1; font-variant-numeric: tabular-nums;
|
font-size: 19rpx; line-height: 1; font-variant-numeric: tabular-nums;
|
||||||
&::after { border: none; }
|
&::after { border: none; }
|
||||||
&--0 { background: #e9eeeb; color: #768780; }
|
&--0 { background: #edf0e9; color: #7c8676; }
|
||||||
&--1 { background: #b8d4ce; color: #355b60; }
|
&--1 { background: #ccd9c6; color: #516d50; }
|
||||||
&--2 { background: #77a9a2; color: #193f43; }
|
&--2 { background: #9cb594; color: #304b36; }
|
||||||
&--3 { background: #3e7575; color: #ffffff; }
|
&--3 { background: #637f5f; color: #ffffff; }
|
||||||
&--today { border-bottom-color: #355b60; }
|
&--today { border-bottom-color: #516d50; }
|
||||||
&--selected { box-shadow: 0 0 0 3rpx #fff, 0 0 0 5rpx #527f80; }
|
&--selected { box-shadow: 0 0 0 3rpx #fff, 0 0 0 5rpx #637f5f; }
|
||||||
}
|
}
|
||||||
&__grid--loading { opacity: 0.5; }
|
&__grid--loading { opacity: 0.5; }
|
||||||
&__footer { margin-top: 22rpx; gap: 12rpx; flex-wrap: wrap; }
|
&__footer { margin-top: 22rpx; gap: 12rpx; flex-wrap: wrap; }
|
||||||
&__detail { font-size: 21rpx; color: #627c77; }
|
&__detail { font-size: 21rpx; color: #788477; }
|
||||||
&__legend { gap: 5rpx; font-size: 18rpx; color: #788b88; }
|
&__legend { gap: 5rpx; font-size: 18rpx; color: #8b817b; }
|
||||||
&__swatch { width: 13rpx; height: 13rpx; border-radius: 3rpx; }
|
&__swatch { width: 13rpx; height: 13rpx; border-radius: 3rpx; }
|
||||||
&__state { min-height: 200rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20rpx; font-size: 24rpx; color: #788b88; }
|
&__state { min-height: 200rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20rpx; font-size: 24rpx; color: #8b817b; }
|
||||||
&__retry { margin: 0; padding: 0 24rpx; line-height: 56rpx; font-size: 24rpx; color: #355b60; background: #e9eeeb; border-radius: 28rpx; &::after { border: none; } }
|
&__retry { margin: 0; padding: 0 24rpx; line-height: 56rpx; font-size: 24rpx; color: #516d50; background: #edf0e9; border-radius: 28rpx; &::after { border: none; } }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,33 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="profile-menu">
|
<view class="profile-menu">
|
||||||
<template v-for="item in menuItems" :key="item.key">
|
<view class="profile-menu__shortcuts">
|
||||||
<!-- Separator -->
|
<view v-for="item in primaryItems" :key="item.key" class="profile-menu__shortcut"
|
||||||
<view v-if="item.type === 'separator'" class="profile-menu__separator" />
|
hover-class="profile-menu__item--hover" @tap="handleTap(item)">
|
||||||
|
<view class="profile-menu__shortcut-head">
|
||||||
|
<view class="profile-menu__icon" :class="'profile-menu__icon--' + item.key" />
|
||||||
|
<text class="profile-menu__arrow">›</text>
|
||||||
|
</view>
|
||||||
|
<text class="profile-menu__shortcut-title">{{ item.title }}</text>
|
||||||
|
<text class="profile-menu__shortcut-note">{{ !requireAuth ? '登录后查看' : item.key === 'membership' ? `${activeMembershipCount || 0} 张可用` : `${upcomingBookingCount || 0} 节待上` }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- Menu item -->
|
<slot />
|
||||||
<view
|
|
||||||
v-else
|
<view class="profile-menu__links">
|
||||||
class="profile-menu__item"
|
<template v-for="item in secondaryItems" :key="item.key">
|
||||||
:class="{ 'profile-menu__item--admin': item.isAdmin }"
|
<view v-if="item.type === 'separator'" class="profile-menu__separator" />
|
||||||
hover-class="profile-menu__item--hover"
|
<view v-else class="profile-menu__item" :class="{ 'profile-menu__item--admin': item.isAdmin }"
|
||||||
hover-stay-time="150"
|
hover-class="profile-menu__item--hover" hover-stay-time="150" @tap="handleTap(item)">
|
||||||
@tap="handleTap(item)"
|
<text class="profile-menu__title">{{ item.title }}</text>
|
||||||
>
|
|
||||||
<view
|
|
||||||
class="profile-menu__icon-wrap"
|
|
||||||
:class="[
|
|
||||||
`profile-menu__icon-wrap--${item.key}`,
|
|
||||||
{ 'profile-menu__icon-wrap--admin': item.isAdmin },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
<text class="profile-menu__title" :class="{ 'profile-menu__title--admin': item.isAdmin }">
|
|
||||||
{{ item.title }}
|
|
||||||
</text>
|
|
||||||
<text v-if="item.badge" class="profile-menu__badge">{{ item.badge }}</text>
|
|
||||||
<text class="profile-menu__arrow">›</text>
|
<text class="profile-menu__arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -135,6 +132,9 @@ const menuItems = computed<MenuItem[]>(() => {
|
|||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const primaryItems = computed(() => menuItems.value.filter(item => item.key === 'membership' || item.key === 'bookings'))
|
||||||
|
const secondaryItems = computed(() => menuItems.value.filter(item => item.key !== 'membership' && item.key !== 'bookings'))
|
||||||
|
|
||||||
function handleTap(item: MenuItem) {
|
function handleTap(item: MenuItem) {
|
||||||
if (item.requireAuth && !props.requireAuth) {
|
if (item.requireAuth && !props.requireAuth) {
|
||||||
emit('require-login')
|
emit('require-login')
|
||||||
@@ -150,269 +150,20 @@ function handleTap(item: MenuItem) {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.profile-menu {
|
.profile-menu {
|
||||||
background: $bg-card;
|
&__shortcuts { display: flex; gap: 20rpx; margin: 24rpx 32rpx 0; }
|
||||||
border-radius: $radius-lg;
|
&__shortcut { flex: 1; min-width: 0; padding: 24rpx; box-sizing: border-box; border-radius: 26rpx; background: #fff; border: 1rpx solid #eee8e3; }
|
||||||
margin: $spacing-lg $spacing-lg 0;
|
&__shortcut-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx; }
|
||||||
overflow: hidden;
|
&__shortcut-title { display: block; font-size: 27rpx; font-weight: 500; color: #514943; }
|
||||||
|
&__shortcut-note { display: block; font-size: 22rpx; color: #8b817b; margin-top: 8rpx; }
|
||||||
&__separator {
|
&__icon { width: 36rpx; height: 30rpx; position: relative; box-sizing: border-box; border: 2rpx solid #9b8878; border-radius: 6rpx; }
|
||||||
height: $spacing-sm;
|
&__icon--membership::after { content: ''; position: absolute; top: 8rpx; left: 0; right: 0; border-top: 2rpx solid #9b8878; }
|
||||||
background: $bg-page;
|
&__icon--bookings { border-color: #829a8b; border-top-width: 7rpx; &::after { content: ''; position: absolute; left: 8rpx; top: 8rpx; width: 12rpx; border-top: 2rpx solid #829a8b; } }
|
||||||
}
|
&__links { margin: 24rpx 32rpx 0; border-radius: 26rpx; background: #fff; overflow: hidden; }
|
||||||
|
&__item { display: flex; align-items: center; gap: 20rpx; min-height: 96rpx; padding: 0 28rpx; box-sizing: border-box; border-bottom: 1rpx solid #f2ede8; &:last-child { border-bottom: none; } }
|
||||||
&__item {
|
&__item--hover { background: #f4f1ec; }
|
||||||
display: flex;
|
&__item--admin { background: #eff3ed; .profile-menu__title { color: #617d73; } }
|
||||||
align-items: center;
|
&__title { flex: 1; font-size: 27rpx; font-weight: 400; color: #6f655e; }
|
||||||
padding: $spacing-md $spacing-lg;
|
&__arrow { font-size: 30rpx; color: #b2a79e; line-height: 1; }
|
||||||
border-bottom: 1rpx solid $border-color;
|
&__separator { height: 12rpx; background: #fbf9f6; }
|
||||||
background: $bg-card;
|
|
||||||
transition: background 0.15s;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--admin {
|
|
||||||
background: rgba($accent-color, 0.04);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon-wrap {
|
|
||||||
width: 56rpx;
|
|
||||||
height: 56rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-right: $spacing-md;
|
|
||||||
position: relative;
|
|
||||||
background: rgba($brand-color, 0.06);
|
|
||||||
|
|
||||||
// ─── Pure CSS Icons ────────────────────────────────
|
|
||||||
|
|
||||||
// 会员卡 — 圆角矩形卡片 + 横线
|
|
||||||
&--membership {
|
|
||||||
background: rgba($accent-color, 0.10);
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
width: 26rpx;
|
|
||||||
height: 18rpx;
|
|
||||||
border: 2.5rpx solid $accent-color;
|
|
||||||
border-radius: 4rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, 1rpx);
|
|
||||||
width: 16rpx;
|
|
||||||
height: 0;
|
|
||||||
border-top: 2.5rpx solid $accent-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预约 — 日历(矩形 + 顶部两个小竖线)
|
|
||||||
&--bookings {
|
|
||||||
background: rgba($brand-color, 0.06);
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
width: 24rpx;
|
|
||||||
height: 22rpx;
|
|
||||||
border: 2.5rpx solid $brand-color;
|
|
||||||
border-radius: 4rpx;
|
|
||||||
border-top-width: 5rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 14rpx;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 10rpx;
|
|
||||||
height: 0;
|
|
||||||
border-top: 2.5rpx solid $brand-color;
|
|
||||||
// 用 box-shadow 模拟两个竖线
|
|
||||||
box-shadow:
|
|
||||||
-4rpx -7rpx 0 0 $brand-color,
|
|
||||||
4rpx -7rpx 0 0 $brand-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--teaching-schedule {
|
|
||||||
background: rgba(93, 140, 138, 0.12);
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
width: 24rpx;
|
|
||||||
height: 22rpx;
|
|
||||||
border: 2.5rpx solid #476d72;
|
|
||||||
border-radius: 6rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
width: 12rpx;
|
|
||||||
height: 12rpx;
|
|
||||||
transform: translate(-30%, -18%) rotate(45deg);
|
|
||||||
border-top: 2.5rpx solid #476d72;
|
|
||||||
border-left: 2.5rpx solid #476d72;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--invite {
|
|
||||||
background: rgba(255, 122, 69, 0.12);
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
width: 20rpx;
|
|
||||||
height: 20rpx;
|
|
||||||
border: 2.5rpx solid #ff7a45;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
top: 12rpx;
|
|
||||||
left: 14rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
box-shadow: 16rpx 8rpx 0 -2rpx rgba(255, 122, 69, 0.95);
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 22rpx;
|
|
||||||
height: 12rpx;
|
|
||||||
border: 2.5rpx solid #ff7a45;
|
|
||||||
border-top: none;
|
|
||||||
border-radius: 0 0 14rpx 14rpx;
|
|
||||||
left: 17rpx;
|
|
||||||
bottom: 13rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 个人信息 — 人形(圆 + 肩弧)
|
|
||||||
&--info {
|
|
||||||
background: rgba($brand-color, 0.06);
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
width: 12rpx;
|
|
||||||
height: 12rpx;
|
|
||||||
border: 2.5rpx solid $brand-color;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
top: 16rpx;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
width: 22rpx;
|
|
||||||
height: 10rpx;
|
|
||||||
border: 2.5rpx solid $brand-color;
|
|
||||||
border-bottom: none;
|
|
||||||
border-radius: 12rpx 12rpx 0 0;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 13rpx;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除缓存 — 旋转的刷新箭头(圆弧)
|
|
||||||
&--clear {
|
|
||||||
background: rgba($text-hint, 0.08);
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
width: 20rpx;
|
|
||||||
height: 20rpx;
|
|
||||||
border: 2.5rpx solid $text-secondary;
|
|
||||||
border-radius: 50%;
|
|
||||||
border-right-color: transparent;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 14rpx;
|
|
||||||
right: 15rpx;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 5rpx solid $text-secondary;
|
|
||||||
border-top: 4rpx solid transparent;
|
|
||||||
border-bottom: 4rpx solid transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 管理中心 — 齿轮(圆 + 四个刻度)
|
|
||||||
&--admin {
|
|
||||||
background: rgba($accent-color, 0.12);
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
width: 14rpx;
|
|
||||||
height: 14rpx;
|
|
||||||
border: 2.5rpx solid $accent-color;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 24rpx;
|
|
||||||
height: 24rpx;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
// 四条刻度线用 box-shadow 实现
|
|
||||||
background:
|
|
||||||
linear-gradient($accent-color, $accent-color) center top / 2.5rpx 5rpx no-repeat,
|
|
||||||
linear-gradient($accent-color, $accent-color) center bottom / 2.5rpx 5rpx no-repeat,
|
|
||||||
linear-gradient($accent-color, $accent-color) left center / 5rpx 2.5rpx no-repeat,
|
|
||||||
linear-gradient($accent-color, $accent-color) right center / 5rpx 2.5rpx no-repeat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 30rpx;
|
|
||||||
color: $text-primary;
|
|
||||||
|
|
||||||
&--admin {
|
|
||||||
color: $accent-color;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__badge {
|
|
||||||
font-size: 22rpx;
|
|
||||||
line-height: 1;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #8f6759;
|
|
||||||
background: linear-gradient(135deg, rgba(255, 248, 244, 0.98), rgba(241, 228, 220, 0.96));
|
|
||||||
border-radius: 999rpx;
|
|
||||||
padding: 9rpx 18rpx;
|
|
||||||
margin-right: $spacing-sm;
|
|
||||||
border: 1rpx solid rgba(192, 154, 137, 0.16);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 1rpx 0 rgba(255, 255, 255, 0.92),
|
|
||||||
0 6rpx 16rpx rgba(143, 103, 89, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__arrow {
|
|
||||||
font-size: 36rpx;
|
|
||||||
color: $text-hint;
|
|
||||||
line-height: 1;
|
|
||||||
transform: scaleX(0.6);
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<view class="quick-entry">
|
<view class="quick-entry">
|
||||||
<!-- ① Not logged in -->
|
<!-- ① Not logged in -->
|
||||||
<view v-if="!userStore.loggedIn" class="entry-pill pill-login" @tap="handleLogin">
|
<view v-if="!userStore.loggedIn" class="entry-pill pill-login" @tap="handleLogin">
|
||||||
<view class="pill-dot dot-login" />
|
|
||||||
<text class="pill-label">欢迎来到工作室</text>
|
<text class="pill-label">欢迎来到工作室</text>
|
||||||
<view class="pill-action action-login">
|
<view class="pill-action action-login">
|
||||||
<text class="pill-action-text">微信登录</text>
|
<text class="pill-action-text">微信登录</text>
|
||||||
@@ -15,7 +14,7 @@
|
|||||||
class="entry-pill pill-trial"
|
class="entry-pill pill-trial"
|
||||||
@tap="handleTrialEntry"
|
@tap="handleTrialEntry"
|
||||||
>
|
>
|
||||||
<view class="pill-tag tag-trial">体验</view>
|
|
||||||
<text class="pill-label">首次体验专属课程</text>
|
<text class="pill-label">首次体验专属课程</text>
|
||||||
<view class="pill-action action-trial">
|
<view class="pill-action action-trial">
|
||||||
<text class="pill-action-text">预约体验课</text>
|
<text class="pill-action-text">预约体验课</text>
|
||||||
@@ -25,10 +24,9 @@
|
|||||||
<!-- ③ Has valid active card -->
|
<!-- ③ Has valid active card -->
|
||||||
<template v-else-if="userStore.hasValidMembership">
|
<template v-else-if="userStore.hasValidMembership">
|
||||||
<view class="entry-pill pill-active" @tap="handleBooking">
|
<view class="entry-pill pill-active" @tap="handleBooking">
|
||||||
<view class="pill-dot dot-active" />
|
|
||||||
<text class="pill-label pill-label-active">{{ activeMembershipLabel }}</text>
|
<text class="pill-label pill-label-active">{{ activeMembershipLabel }}</text>
|
||||||
<view class="pill-action action-book">
|
<view class="pill-action action-book">
|
||||||
<text class="pill-action-text">约课</text>
|
<text class="pill-action-text">预约课程</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -45,8 +43,7 @@
|
|||||||
class="entry-pill pill-expired"
|
class="entry-pill pill-expired"
|
||||||
@tap="handleRenew"
|
@tap="handleRenew"
|
||||||
>
|
>
|
||||||
<view class="pill-dot dot-expired" />
|
<text class="pill-label">暂无可用会员卡</text>
|
||||||
<text class="pill-label">会员卡已到期</text>
|
|
||||||
<view class="pill-action action-renew">
|
<view class="pill-action action-renew">
|
||||||
<text class="pill-action-text">续卡</text>
|
<text class="pill-action-text">续卡</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -130,160 +127,15 @@ const renewStripText = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.quick-entry {
|
.quick-entry { margin: 24rpx 32rpx 0; }
|
||||||
padding: 20rpx 24rpx 0;
|
.entry-pill { display: flex; align-items: center; min-height: 120rpx; box-sizing: border-box; padding: 24rpx; gap: 20rpx; border-radius: 28rpx; background: #f1e8e1; }
|
||||||
}
|
.pill-active { background: #edf2ec; }
|
||||||
|
.pill-expired { background: #f0ece7; }
|
||||||
/* ── Pill base ── */
|
.pill-label { flex: 1; min-width: 0; font-size: 26rpx; line-height: 1.6; color: #6f5c50; overflow-wrap: anywhere; }
|
||||||
.entry-pill {
|
.pill-label-active { color: #526b5d; }
|
||||||
display: flex;
|
.pill-action { flex-shrink: 0; min-height: 68rpx; padding: 0 24rpx; border-radius: 999rpx; display: flex; align-items: center; justify-content: center; background: #6b8276; }
|
||||||
align-items: center;
|
.pill-action-text { font-size: 24rpx; font-weight: 400; color: #fff; white-space: nowrap; }
|
||||||
height: 80rpx;
|
.renew-strip { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; padding: 18rpx 12rpx 0; }
|
||||||
border-radius: 40rpx;
|
.renew-strip-text { font-size: 22rpx; color: #8b817b; line-height: 1.5; }
|
||||||
padding: 0 8rpx 0 24rpx;
|
.renew-strip-arrow { font-size: 28rpx; color: #8b817b; }
|
||||||
gap: 16rpx;
|
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Pill variants ── */
|
|
||||||
.pill-login {
|
|
||||||
background: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-trial {
|
|
||||||
background: linear-gradient(135deg, #2d2d5e 0%, #4a3f7a 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-active {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1rpx solid rgba(0, 0, 0, 0.06);
|
|
||||||
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-expired {
|
|
||||||
background: #f5f5f5;
|
|
||||||
border: 1rpx solid rgba(0, 0, 0, 0.04);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Status dot ── */
|
|
||||||
.pill-dot {
|
|
||||||
width: 14rpx;
|
|
||||||
height: 14rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-login {
|
|
||||||
background: $primary-color;
|
|
||||||
box-shadow: 0 0 8rpx rgba($primary-color, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-active {
|
|
||||||
background: #34c759;
|
|
||||||
box-shadow: 0 0 8rpx rgba(52, 199, 89, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot-expired {
|
|
||||||
background: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Tag (trial only) ── */
|
|
||||||
.pill-tag {
|
|
||||||
font-size: 20rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 4rpx 14rpx;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
letter-spacing: 1rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-trial {
|
|
||||||
background: rgba(255, 215, 0, 0.25);
|
|
||||||
color: #ffd700;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Label text ── */
|
|
||||||
.pill-label {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 26rpx;
|
|
||||||
font-weight: 500;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-label-active {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-expired .pill-label {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Action button ── */
|
|
||||||
.pill-action {
|
|
||||||
flex-shrink: 0;
|
|
||||||
height: 60rpx;
|
|
||||||
padding: 0 28rpx;
|
|
||||||
border-radius: 30rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-action-text {
|
|
||||||
font-size: 24rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-login {
|
|
||||||
background: $primary-color;
|
|
||||||
.pill-action-text { color: #1a1a2e; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-trial {
|
|
||||||
background: rgba(255, 215, 0, 0.2);
|
|
||||||
.pill-action-text { color: #ffd700; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-book {
|
|
||||||
background: #1a1a2e;
|
|
||||||
.pill-action-text { color: #fff; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-renew {
|
|
||||||
background: #e8efe9;
|
|
||||||
.pill-action-text { color: #5f7a6e; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Renew strip (running low) ── */
|
|
||||||
.renew-strip {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8rpx;
|
|
||||||
margin-top: 12rpx;
|
|
||||||
padding: 14rpx 24rpx;
|
|
||||||
background: linear-gradient(135deg, #8fa89a, #a8c0b4);
|
|
||||||
border-radius: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.renew-strip-text {
|
|
||||||
font-size: 22rpx;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #fffcfa;
|
|
||||||
letter-spacing: 0.5rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.renew-strip-arrow {
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: rgba(255, 252, 250, 0.82);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,94 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="slot-card-wrapper" :class="[`status-${statusClass}`]" @tap="emit('cardTap', timeSlot)">
|
<view class="slot-card" :class="`slot-card--${statusClass}`" @tap="emit('cardTap', timeSlot)">
|
||||||
<!-- Ticket background image -->
|
<view class="slot-heading">
|
||||||
<image
|
<view class="slot-time">
|
||||||
class="ticket-bg"
|
<text class="time-start">{{ startTimeDisplay }}</text>
|
||||||
src="/static/courseBg.png"
|
<text class="time-end">— {{ endTimeDisplay }}</text>
|
||||||
mode="aspectFill"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Card content overlay -->
|
|
||||||
<view class="ticket-content">
|
|
||||||
<!-- ── Top section: Time row (like flight ticket) ── -->
|
|
||||||
<view class="ticket-top">
|
|
||||||
<!-- Left: Start time -->
|
|
||||||
<view class="time-block">
|
|
||||||
<text class="time-main">{{ startTimeDisplay }}</text>
|
|
||||||
<text class="time-label">{{ timeSlot.date }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
<text class="slot-duration">{{ durationMin }} 分钟</text>
|
||||||
<!-- Center: Duration + icon -->
|
|
||||||
<view class="duration-block">
|
|
||||||
<view class="duration-line">
|
|
||||||
<view class="line-dot" />
|
|
||||||
<view class="line-dash" />
|
|
||||||
<view class="duration-icon">
|
|
||||||
<text class="icon-text">⏱</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="line-dash" />
|
<view class="slot-body">
|
||||||
<view class="line-dot" />
|
|
||||||
</view>
|
|
||||||
<text class="duration-text">{{ durationMin }}分钟</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- Right: End time -->
|
|
||||||
<view class="time-block time-block--right">
|
|
||||||
<text class="time-main">{{ endTimeDisplay }}</text>
|
|
||||||
<view class="capacity-tag" :class="capacityClass">
|
|
||||||
<text class="capacity-text">{{ capacityLabel }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- ── Dashed tear-off line ── -->
|
|
||||||
<view class="tear-line" />
|
|
||||||
|
|
||||||
<!-- ── Bottom section: Course name + Action ── -->
|
|
||||||
<view class="ticket-bottom">
|
|
||||||
<view class="course-info">
|
<view class="course-info">
|
||||||
<text class="course-name">普拉提私教</text>
|
<text class="course-name">普拉提私教</text>
|
||||||
|
<text class="slot-status">{{ capacityLabel }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Action area -->
|
|
||||||
<view class="action-area">
|
<view class="action-area">
|
||||||
<!-- Expired -->
|
<template v-if="timeSlot.isBookedByMe">
|
||||||
<template v-if="isPast && !timeSlot.isBookedByMe">
|
<text class="booked-label">已预约</text>
|
||||||
<view class="action-badge badge-expired">
|
<view v-if="timeSlot.status === TimeSlotStatus.OPEN" class="cancel-link" @tap.stop="emit('cancel', timeSlot)">
|
||||||
<text>已过期</text>
|
<text>取消预约</text>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
<button v-else-if="!isPast && timeSlot.status === TimeSlotStatus.OPEN && timeSlot.bookedCount < timeSlot.capacity"
|
||||||
<!-- OPEN + not booked -->
|
class="book-button" hover-class="book-button--pressed" @tap.stop="emit('book', timeSlot)">
|
||||||
<template v-else-if="timeSlot.status === TimeSlotStatus.OPEN && !timeSlot.isBookedByMe">
|
预约
|
||||||
<view class="action-btn btn-book" @tap.stop="emit('book', timeSlot)">
|
</button>
|
||||||
<text>预约</text>
|
<text v-else class="unavailable-label">{{ isPast ? '已结束' : timeSlot.status === TimeSlotStatus.CLOSED ? '未开放' : '已约满' }}</text>
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- OPEN + booked by me -->
|
|
||||||
<template v-else-if="timeSlot.status === TimeSlotStatus.OPEN && timeSlot.isBookedByMe">
|
|
||||||
<view class="action-badge badge-booked">
|
|
||||||
<text>{{ myBookingLabel }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="cancel-link" @tap.stop="emit('cancel', timeSlot)">
|
|
||||||
<text>取消</text>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- FULL -->
|
|
||||||
<template v-else-if="timeSlot.status === TimeSlotStatus.FULL">
|
|
||||||
<view class="action-badge badge-full">
|
|
||||||
<text>已约满</text>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- CLOSED -->
|
|
||||||
<template v-else>
|
|
||||||
<view class="action-badge badge-closed">
|
|
||||||
<text>已关闭</text>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -122,24 +57,17 @@ const durationMin = computed(() => {
|
|||||||
|
|
||||||
const myBookingLabel = computed(() => (
|
const myBookingLabel = computed(() => (
|
||||||
props.timeSlot.myBookingStatus === BookingStatus.PENDING_CONFIRMATION
|
props.timeSlot.myBookingStatus === BookingStatus.PENDING_CONFIRMATION
|
||||||
? '已预约待确认'
|
? '待老师确认'
|
||||||
: '已预约'
|
: '已预约'
|
||||||
))
|
))
|
||||||
|
|
||||||
const capacityLabel = computed(() => {
|
const capacityLabel = computed(() => {
|
||||||
const { bookedCount, capacity, status } = props.timeSlot
|
const { bookedCount, capacity, status, isBookedByMe } = props.timeSlot
|
||||||
if (status === TimeSlotStatus.CLOSED) return '已关闭'
|
if (isBookedByMe) return myBookingLabel.value
|
||||||
if (status === TimeSlotStatus.FULL) return '已约满'
|
if (isPast.value) return '已结束'
|
||||||
const remaining = capacity - bookedCount
|
if (status === TimeSlotStatus.CLOSED) return '暂未开放'
|
||||||
return `剩余${remaining}位`
|
if (status === TimeSlotStatus.FULL || bookedCount >= capacity) return '名额已满'
|
||||||
})
|
return `还可约 ${Math.max(0, capacity - bookedCount)} 位`
|
||||||
|
|
||||||
const capacityClass = computed(() => {
|
|
||||||
const { bookedCount, capacity, status } = props.timeSlot
|
|
||||||
if (status === TimeSlotStatus.CLOSED) return 'cap-closed'
|
|
||||||
if (status === TimeSlotStatus.FULL) return 'cap-full'
|
|
||||||
if (bookedCount >= capacity * 0.8) return 'cap-almost'
|
|
||||||
return 'cap-open'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const statusClass = computed(() => {
|
const statusClass = computed(() => {
|
||||||
@@ -154,276 +82,39 @@ const isPast = computed(() => isSlotPast(props.timeSlot.date, props.timeSlot.sta
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
/* ─── Wrapper ─── */
|
.slot-card {
|
||||||
.slot-card-wrapper {
|
margin: 0 32rpx 20rpx;
|
||||||
position: relative;
|
padding: 28rpx;
|
||||||
margin: 0 24rpx 20rpx;
|
border: 1rpx solid #eee8e3;
|
||||||
min-height: 220rpx;
|
border-radius: 28rpx;
|
||||||
transition: all 0.2s ease;
|
background: #fff;
|
||||||
filter: drop-shadow(0 16rpx 28rpx rgba(120, 91, 79, 0.08));
|
color: #514943;
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
}
|
||||||
|
.slot-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 16rpx; }
|
||||||
/* Status-based opacity */
|
.slot-time { display: flex; align-items: baseline; gap: 12rpx; font-variant-numeric: tabular-nums; }
|
||||||
&.status-expired {
|
.time-start { font-size: 40rpx; font-weight: 500; line-height: 1.2; }
|
||||||
opacity: 0.55;
|
.time-end { font-size: 28rpx; color: #8b817b; }
|
||||||
|
.slot-duration { flex-shrink: 0; font-size: 22rpx; color: #8b817b; }
|
||||||
|
.slot-body { display: flex; align-items: center; justify-content: space-between; gap: 20rpx; margin-top: 24rpx; }
|
||||||
|
.course-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8rpx; }
|
||||||
|
.course-name { font-size: 26rpx; font-weight: 400; }
|
||||||
|
.slot-status { font-size: 22rpx; color: #617d73; }
|
||||||
|
.action-area { display: flex; flex-direction: column; align-items: flex-end; gap: 2rpx; flex-shrink: 0; }
|
||||||
|
.book-button {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
min-width: 136rpx; height: 72rpx; margin: 0; padding: 0 30rpx;
|
||||||
|
border: none; border-radius: 999rpx; background: #6b8276;
|
||||||
|
font-size: 26rpx; font-weight: 400; line-height: 1; color: #fff;
|
||||||
|
&::after { border: none; }
|
||||||
|
&--pressed { background: #597264; }
|
||||||
}
|
}
|
||||||
|
.booked-label { font-size: 24rpx; color: #617d73; }
|
||||||
&.status-full,
|
.cancel-link { padding: 12rpx 0 8rpx 20rpx; font-size: 21rpx; color: #8b817b; }
|
||||||
&.status-closed {
|
.unavailable-label { padding: 14rpx 24rpx; border-radius: 999rpx; background: #f3f0ed; color: #8b817b; font-size: 23rpx; }
|
||||||
opacity: 0.75;
|
.slot-card--booked { background: #f2f5f0; border-color: #e2e9df; }
|
||||||
}
|
.slot-card--expired, .slot-card--closed, .slot-card--full {
|
||||||
}
|
background: #f7f5f2;
|
||||||
|
.time-start, .course-name { color: #8b817b; }
|
||||||
/* ─── Ticket background image ─── */
|
.slot-status { color: #948a82; }
|
||||||
.ticket-bg {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Content overlay ─── */
|
|
||||||
.ticket-content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 28rpx 40rpx 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ═══ Top section: Time row ═══ */
|
|
||||||
.ticket-top {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-block {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
min-width: 100rpx;
|
|
||||||
|
|
||||||
&--right {
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-main {
|
|
||||||
font-size: 40rpx;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #3a2e2a;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: 1rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-label {
|
|
||||||
margin-top: 8rpx;
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #a18a82;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Duration center block */
|
|
||||||
.duration-block {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
padding: 0 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.duration-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-dot {
|
|
||||||
width: 10rpx;
|
|
||||||
height: 10rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #ccb7ae;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-dash {
|
|
||||||
flex: 1;
|
|
||||||
height: 2rpx;
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
to right,
|
|
||||||
#dccbc2 0,
|
|
||||||
#dccbc2 8rpx,
|
|
||||||
transparent 8rpx,
|
|
||||||
transparent 16rpx
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.duration-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 48rpx;
|
|
||||||
height: 48rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(185, 143, 125, 0.12);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-text {
|
|
||||||
font-size: 22rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.duration-text {
|
|
||||||
margin-top: 6rpx;
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #a18a82;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Capacity tag */
|
|
||||||
.capacity-tag {
|
|
||||||
margin-top: 8rpx;
|
|
||||||
padding: 4rpx 12rpx;
|
|
||||||
border-radius: 6rpx;
|
|
||||||
font-size: 20rpx;
|
|
||||||
|
|
||||||
&.cap-open {
|
|
||||||
background: rgba(101, 163, 126, 0.12);
|
|
||||||
|
|
||||||
.capacity-text {
|
|
||||||
color: #5d9472;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.cap-almost {
|
|
||||||
background: rgba(214, 161, 92, 0.14);
|
|
||||||
|
|
||||||
.capacity-text {
|
|
||||||
color: #b98543;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.cap-full {
|
|
||||||
background: rgba(216, 91, 87, 0.12);
|
|
||||||
|
|
||||||
.capacity-text {
|
|
||||||
color: #c96763;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.cap-closed {
|
|
||||||
background: rgba(111, 96, 91, 0.08);
|
|
||||||
|
|
||||||
.capacity-text {
|
|
||||||
color: #9d8f89;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.capacity-text {
|
|
||||||
font-size: 20rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ═══ Tear-off dashed line ═══ */
|
|
||||||
.tear-line {
|
|
||||||
margin: 20rpx -40rpx 16rpx;
|
|
||||||
height: 2rpx;
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
to right,
|
|
||||||
#e0dcd6 0,
|
|
||||||
#e0dcd6 10rpx,
|
|
||||||
transparent 10rpx,
|
|
||||||
transparent 20rpx
|
|
||||||
);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ═══ Bottom section ═══ */
|
|
||||||
.ticket-bottom {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.course-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.course-name {
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #3a2e2a;
|
|
||||||
letter-spacing: 1rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Action area ─── */
|
|
||||||
.action-area {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn,
|
|
||||||
.action-badge {
|
|
||||||
padding: 10rpx 24rpx;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
font-size: 26rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-book {
|
|
||||||
background: linear-gradient(135deg, #d5b9ab 0%, #b98f7d 100%);
|
|
||||||
color: #fffaf7;
|
|
||||||
box-shadow: 0 8rpx 18rpx rgba(143, 103, 89, 0.24);
|
|
||||||
min-width: 120rpx;
|
|
||||||
height: 60rpx;
|
|
||||||
transition: all 0.15s;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 0.85;
|
|
||||||
transform: scale(0.96);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-booked {
|
|
||||||
background: linear-gradient(135deg, rgba(247, 240, 235, 0.96), rgba(236, 225, 217, 0.98));
|
|
||||||
color: #8f6759;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-expired {
|
|
||||||
background: rgba(111, 96, 91, 0.08);
|
|
||||||
color: #9d8f89;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-full {
|
|
||||||
background: rgba(216, 91, 87, 0.12);
|
|
||||||
color: #c96763;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-closed {
|
|
||||||
background: rgba(111, 96, 91, 0.08);
|
|
||||||
color: #b5a8a1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-link {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #c96763;
|
|
||||||
font-weight: 500;
|
|
||||||
text-decoration: underline;
|
|
||||||
text-decoration-color: rgba(201, 103, 99, 0.28);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="studio-info">
|
<view class="studio-info">
|
||||||
|
<text class="section-title">来工作室坐坐</text>
|
||||||
<!-- Address + Chat row -->
|
<!-- Address + Chat row -->
|
||||||
<view class="location-row">
|
<view class="location-row">
|
||||||
<view class="location-left" @tap="handleAddressTap">
|
<view class="location-left" @tap="handleAddressTap">
|
||||||
<view class="location-icon" />
|
|
||||||
<view class="location-content">
|
<view class="location-content">
|
||||||
<text class="location-label">场馆地址</text>
|
<text class="location-label">场馆地址</text>
|
||||||
<text class="location-text">
|
<text class="location-text">
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<button class="chat-btn" open-type="contact">
|
<button class="chat-btn" open-type="contact">
|
||||||
<view class="chat-icon" />
|
<text>联系老师</text>
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
@tap="previewPhoto(idx)"
|
@tap="previewPhoto(idx)"
|
||||||
>
|
>
|
||||||
<image class="gallery-image" :src="photo" mode="aspectFill" />
|
<image class="gallery-image" :src="photo" mode="aspectFill" />
|
||||||
<view class="gallery-overlay" />
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@@ -88,179 +87,15 @@ function copyAddress() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.studio-info {
|
.studio-info { margin: 36rpx 32rpx 0; }
|
||||||
margin: 16rpx 24rpx 0;
|
.section-title { display: block; margin-bottom: 20rpx; font-size: 30rpx; font-weight: 500; color: #514943; }
|
||||||
background: #ffffff;
|
.location-row { display: flex; align-items: center; justify-content: space-between; gap: 20rpx; margin-bottom: 20rpx; }
|
||||||
border-radius: 20rpx;
|
.location-left, .location-content { flex: 1; min-width: 0; }
|
||||||
overflow: hidden;
|
.location-label { display: block; font-size: 21rpx; color: #8b817b; margin-bottom: 6rpx; }
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
.location-text { font-size: 24rpx; color: #6f655e; line-height: 1.7; overflow-wrap: anywhere; }
|
||||||
}
|
.chat-btn { flex-shrink: 0; margin: 0; padding: 0 20rpx; line-height: 64rpx; font-size: 23rpx; border: none; border-radius: 999rpx; background: #f0eae4; color: #78675c; &::after { border: none; } }
|
||||||
|
.gallery-scroll { width: 100%; }
|
||||||
/* ── Location row ── */
|
.gallery-track { display: inline-flex; gap: 16rpx; }
|
||||||
.location-row {
|
.gallery-item { width: 264rpx; height: 180rpx; border-radius: 20rpx; overflow: hidden; flex-shrink: 0; background: #e9e1d8; }
|
||||||
display: flex;
|
.gallery-image { width: 100%; height: 100%; }
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 28rpx 32rpx 24rpx;
|
|
||||||
gap: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.location-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12rpx;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.location-content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.location-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #b39a92;
|
|
||||||
letter-spacing: 2rpx;
|
|
||||||
margin-bottom: 6rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.location-text {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #5f5955;
|
|
||||||
line-height: 1.6;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Gallery ── */
|
|
||||||
.gallery-block {
|
|
||||||
padding: 6rpx 0 28rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-scroll {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-track {
|
|
||||||
display: flex;
|
|
||||||
gap: 14rpx;
|
|
||||||
padding: 0 32rpx;
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-item {
|
|
||||||
position: relative;
|
|
||||||
width: 192rpx;
|
|
||||||
height: 108rpx;
|
|
||||||
border-radius: 14rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: linear-gradient(135deg, #eadfd8 0%, #d5c0b4 100%);
|
|
||||||
box-shadow: 0 8rpx 18rpx rgba(124, 95, 82, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-overlay {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(38, 28, 24, 0.2) 100%),
|
|
||||||
linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, transparent 48%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Icons ── */
|
|
||||||
.location-icon {
|
|
||||||
width: 56rpx;
|
|
||||||
height: 56rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba($brand-color, 0.06);
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
// 定位图标 — 圆头 + 尖尾
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 14rpx;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 18rpx;
|
|
||||||
height: 18rpx;
|
|
||||||
border: 2.5rpx solid $brand-color;
|
|
||||||
border-radius: 50% 50% 50% 0;
|
|
||||||
transform: translateX(-50%) rotate(-45deg);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
// 中心白点
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 21rpx;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 6rpx;
|
|
||||||
height: 6rpx;
|
|
||||||
background: $brand-color;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-btn {
|
|
||||||
width: 72rpx;
|
|
||||||
height: 72rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba($brand-color, 0.06);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-btn::after {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-icon {
|
|
||||||
width: 56rpx;
|
|
||||||
height: 56rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba($brand-color, 0.06);
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
// 消息气泡
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 26rpx;
|
|
||||||
height: 20rpx;
|
|
||||||
border: 2.5rpx solid $brand-color;
|
|
||||||
border-radius: 6rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
// 气泡尾巴
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 12rpx;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 5rpx solid transparent;
|
|
||||||
border-right: 5rpx solid transparent;
|
|
||||||
border-top: 7rpx solid $brand-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ interface Tab {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: PeriodKey
|
modelValue: PeriodKey
|
||||||
variant?: 'default' | 'booking'
|
variant?: 'default' | 'booking' | 'soft'
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -117,4 +117,18 @@ function handleChange(key: PeriodKey) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time-period-filter.time-period-filter--soft {
|
||||||
|
margin: 0 32rpx 24rpx; padding: 6rpx;
|
||||||
|
border: none; border-radius: 999rpx; background: #f0ece7;
|
||||||
|
.tab-item {
|
||||||
|
padding: 14rpx 0; border-radius: 999rpx;
|
||||||
|
.tab-label { font-size: 24rpx; color: #8b817b; }
|
||||||
|
&.active {
|
||||||
|
background: #fff;
|
||||||
|
.tab-label { color: #617d73; font-weight: 500; }
|
||||||
|
&::after { display: none; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
<view class="details-col">
|
<view class="details-col">
|
||||||
<text class="card-name">{{ booking.membership.cardType.name }}</text>
|
<text class="card-name">{{ booking.membership.cardType.name }}</text>
|
||||||
<view class="time-row">
|
<view class="time-row">
|
||||||
<text class="time-icon">🕐</text>
|
|
||||||
<text class="time-text">
|
<text class="time-text">
|
||||||
{{ formatTime(booking.timeSlot.startTime) }} – {{ formatTime(booking.timeSlot.endTime) }}
|
{{ formatTime(booking.timeSlot.startTime) }} – {{ formatTime(booking.timeSlot.endTime) }}
|
||||||
</text>
|
</text>
|
||||||
@@ -108,132 +107,25 @@ function goToBookingDetail(id: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.upcoming-section {
|
.upcoming-section { margin: 36rpx 32rpx 0; }
|
||||||
margin: 24rpx 24rpx 0;
|
.section-header { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; margin-bottom: 20rpx; }
|
||||||
}
|
.section-title { font-size: 30rpx; font-weight: 500; color: #514943; }
|
||||||
|
.section-more { font-size: 23rpx; color: #8b817b; padding: 8rpx 0; }
|
||||||
.section-header {
|
.booking-card { display: flex; align-items: center; gap: 24rpx; margin-bottom: 16rpx; padding: 26rpx; border-radius: 28rpx; background: #fff; border: 1rpx solid #eee8e3; }
|
||||||
display: flex;
|
.date-col { display: flex; flex-direction: column; align-items: center; gap: 4rpx; width: 72rpx; flex-shrink: 0; }
|
||||||
align-items: center;
|
.date-day { font-size: 44rpx; font-weight: 400; line-height: 1.1; color: #617d73; }
|
||||||
justify-content: space-between;
|
.date-month, .date-weekday { font-size: 20rpx; color: #8b817b; }
|
||||||
margin-bottom: 16rpx;
|
.booking-divider { width: 1rpx; height: 80rpx; background: #eee8e3; flex-shrink: 0; }
|
||||||
}
|
.details-col { flex: 1; min-width: 0; }
|
||||||
|
.card-name { display: block; font-size: 26rpx; color: #514943; font-weight: 400; line-height: 1.5; margin-bottom: 8rpx; overflow-wrap: anywhere; }
|
||||||
.section-title {
|
.time-row { margin-bottom: 8rpx; }
|
||||||
font-size: 32rpx;
|
.time-text { font-size: 24rpx; color: #6f655e; font-variant-numeric: tabular-nums; }
|
||||||
font-weight: 700;
|
.status-row { display: flex; align-items: center; gap: 8rpx; }
|
||||||
color: #1a1a2e;
|
.status-dot { width: 8rpx; height: 8rpx; border-radius: 50%; background: #a5b8ab; }
|
||||||
}
|
.status-text { font-size: 21rpx; color: #617d73; }
|
||||||
|
.dot--pending { background: #b8a18b; }
|
||||||
.section-more {
|
.text--pending { color: #957c65; }
|
||||||
font-size: 26rpx;
|
.dot--cancelled { background: #b69589; }
|
||||||
color: $primary-dark;
|
.text--cancelled { color: #9c7a6e; }
|
||||||
}
|
.booking-arrow { font-size: 32rpx; color: #b2a79e; flex-shrink: 0; }
|
||||||
|
|
||||||
.booking-card {
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 16rpx;
|
|
||||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
|
||||||
padding: 28rpx 28rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24rpx;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-col {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 88rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-day {
|
|
||||||
font-size: 52rpx;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #1a1a2e;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-month {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: $primary-dark;
|
|
||||||
margin-top: 4rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-weekday {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #999;
|
|
||||||
margin-top: 4rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.booking-divider {
|
|
||||||
width: 2rpx;
|
|
||||||
height: 80rpx;
|
|
||||||
background: #f0f0f0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-col {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-name {
|
|
||||||
display: block;
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1a1a2e;
|
|
||||||
margin-bottom: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8rpx;
|
|
||||||
margin-bottom: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-icon {
|
|
||||||
font-size: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-text {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
width: 12rpx;
|
|
||||||
height: 12rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot--pending { background: #f39c12; }
|
|
||||||
.dot--confirmed { background: #27ae60; }
|
|
||||||
.dot--completed { background: #3498db; }
|
|
||||||
.dot--cancelled { background: #e74c3c; }
|
|
||||||
.dot--default { background: #999; }
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text--pending { color: #f39c12; }
|
|
||||||
.text--confirmed { color: #27ae60; }
|
|
||||||
.text--completed { color: #3498db; }
|
|
||||||
.text--cancelled { color: #e74c3c; }
|
|
||||||
|
|
||||||
.booking-arrow {
|
|
||||||
font-size: 36rpx;
|
|
||||||
color: #ccc;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="user-card">
|
<view class="user-card">
|
||||||
<!-- Header: gradient background, padded to sit below nav bar -->
|
<view class="user-card__header">
|
||||||
<view class="user-card__header" :style="{ paddingTop: (navBarHeight ?? 0) + 'px' }">
|
|
||||||
<!-- Not logged in state -->
|
<!-- Not logged in state -->
|
||||||
<view v-if="!loggedIn" class="user-card__guest">
|
<view v-if="!loggedIn" class="user-card__guest">
|
||||||
<view class="user-card__avatar-wrap">
|
<view class="user-card__avatar-wrap">
|
||||||
<image class="user-card__avatar-img" src="/static/default-avatar.jpg" mode="aspectFill" />
|
<image class="user-card__avatar-img" src="/static/default-avatar.jpg" mode="aspectFill" />
|
||||||
</view>
|
</view>
|
||||||
<view class="user-card__guest-info">
|
<view class="user-card__guest-info">
|
||||||
<text class="user-card__guest-title">Hi,欢迎来到普拉提</text>
|
<text class="user-card__guest-title">欢迎来到工作室</text>
|
||||||
<text class="user-card__guest-sub">登录后查看个人数据</text>
|
<text class="user-card__guest-sub">登录后查看会员卡与练习记录</text>
|
||||||
</view>
|
</view>
|
||||||
<button class="user-card__login-btn" :loading="loading" @tap="handleLogin">
|
<button class="user-card__login-btn" :loading="loading" @tap="handleLogin">
|
||||||
微信登录
|
微信登录
|
||||||
@@ -17,7 +16,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Logged in + profile loaded -->
|
<!-- Logged in + profile loaded -->
|
||||||
<view v-else-if="loggedIn && hasProfile" class="user-card__user">
|
<view v-else-if="loggedIn && hasProfile" class="user-card__user" @tap="emit('edit')">
|
||||||
<view class="user-card__avatar-wrap">
|
<view class="user-card__avatar-wrap">
|
||||||
<image
|
<image
|
||||||
class="user-card__avatar-img"
|
class="user-card__avatar-img"
|
||||||
@@ -29,15 +28,11 @@
|
|||||||
<view class="user-card__info">
|
<view class="user-card__info">
|
||||||
<view class="user-card__name-row">
|
<view class="user-card__name-row">
|
||||||
<text class="user-card__nickname">{{ user!.nickname }}</text>
|
<text class="user-card__nickname">{{ user!.nickname }}</text>
|
||||||
<view v-if="hasMembership" class="user-card__member-badge">
|
<text v-if="hasMembership" class="user-card__member-label">会员</text>
|
||||||
<view class="user-card__member-icon">
|
|
||||||
<text class="user-card__member-letter">C</text>
|
|
||||||
</view>
|
</view>
|
||||||
<text class="user-card__member-label">CLUB</text>
|
<text class="user-card__phone">{{ maskedPhone || '完善个人资料' }}</text>
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<text v-if="maskedPhone" class="user-card__phone">{{ maskedPhone }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
<text class="user-card__edit">资料 ›</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Logged in but profile still loading -->
|
<!-- Logged in but profile still loading -->
|
||||||
@@ -57,18 +52,18 @@
|
|||||||
<!-- Stats row: shown only when profile is loaded -->
|
<!-- Stats row: shown only when profile is loaded -->
|
||||||
<view v-if="loggedIn && hasProfile" class="user-card__stats">
|
<view v-if="loggedIn && hasProfile" class="user-card__stats">
|
||||||
<view class="user-card__stat-item">
|
<view class="user-card__stat-item">
|
||||||
<text class="user-card__stat-value">{{ stats?.totalBookings ?? 0 }}</text>
|
<text class="user-card__stat-value">{{ stats?.totalBookings ?? '—' }}</text>
|
||||||
<text class="user-card__stat-label">总训练(次)</text>
|
<text class="user-card__stat-label">累计上课 · 节</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="user-card__stat-divider" />
|
<view class="user-card__stat-divider" />
|
||||||
<view class="user-card__stat-item">
|
<view class="user-card__stat-item">
|
||||||
<text class="user-card__stat-value">{{ stats?.monthBookings ?? 0 }}</text>
|
<text class="user-card__stat-value">{{ stats?.monthBookings ?? '—' }}</text>
|
||||||
<text class="user-card__stat-label">本月(次)</text>
|
<text class="user-card__stat-label">本月上课 · 节</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="user-card__stat-divider" />
|
<view class="user-card__stat-divider" />
|
||||||
<view class="user-card__stat-item">
|
<view class="user-card__stat-item">
|
||||||
<text class="user-card__stat-value">{{ remainingSessions }}</text>
|
<text class="user-card__stat-value">{{ remainingSessions }}</text>
|
||||||
<text class="user-card__stat-label">剩余课时(节)</text>
|
<text class="user-card__stat-label">剩余课时 · 节</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -86,12 +81,11 @@ const props = defineProps<{
|
|||||||
stats: UserStatsResponse | null
|
stats: UserStatsResponse | null
|
||||||
memberships?: readonly MembershipWithCardType[]
|
memberships?: readonly MembershipWithCardType[]
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
/** Height of the custom nav bar in px, so header content starts below it */
|
|
||||||
navBarHeight?: number
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'login'): void
|
(e: 'login'): void
|
||||||
|
(e: 'edit'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const avatarFailed = ref(false)
|
const avatarFailed = ref(false)
|
||||||
@@ -151,221 +145,28 @@ function handleLogin() {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.user-card {
|
.user-card {
|
||||||
background: linear-gradient(160deg, #E1F4FA 0%, $primary-color 50%, $primary-dark 100%);
|
margin: 20rpx 32rpx 0; padding: 28rpx; background: #f2e9e3; border-radius: 32rpx;
|
||||||
border-radius: 0 0 40rpx 40rpx;
|
&__user, &__loading { display: flex; align-items: center; gap: 20rpx; }
|
||||||
overflow: hidden;
|
&__avatar-wrap { width: 108rpx; height: 108rpx; flex-shrink: 0; }
|
||||||
|
&__avatar-img { width: 100%; height: 100%; box-sizing: border-box; border-radius: 50%; border: 5rpx solid #fcf8f4; }
|
||||||
&__header {
|
&__info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10rpx; }
|
||||||
padding: $spacing-lg $spacing-lg $spacing-lg;
|
&__name-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10rpx; }
|
||||||
}
|
&__nickname { font-size: 36rpx; font-weight: 500; line-height: 1.4; color: #514943; overflow-wrap: anywhere; }
|
||||||
|
&__member-label { flex-shrink: 0; font-size: 19rpx; color: #8b6c5b; background: #fbf5ef; padding: 4rpx 12rpx; border-radius: 999rpx; }
|
||||||
// ── Guest state ──
|
&__phone { font-size: 24rpx; color: #8b7b70; }
|
||||||
&__guest {
|
&__edit { flex-shrink: 0; font-size: 22rpx; color: #8b7b70; padding: 16rpx 0 16rpx 8rpx; }
|
||||||
display: flex;
|
&__stats { display: flex; align-items: stretch; margin-top: 28rpx; padding-top: 26rpx; border-top: 1rpx solid #e3d7ce; }
|
||||||
align-items: center;
|
&__stat-item { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 10rpx; }
|
||||||
gap: $spacing-md;
|
&__stat-value { font-size: 40rpx; font-weight: 400; color: #6f5c50; line-height: 1.15; font-variant-numeric: tabular-nums; }
|
||||||
}
|
&__stat-label { font-size: 20rpx; color: #8b7b70; }
|
||||||
|
&__stat-divider { width: 1rpx; margin: 6rpx 0; background: #e3d7ce; }
|
||||||
&__guest-info {
|
&__guest { display: flex; align-items: center; flex-wrap: wrap; gap: 20rpx; }
|
||||||
flex: 1;
|
&__guest-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10rpx; }
|
||||||
display: flex;
|
&__guest-title { font-size: 32rpx; font-weight: 500; color: #514943; }
|
||||||
flex-direction: column;
|
&__guest-sub { font-size: 23rpx; color: #8b7b70; line-height: 1.6; }
|
||||||
gap: $spacing-xs;
|
&__login-btn { width: 100%; height: 76rpx; line-height: 76rpx; margin: 4rpx 0 0; padding: 0; border: none; border-radius: 999rpx; background: #6b8276; color: #fff; font-size: 26rpx; font-weight: 400; &::after { border: none; } }
|
||||||
}
|
&__avatar-skeleton { width: 108rpx; height: 108rpx; border-radius: 50%; background: #e6d9ce; }
|
||||||
|
&__nickname-skeleton { width: 150rpx; height: 32rpx; border-radius: 8rpx; background: #e6d9ce; }
|
||||||
&__guest-title {
|
&__phone-skeleton { width: 180rpx; height: 22rpx; border-radius: 6rpx; background: #e6d9ce; }
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__guest-sub {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: rgba(255, 255, 255, 0.65);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__login-btn {
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 26rpx;
|
|
||||||
font-weight: 500;
|
|
||||||
border: none;
|
|
||||||
border-radius: $radius-lg;
|
|
||||||
padding: 0 $spacing-md;
|
|
||||||
height: 64rpx;
|
|
||||||
line-height: 64rpx;
|
|
||||||
min-width: 160rpx;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Logged-in user ──
|
|
||||||
&__user {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: $spacing-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__avatar-wrap {
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 120rpx;
|
|
||||||
height: 120rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__avatar-img {
|
|
||||||
width: 120rpx;
|
|
||||||
height: 120rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 4rpx solid rgba(255, 255, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__info {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__name-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: $spacing-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__nickname {
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__member-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10rpx;
|
|
||||||
padding: 6rpx 14rpx 6rpx 8rpx;
|
|
||||||
border-radius: 999rpx;
|
|
||||||
background: linear-gradient(135deg, rgba(255, 249, 245, 0.98), rgba(242, 229, 221, 0.96));
|
|
||||||
border: 1rpx solid rgba(192, 154, 137, 0.18);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 1rpx 0 rgba(255, 255, 255, 0.9),
|
|
||||||
0 8rpx 20rpx rgba(143, 103, 89, 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__member-icon {
|
|
||||||
width: 34rpx;
|
|
||||||
height: 34rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: radial-gradient(circle at 30% 30%, #fffdfb 0%, #f2e2d8 40%, #c79d89 100%);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.76),
|
|
||||||
0 3rpx 8rpx rgba(143, 103, 89, 0.16);
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 3rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1.5rpx solid rgba(143, 103, 89, 0.28);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__member-letter {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
font-size: 20rpx;
|
|
||||||
line-height: 1;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #8f6759;
|
|
||||||
letter-spacing: 1rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__member-label {
|
|
||||||
font-size: 20rpx;
|
|
||||||
line-height: 1;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #8f6759;
|
|
||||||
letter-spacing: 2rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__phone {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: rgba(255, 255, 255, 0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Loading state ──
|
|
||||||
&__loading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: $spacing-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__avatar-skeleton {
|
|
||||||
width: 120rpx;
|
|
||||||
height: 120rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__nickname-skeleton {
|
|
||||||
width: 160rpx;
|
|
||||||
height: 36rpx;
|
|
||||||
border-radius: $radius-sm;
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__phone-skeleton {
|
|
||||||
width: 120rpx;
|
|
||||||
height: 26rpx;
|
|
||||||
border-radius: $radius-sm;
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
margin-top: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Stats row ──
|
|
||||||
&__stats {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
margin: 0 $spacing-lg $spacing-lg;
|
|
||||||
border-radius: $radius-lg;
|
|
||||||
padding: $spacing-md 0;
|
|
||||||
backdrop-filter: blur(10rpx);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__stat-item {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: $spacing-xs 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__stat-divider {
|
|
||||||
width: 1rpx;
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
margin: $spacing-xs 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__stat-value {
|
|
||||||
font-size: 44rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #ffffff;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__stat-label {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
margin-top: 6rpx;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="booking-page">
|
<view class="booking-page" :style="{ height: pageHeight }">
|
||||||
<!-- ──────────── Status bar spacing ──────────── -->
|
<!-- ──────────── Status bar spacing ──────────── -->
|
||||||
<view class="status-bar" :style="{ height: statusBarHeight }" />
|
<view class="status-bar" :style="{ height: statusBarHeight }" />
|
||||||
|
|
||||||
@@ -10,15 +10,18 @@
|
|||||||
|
|
||||||
<!-- ──────────── Date & period filters ──────────── -->
|
<!-- ──────────── Date & period filters ──────────── -->
|
||||||
<view class="filter-header">
|
<view class="filter-header">
|
||||||
<DateSelector v-model="selectedDate" variant="booking" @select="onDateSelect" />
|
<view class="calendar-heading">
|
||||||
<TimePeriodFilter v-model="selectedPeriod" variant="booking" @change="onPeriodChange" />
|
<text class="calendar-month">{{ selectedMonthLabel }}</text>
|
||||||
|
<text class="calendar-hint">选择上课日期</text>
|
||||||
|
</view>
|
||||||
|
<DateSelector v-model="selectedDate" variant="soft" @select="onDateSelect" />
|
||||||
|
<TimePeriodFilter v-model="selectedPeriod" variant="soft" @change="onPeriodChange" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- ──────────── Slot list ──────────── -->
|
<!-- ──────────── Slot list ──────────── -->
|
||||||
<scroll-view
|
<scroll-view
|
||||||
class="slot-scroll"
|
class="slot-scroll"
|
||||||
scroll-y
|
scroll-y
|
||||||
:style="{ height: scrollHeight }"
|
|
||||||
refresher-enabled
|
refresher-enabled
|
||||||
:refresher-triggered="refreshing"
|
:refresher-triggered="refreshing"
|
||||||
@refresherrefresh="onRefresh"
|
@refresherrefresh="onRefresh"
|
||||||
@@ -37,12 +40,7 @@
|
|||||||
|
|
||||||
<!-- Empty state -->
|
<!-- Empty state -->
|
||||||
<view v-else-if="filteredSlots.length === 0" class="empty-wrap">
|
<view v-else-if="filteredSlots.length === 0" class="empty-wrap">
|
||||||
<view class="empty-illustration">
|
<text class="empty-text">这个时段还没有课程</text>
|
||||||
<view class="empty-circle outer" />
|
|
||||||
<view class="empty-circle inner" />
|
|
||||||
<view class="empty-dot" />
|
|
||||||
</view>
|
|
||||||
<text class="empty-text">当日暂无可约时段</text>
|
|
||||||
<text class="empty-sub">请选择其他日期或时段查看</text>
|
<text class="empty-sub">请选择其他日期或时段查看</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -50,9 +48,8 @@
|
|||||||
<view v-else class="slot-list">
|
<view v-else class="slot-list">
|
||||||
<!-- Date summary -->
|
<!-- Date summary -->
|
||||||
<view class="date-summary">
|
<view class="date-summary">
|
||||||
<text class="date-summary-text">
|
<text class="date-summary-title">{{ selectedDayLabel }}</text>
|
||||||
共 {{ filteredSlots.length }} 个可选时段
|
<text class="date-summary-text">{{ filteredSlots.length }} 个时段</text>
|
||||||
</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<SlotCard
|
<SlotCard
|
||||||
@@ -82,7 +79,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
import { onResize, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||||
import type { TimeSlotWithBookingStatus, MembershipWithCardType } from '@mp-pilates/shared'
|
import type { TimeSlotWithBookingStatus, MembershipWithCardType } from '@mp-pilates/shared'
|
||||||
import { BookingStatus, TIME_PERIODS } from '@mp-pilates/shared'
|
import { BookingStatus, TIME_PERIODS } from '@mp-pilates/shared'
|
||||||
import { useBookingStore } from '../../stores/booking'
|
import { useBookingStore } from '../../stores/booking'
|
||||||
@@ -126,28 +123,26 @@ onShareTimeline(() => {
|
|||||||
|
|
||||||
// ─── Layout ───────────────────────────────────────────────
|
// ─── Layout ───────────────────────────────────────────────
|
||||||
const statusBarHeight = ref('20px')
|
const statusBarHeight = ref('20px')
|
||||||
const scrollHeight = ref('500px')
|
const pageHeight = ref('100vh')
|
||||||
// Heights of static elements above scroll-view (in rpx, converted to px)
|
|
||||||
const PAGE_HEADER_RPX = 88 // title bar height
|
|
||||||
const FILTER_HEADER_RPX = 240 // DateSelector + TimePeriodFilter
|
|
||||||
const TABBAR_RPX = 100
|
|
||||||
|
|
||||||
function updateLayout() {
|
function updateLayout() {
|
||||||
const { statusBarHeight: statusBarPx, windowWidth } = getSystemLayout()
|
statusBarHeight.value = `${getSystemLayout().statusBarHeight}px`
|
||||||
const ratio = windowWidth / 750
|
// The mini-program window already excludes its native tab bar. Flex layout
|
||||||
statusBarHeight.value = `${statusBarPx}px`
|
// gives the remaining height to the list as the filters change size.
|
||||||
|
pageHeight.value = `${uni.getWindowInfo().windowHeight}px`
|
||||||
const headerPx = Math.round(PAGE_HEADER_RPX * ratio)
|
|
||||||
const filterPx = Math.round(FILTER_HEADER_RPX * ratio)
|
|
||||||
const tabbarPx = Math.round(TABBAR_RPX * ratio)
|
|
||||||
|
|
||||||
// scroll-view fills remaining space: window - statusBar - pageHeader - filters - tabbar
|
|
||||||
const { windowHeight } = uni.getWindowInfo()
|
|
||||||
const remaining = windowHeight - statusBarPx - headerPx - filterPx - tabbarPx
|
|
||||||
scrollHeight.value = `${remaining}px`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLayout()
|
updateLayout()
|
||||||
|
onResize(updateLayout)
|
||||||
|
|
||||||
|
const selectedMonthLabel = computed(() => {
|
||||||
|
const [year, month] = selectedDate.value.split('-')
|
||||||
|
return `${year} 年 ${Number(month)} 月`
|
||||||
|
})
|
||||||
|
const selectedDayLabel = computed(() => {
|
||||||
|
const [, month, day] = selectedDate.value.split('-')
|
||||||
|
return `${Number(month)} 月 ${Number(day)} 日的课程`
|
||||||
|
})
|
||||||
|
|
||||||
// ─── Filtered slots ───────────────────────────────────────
|
// ─── Filtered slots ───────────────────────────────────────
|
||||||
const filteredSlots = computed<TimeSlotWithBookingStatus[]>(() => {
|
const filteredSlots = computed<TimeSlotWithBookingStatus[]>(() => {
|
||||||
@@ -310,216 +305,33 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.booking-page {
|
.booking-page { height: 100vh; background: #fbf9f6; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
height: 100vh;
|
.status-bar { flex-shrink: 0; }
|
||||||
background:
|
.page-header { flex-shrink: 0; height: 88rpx; display: flex; align-items: center; justify-content: center; }
|
||||||
radial-gradient(circle at top, rgba(255, 232, 218, 0.36), transparent 34%),
|
.page-title { font-size: 34rpx; font-weight: 500; color: #514943; }
|
||||||
linear-gradient(180deg, #fbf7f3 0%, #f6efea 100%);
|
.filter-header { flex-shrink: 0; border-bottom: 1rpx solid #eee8e3; }
|
||||||
display: flex;
|
.calendar-heading { display: flex; align-items: baseline; justify-content: space-between; padding: 24rpx 32rpx 16rpx; gap: 16rpx; }
|
||||||
flex-direction: column;
|
.calendar-month { font-size: 28rpx; font-weight: 500; color: #514943; }
|
||||||
overflow: hidden;
|
.calendar-hint { font-size: 22rpx; color: #8b817b; }
|
||||||
}
|
.slot-scroll { flex: 1; height: 0; min-height: 0; width: 100%; box-sizing: border-box; }
|
||||||
|
.slot-list { display: flex; flex-direction: column; padding-top: 28rpx; }
|
||||||
/* ── Status bar ───────────────────────────────────── */
|
.date-summary { display: flex; justify-content: space-between; align-items: baseline; gap: 16rpx; padding: 0 32rpx 20rpx; }
|
||||||
.status-bar {
|
.date-summary-title { font-size: 25rpx; color: #6f655e; }
|
||||||
flex-shrink: 0;
|
.date-summary-text { font-size: 22rpx; color: #8b817b; }
|
||||||
background: #fcfaf8;
|
.loading-wrap { display: flex; flex-direction: column; gap: 20rpx; padding: 28rpx 32rpx; }
|
||||||
}
|
.skeleton-card { height: 200rpx; box-sizing: border-box; border-radius: 28rpx; background: #fff; display: flex; align-items: center; padding: 28rpx; gap: 20rpx; }
|
||||||
|
.skeleton-time, .skeleton-title, .skeleton-sub, .skeleton-btn {
|
||||||
/* ── Page header ──────────────────────────────────── */
|
border-radius: 10rpx;
|
||||||
.page-header {
|
background: linear-gradient(90deg, #f0eae5 25%, #faf7f3 50%, #f0eae5 75%);
|
||||||
flex-shrink: 0;
|
background-size: 400% 100%; animation: shimmer 1.4s infinite;
|
||||||
height: 88rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: #fcfaf8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 34rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #3a2e2a;
|
|
||||||
letter-spacing: 1rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Filter header ────────────────────────────────── */
|
|
||||||
.filter-header {
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: rgba(252, 250, 248, 0.96);
|
|
||||||
box-shadow: 0 12rpx 30rpx rgba(120, 91, 79, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Scroll container ──────────────────────────────── */
|
|
||||||
.slot-scroll {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Slot list ─────────────────────────────────────── */
|
|
||||||
.slot-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 28rpx 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Date summary ──────────────────────────────────── */
|
|
||||||
.date-summary {
|
|
||||||
padding: 0 24rpx 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-summary-text {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #9d8b83;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Loading skeleton ──────────────────────────────── */
|
|
||||||
.loading-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24rpx;
|
|
||||||
padding: 28rpx 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-card {
|
|
||||||
height: 220rpx;
|
|
||||||
border-radius: 26rpx;
|
|
||||||
background: rgba(255, 255, 255, 0.88);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
padding: 28rpx 48rpx;
|
|
||||||
gap: 20rpx;
|
|
||||||
margin: 0 24rpx;
|
|
||||||
box-shadow: 0 16rpx 36rpx rgba(120, 91, 79, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-time {
|
|
||||||
width: 90rpx;
|
|
||||||
height: 80rpx;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
background: linear-gradient(90deg, rgba(236, 225, 217, 0.9) 25%, rgba(248, 240, 233, 0.98) 50%, rgba(236, 225, 217, 0.9) 75%);
|
|
||||||
background-size: 400% 100%;
|
|
||||||
animation: shimmer 1.4s infinite;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-body {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-title {
|
|
||||||
width: 60%;
|
|
||||||
height: 28rpx;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
background: linear-gradient(90deg, rgba(236, 225, 217, 0.9) 25%, rgba(248, 240, 233, 0.98) 50%, rgba(236, 225, 217, 0.9) 75%);
|
|
||||||
background-size: 400% 100%;
|
|
||||||
animation: shimmer 1.4s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-sub {
|
|
||||||
width: 40%;
|
|
||||||
height: 20rpx;
|
|
||||||
border-radius: 6rpx;
|
|
||||||
background: linear-gradient(90deg, rgba(236, 225, 217, 0.9) 25%, rgba(248, 240, 233, 0.98) 50%, rgba(236, 225, 217, 0.9) 75%);
|
|
||||||
background-size: 400% 100%;
|
|
||||||
animation: shimmer 1.4s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-btn {
|
|
||||||
width: 100rpx;
|
|
||||||
height: 60rpx;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
background: linear-gradient(90deg, rgba(236, 225, 217, 0.9) 25%, rgba(248, 240, 233, 0.98) 50%, rgba(236, 225, 217, 0.9) 75%);
|
|
||||||
background-size: 400% 100%;
|
|
||||||
animation: shimmer 1.4s infinite;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Empty state ───────────────────────────────────── */
|
|
||||||
.empty-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 120rpx 40rpx 80rpx;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Zen-inspired geometric illustration */
|
|
||||||
.empty-illustration {
|
|
||||||
position: relative;
|
|
||||||
width: 200rpx;
|
|
||||||
height: 200rpx;
|
|
||||||
margin-bottom: 56rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-circle {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
|
|
||||||
&.outer {
|
|
||||||
width: 180rpx;
|
|
||||||
height: 180rpx;
|
|
||||||
border: 2rpx solid rgba(192, 154, 137, 0.18);
|
|
||||||
animation: breathe 3s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.inner {
|
|
||||||
width: 120rpx;
|
|
||||||
height: 120rpx;
|
|
||||||
background: linear-gradient(135deg, #f6e9e1 0%, #ddc1b4 50%, #b98f7d 100%);
|
|
||||||
opacity: 0.6;
|
|
||||||
animation: breathe 3s ease-in-out infinite 0.5s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-dot {
|
|
||||||
position: absolute;
|
|
||||||
width: 16rpx;
|
|
||||||
height: 16rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #a87d6c;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
animation: pulse 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-text {
|
|
||||||
font-size: 32rpx;
|
|
||||||
color: #6f605b;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 2rpx;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-sub {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #a18a82;
|
|
||||||
letter-spacing: 1rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes breathe {
|
|
||||||
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.6; }
|
|
||||||
50% { transform: translate(-50%, -50%) scale(1.05); opacity: 0.4; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
|
||||||
50% { opacity: 0.5; transform: translate(-50%, -50%) scale(0.8); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Bottom spacer ─────────────────────────────────── */
|
|
||||||
.scroll-bottom-spacer {
|
|
||||||
height: 48rpx;
|
|
||||||
}
|
}
|
||||||
|
.skeleton-time { width: 90rpx; height: 70rpx; flex-shrink: 0; }
|
||||||
|
.skeleton-body { flex: 1; display: flex; flex-direction: column; gap: 12rpx; }
|
||||||
|
.skeleton-title { width: 80%; height: 28rpx; }
|
||||||
|
.skeleton-sub { width: 60%; height: 20rpx; }
|
||||||
|
.skeleton-btn { width: 110rpx; height: 60rpx; border-radius: 999rpx; flex-shrink: 0; }
|
||||||
|
.empty-wrap { margin: 32rpx; padding: 64rpx 28rpx; border-radius: 28rpx; background: #f3efea; display: flex; flex-direction: column; align-items: center; gap: 14rpx; }
|
||||||
|
.empty-text { font-size: 28rpx; color: #6f655e; font-weight: 400; }
|
||||||
|
.empty-sub { font-size: 23rpx; color: #8b817b; text-align: center; line-height: 1.6; }
|
||||||
|
.scroll-bottom-spacer { height: 28rpx; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,52 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="home-page">
|
<view class="home-page" :style="{ height: pageHeight }">
|
||||||
<!-- ① Brand Banner — fixed background layer -->
|
|
||||||
<view class="banner-fixed">
|
<view class="banner-fixed">
|
||||||
<BrandBanner :studio-info="studioStore.studioInfo" />
|
<BrandBanner :studio-info="studioStore.studioInfo" />
|
||||||
</view>
|
</view>
|
||||||
|
<scroll-view class="page-scroll" scroll-y :scroll-into-view="scrollTarget"
|
||||||
<!-- Pull-to-refresh wrapper — scrollable foreground -->
|
scroll-with-animation refresher-enabled :refresher-triggered="refreshing"
|
||||||
<scroll-view
|
@refresherrefresh="handleRefresh" @refresherrestore="refreshing = false">
|
||||||
class="page-scroll"
|
|
||||||
scroll-y
|
|
||||||
:scroll-top="scrollTop"
|
|
||||||
:scroll-with-animation="true"
|
|
||||||
:refresher-enabled="true"
|
|
||||||
:refresher-triggered="refreshing"
|
|
||||||
@refresherrefresh="handleRefresh"
|
|
||||||
@refresherrestore="refreshing = false"
|
|
||||||
>
|
|
||||||
<!-- Transparent spacer to reveal Banner behind -->
|
|
||||||
<view class="banner-spacer" />
|
<view class="banner-spacer" />
|
||||||
|
|
||||||
<!-- Floating card with rounded top corners -->
|
|
||||||
<view class="floating-card">
|
<view class="floating-card">
|
||||||
<!-- Drag indicator -->
|
<view class="card-handle"><view class="card-handle-bar" /></view>
|
||||||
<view class="card-handle">
|
|
||||||
<view class="card-handle-bar" />
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- ② Studio Info (photo strip + address/phone) -->
|
|
||||||
<StudioInfo :studio-info="studioStore.studioInfo" />
|
|
||||||
|
|
||||||
<!-- ③ Quick Entry (login / trial / book / renew) -->
|
|
||||||
<QuickEntry @scroll-to-card-shop="scrollToCardShop" />
|
<QuickEntry @scroll-to-card-shop="scrollToCardShop" />
|
||||||
|
|
||||||
<!-- ④ Upcoming Bookings -->
|
|
||||||
<UpcomingBooking />
|
<UpcomingBooking />
|
||||||
|
<StudioInfo :studio-info="studioStore.studioInfo" />
|
||||||
<!-- ④.5 Flash Sale Section -->
|
|
||||||
<FlashSaleSection ref="flashSaleRef" />
|
<FlashSaleSection ref="flashSaleRef" />
|
||||||
|
|
||||||
<!-- ⑤ Card Shop (vertical list) -->
|
|
||||||
<view :id="cardShopAnchorId">
|
<view :id="cardShopAnchorId">
|
||||||
<CardShop ref="cardShopRef" />
|
<CardShop ref="cardShopRef" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- ⑥ About (teacher + studio gallery) -->
|
|
||||||
<AboutSection />
|
<AboutSection />
|
||||||
|
|
||||||
<!-- Bottom padding for tab bar -->
|
|
||||||
<view class="bottom-padding" />
|
<view class="bottom-padding" />
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@@ -55,7 +25,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, nextTick, onUnmounted } from 'vue'
|
import { ref, nextTick, onUnmounted } from 'vue'
|
||||||
import { onShow, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
import { onShow, onResize, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
import BrandBanner from '../../components/BrandBanner.vue'
|
import BrandBanner from '../../components/BrandBanner.vue'
|
||||||
import StudioInfo from '../../components/StudioInfo.vue'
|
import StudioInfo from '../../components/StudioInfo.vue'
|
||||||
@@ -94,7 +64,9 @@ const refreshing = ref(false)
|
|||||||
const cardShopRef = ref<InstanceType<typeof CardShop> | null>(null)
|
const cardShopRef = ref<InstanceType<typeof CardShop> | null>(null)
|
||||||
const flashSaleRef = ref<InstanceType<typeof FlashSaleSection> | null>(null)
|
const flashSaleRef = ref<InstanceType<typeof FlashSaleSection> | null>(null)
|
||||||
const cardShopAnchorId = 'card-shop-anchor'
|
const cardShopAnchorId = 'card-shop-anchor'
|
||||||
const scrollTop = ref(0)
|
const scrollTarget = ref('')
|
||||||
|
const pageHeight = ref(`${uni.getWindowInfo().windowHeight}px`)
|
||||||
|
onResize(() => { pageHeight.value = `${uni.getWindowInfo().windowHeight}px` })
|
||||||
const pendingScrollToCardShop = ref(false)
|
const pendingScrollToCardShop = ref(false)
|
||||||
|
|
||||||
// Listen for cross-page scroll request (e.g. from booking page "去购买")
|
// Listen for cross-page scroll request (e.g. from booking page "去购买")
|
||||||
@@ -132,8 +104,10 @@ async function refreshData() {
|
|||||||
await Promise.allSettled(tasks)
|
await Promise.allSettled(tasks)
|
||||||
|
|
||||||
// Also refresh card shop and flash sales
|
// Also refresh card shop and flash sales
|
||||||
cardShopRef.value?.fetchCardTypes()
|
await Promise.allSettled([
|
||||||
flashSaleRef.value?.fetchFlashSales()
|
cardShopRef.value?.fetchCardTypes(),
|
||||||
|
flashSaleRef.value?.fetchFlashSales(),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRefresh() {
|
async function handleRefresh() {
|
||||||
@@ -145,78 +119,30 @@ async function handleRefresh() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToCardShop() {
|
async function scrollToCardShop() {
|
||||||
// Reset first so setting the same value still triggers scroll
|
scrollTarget.value = ''
|
||||||
scrollTop.value = 0
|
await nextTick()
|
||||||
nextTick(() => {
|
scrollTarget.value = cardShopAnchorId
|
||||||
uni.createSelectorQuery()
|
|
||||||
.select(`#${cardShopAnchorId}`)
|
|
||||||
.boundingClientRect()
|
|
||||||
.selectViewport()
|
|
||||||
.scrollOffset((res) => {
|
|
||||||
if (res) {
|
|
||||||
scrollTop.value = (res as UniApp.NodeInfo).scrollTop ?? 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.exec()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.home-page {
|
.home-page { position: relative; height: 100vh; background: #fbf9f6; }
|
||||||
position: relative;
|
.banner-fixed { position: fixed; top: 0; left: 0; width: 100%; z-index: 0; }
|
||||||
height: 100vh;
|
.page-scroll { position: relative; z-index: 1; height: 100%; }
|
||||||
background: #FAF8F5;
|
.banner-spacer { height: 420rpx; }
|
||||||
}
|
|
||||||
|
|
||||||
/* Banner fixed behind everything */
|
|
||||||
.banner-fixed {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scroll layer sits above banner */
|
|
||||||
.page-scroll {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
flex: 1;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Transparent spacer lets banner peek through */
|
|
||||||
.banner-spacer {
|
|
||||||
height: 420rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Floating card that overlaps the banner */
|
|
||||||
.floating-card {
|
.floating-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #FAF8F5;
|
|
||||||
border-radius: 32rpx 32rpx 0 0;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding-top: 12rpx;
|
padding-top: 12rpx;
|
||||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
border-radius: 32rpx 32rpx 0 0;
|
||||||
}
|
border-top: 1rpx solid rgba(255, 255, 255, 0.7);
|
||||||
|
background: linear-gradient(180deg, rgba(251, 249, 246, 0.86), #fbf9f6 260rpx);
|
||||||
/* Small drag indicator at top of card */
|
-webkit-backdrop-filter: blur(16px);
|
||||||
.card-handle {
|
backdrop-filter: blur(16px);
|
||||||
display: flex;
|
box-shadow: 0 -4rpx 20rpx rgba(66, 54, 45, 0.06);
|
||||||
justify-content: center;
|
|
||||||
padding: 16rpx 0 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-handle-bar {
|
|
||||||
width: 64rpx;
|
|
||||||
height: 8rpx;
|
|
||||||
border-radius: 4rpx;
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-padding {
|
|
||||||
height: 120rpx;
|
|
||||||
}
|
}
|
||||||
|
.card-handle { display: flex; justify-content: center; padding: 16rpx 0 8rpx; }
|
||||||
|
.card-handle-bar { width: 64rpx; height: 8rpx; border-radius: 4rpx; background: rgba(112, 94, 79, 0.18); }
|
||||||
|
.bottom-padding { height: 48rpx; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="profile-page">
|
<view class="profile-page" :style="{ paddingTop: navBarHeight + 'px' }">
|
||||||
<!-- Custom nav bar (transparent, blends with UserCard gradient) -->
|
<view class="profile-page__nav" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||||
<CustomNavBar title="我的" transparent />
|
<view class="profile-page__nav-title">我的</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- User card -->
|
<!-- User card -->
|
||||||
<UserCard :logged-in="loggedIn" :has-profile="hasProfile" :user="user" :stats="stats" :memberships="memberships"
|
<UserCard :logged-in="loggedIn" :has-profile="hasProfile" :user="user" :stats="stats" :memberships="memberships"
|
||||||
:loading="loginLoading" :nav-bar-height="navBarHeight" @login="handleLogin" />
|
:loading="loginLoading" @login="handleLogin" @edit="goToInfo" />
|
||||||
|
|
||||||
<PracticeActivityCard v-if="loggedIn" :key="userStore.token" :refresh-key="activityRefreshKey" />
|
|
||||||
|
|
||||||
<!-- Menu section: always visible -->
|
<!-- Menu section: always visible -->
|
||||||
<ProfileMenu
|
<ProfileMenu
|
||||||
@@ -18,7 +17,9 @@
|
|||||||
:invite-share-eligible="!!user?.inviteShareEligible"
|
:invite-share-eligible="!!user?.inviteShareEligible"
|
||||||
@clear-cache="handleClearCache"
|
@clear-cache="handleClearCache"
|
||||||
@require-login="handleLogin"
|
@require-login="handleLogin"
|
||||||
/>
|
>
|
||||||
|
<PracticeActivityCard v-if="loggedIn" :key="userStore.token" :refresh-key="activityRefreshKey" />
|
||||||
|
</ProfileMenu>
|
||||||
|
|
||||||
<!-- Logout button: only when logged in -->
|
<!-- Logout button: only when logged in -->
|
||||||
<view v-if="loggedIn" class="profile-page__logout-wrap">
|
<view v-if="loggedIn" class="profile-page__logout-wrap">
|
||||||
@@ -38,7 +39,6 @@ import { getErrorMessage } from '../../utils/auth'
|
|||||||
import PracticeActivityCard from '../../components/PracticeActivityCard.vue'
|
import PracticeActivityCard from '../../components/PracticeActivityCard.vue'
|
||||||
import UserCard from '../../components/UserCard.vue'
|
import UserCard from '../../components/UserCard.vue'
|
||||||
import ProfileMenu from '../../components/ProfileMenu.vue'
|
import ProfileMenu from '../../components/ProfileMenu.vue'
|
||||||
import CustomNavBar from '../../components/CustomNavBar.vue'
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const bookingStore = useBookingStore()
|
const bookingStore = useBookingStore()
|
||||||
@@ -47,7 +47,8 @@ const { upcomingBookings } = storeToRefs(bookingStore)
|
|||||||
|
|
||||||
const activityRefreshKey = ref(0)
|
const activityRefreshKey = ref(0)
|
||||||
const loginLoading = ref(false)
|
const loginLoading = ref(false)
|
||||||
const navBarHeight = ref(64)
|
const navBarHeight = ref(getSystemLayout().navBarHeight)
|
||||||
|
const statusBarHeight = getSystemLayout().statusBarHeight
|
||||||
|
|
||||||
const activeMembershipCount = computed(
|
const activeMembershipCount = computed(
|
||||||
() => user.value?.activeMembershipCount ?? userStore.activeMemberships.length,
|
() => user.value?.activeMembershipCount ?? userStore.activeMemberships.length,
|
||||||
@@ -107,12 +108,16 @@ async function handleLogin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goToInfo() {
|
||||||
|
uni.navigateTo({ url: '/pages/profile/info' })
|
||||||
|
}
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '退出登录',
|
title: '退出登录',
|
||||||
content: '确定要退出登录吗?',
|
content: '确定要退出登录吗?',
|
||||||
confirmText: '退出',
|
confirmText: '退出',
|
||||||
confirmColor: '#ff4d4f',
|
confirmColor: '#9c7a6e',
|
||||||
success(res) {
|
success(res) {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
userStore.logout()
|
userStore.logout()
|
||||||
@@ -138,27 +143,10 @@ function handleClearCache() {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.profile-page {
|
.profile-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh; box-sizing: border-box; background: #fbf9f6; padding-bottom: 36rpx;
|
||||||
|
&__nav { position: fixed; top: 0; left: 0; right: 0; z-index: 101; background: #fbf9f6; }
|
||||||
&__logout-wrap {
|
&__nav-title { height: 88rpx; display: flex; align-items: center; justify-content: center; font-size: 34rpx; font-weight: 500; color: #514943; }
|
||||||
margin: $spacing-xl $spacing-lg $spacing-xl;
|
&__logout-wrap { margin: 28rpx 32rpx 0; }
|
||||||
}
|
&__logout-btn { width: 100%; height: 80rpx; line-height: 80rpx; padding: 0; margin: 0; border: none; border-radius: 24rpx; background: #f2ede7; color: #91766a; font-size: 25rpx; font-weight: 400; &::after { border: none; } }
|
||||||
|
|
||||||
&__logout-btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 88rpx;
|
|
||||||
line-height: 88rpx;
|
|
||||||
background: $bg-card;
|
|
||||||
color: $error-color;
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 500;
|
|
||||||
border: none;
|
|
||||||
border-radius: $radius-lg;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user