feat: UI 页面优化

This commit is contained in:
richarjiang
2026-04-14 10:07:31 +08:00
parent 52cc3a2985
commit 7ce7cef77c
8 changed files with 958 additions and 448 deletions

View File

@@ -1,12 +1,15 @@
<template>
<view class="date-selector">
<view class="date-selector" :class="`date-selector--${variant}`">
<scroll-view class="scroll" scroll-x enhanced :show-scrollbar="false">
<view class="track">
<view
v-for="item in dateRange"
:key="item.date"
class="date-item"
:class="{ active: item.date === modelValue, today: item.isToday }"
:class="[
`date-item--${variant}`,
{ active: item.date === modelValue, today: item.isToday },
]"
@tap="handleSelect(item.date)"
>
<text class="weekday">{{ item.isToday ? '今天' : item.weekday }}</text>
@@ -25,6 +28,7 @@ import { getDateRange } from '../utils/format'
interface Props {
modelValue: string
variant?: 'default' | 'booking'
}
const props = defineProps<Props>()
@@ -47,6 +51,8 @@ function handleSelect(date: string) {
emit('update:modelValue', date)
emit('select', date)
}
const variant = computed(() => props.variant ?? 'default')
</script>
<style lang="scss" scoped>
@@ -55,6 +61,11 @@ function handleSelect(date: string) {
padding: 16rpx 0 20rpx;
border-bottom: 1rpx solid $primary-border;
&.date-selector--booking {
background: rgba(252, 250, 248, 0.96);
border-bottom-color: rgba(192, 154, 137, 0.12);
}
.scroll {
width: 100%;
white-space: nowrap;
@@ -121,6 +132,40 @@ function handleSelect(date: string) {
font-weight: 600;
}
}
&.date-item--booking {
background: rgba(247, 242, 238, 0.88);
border: 1rpx solid rgba(192, 154, 137, 0.08);
.weekday {
color: #9d8b83;
}
.day {
color: #3a2e2a;
}
.month {
color: #b7a79f;
}
&.active {
background: linear-gradient(135deg, #d7beb1, #b98f7d);
box-shadow: 0 12rpx 28rpx rgba(143, 103, 89, 0.16);
.weekday,
.day,
.month {
color: #fffaf7;
}
}
&.today:not(.active) {
.weekday {
color: #8f6759;
}
}
}
}
}
</style>

View File

@@ -40,7 +40,7 @@ interface MenuItem {
path?: string
isAdmin?: boolean
badge?: string
action?: 'clear' | 'about'
action?: 'clear'
requireAuth?: boolean
}
@@ -53,7 +53,6 @@ const props = defineProps<{
const emit = defineEmits<{
(e: 'clear-cache'): void
(e: 'about'): void
(e: 'require-login'): void
}>()
@@ -99,12 +98,6 @@ const menuItems = computed<MenuItem[]>(() => {
title: '清除缓存',
action: 'clear',
},
{
key: 'about',
type: 'item',
title: '关于我们',
action: 'about',
},
]
if (props.isAdmin) {
@@ -129,8 +122,6 @@ function handleTap(item: MenuItem) {
}
if (item.action === 'clear') {
emit('clear-cache')
} else if (item.action === 'about') {
emit('about')
} else if (item.path) {
uni.navigateTo({ url: item.path })
}
@@ -290,31 +281,6 @@ function handleTap(item: MenuItem) {
}
}
// 关于我们 — 圆形中心一个点 + 竖线info 标记)
&--about {
background: rgba($text-hint, 0.08);
&::before {
content: '';
width: 22rpx;
height: 22rpx;
border: 2.5rpx solid $text-secondary;
border-radius: 50%;
box-sizing: border-box;
}
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2.5rpx;
height: 8rpx;
background: $text-secondary;
border-radius: 1rpx;
box-shadow: 0 -6rpx 0 0 $text-secondary;
}
}
// 管理中心 — 齿轮(圆 + 四个刻度)
&--admin {
background: rgba($accent-color, 0.12);
@@ -359,15 +325,15 @@ function handleTap(item: MenuItem) {
font-size: 22rpx;
line-height: 1;
font-weight: 600;
color: #496578;
background: linear-gradient(135deg, rgba(239, 247, 251, 0.98), rgba(218, 234, 243, 0.96));
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(123, 165, 190, 0.18);
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(123, 165, 190, 0.16);
0 6rpx 16rpx rgba(143, 103, 89, 0.12);
}
&__arrow {

View File

@@ -160,6 +160,7 @@ const isPast = computed(() => isSlotPast(props.timeSlot.date, props.timeSlot.sta
margin: 0 24rpx 20rpx;
min-height: 220rpx;
transition: all 0.2s ease;
filter: drop-shadow(0 16rpx 28rpx rgba(120, 91, 79, 0.08));
&:active {
transform: scale(0.98);
@@ -216,7 +217,7 @@ const isPast = computed(() => isSlotPast(props.timeSlot.date, props.timeSlot.sta
.time-main {
font-size: 40rpx;
font-weight: 800;
color: #1a1a2e;
color: #3a2e2a;
line-height: 1;
letter-spacing: 1rpx;
}
@@ -224,7 +225,7 @@ const isPast = computed(() => isSlotPast(props.timeSlot.date, props.timeSlot.sta
.time-label {
margin-top: 8rpx;
font-size: 22rpx;
color: #999;
color: #a18a82;
font-weight: 500;
}
@@ -248,7 +249,7 @@ const isPast = computed(() => isSlotPast(props.timeSlot.date, props.timeSlot.sta
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: #ccc;
background: #ccb7ae;
flex-shrink: 0;
}
@@ -257,8 +258,8 @@ const isPast = computed(() => isSlotPast(props.timeSlot.date, props.timeSlot.sta
height: 2rpx;
background: repeating-linear-gradient(
to right,
#d0d0d0 0,
#d0d0d0 8rpx,
#dccbc2 0,
#dccbc2 8rpx,
transparent 8rpx,
transparent 16rpx
);
@@ -269,7 +270,7 @@ const isPast = computed(() => isSlotPast(props.timeSlot.date, props.timeSlot.sta
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: rgba($primary-color, 0.1);
background: rgba(185, 143, 125, 0.12);
display: flex;
align-items: center;
justify-content: center;
@@ -283,7 +284,7 @@ const isPast = computed(() => isSlotPast(props.timeSlot.date, props.timeSlot.sta
.duration-text {
margin-top: 6rpx;
font-size: 22rpx;
color: #999;
color: #a18a82;
font-weight: 500;
}
@@ -295,34 +296,34 @@ const isPast = computed(() => isSlotPast(props.timeSlot.date, props.timeSlot.sta
font-size: 20rpx;
&.cap-open {
background: rgba(76, 175, 80, 0.08);
background: rgba(101, 163, 126, 0.12);
.capacity-text {
color: #4caf50;
color: #5d9472;
}
}
&.cap-almost {
background: rgba(245, 158, 11, 0.08);
background: rgba(214, 161, 92, 0.14);
.capacity-text {
color: #f59e0b;
color: #b98543;
}
}
&.cap-full {
background: rgba(239, 68, 68, 0.08);
background: rgba(216, 91, 87, 0.12);
.capacity-text {
color: #ef4444;
color: #c96763;
}
}
&.cap-closed {
background: rgba(0, 0, 0, 0.04);
background: rgba(111, 96, 91, 0.08);
.capacity-text {
color: #999;
color: #9d8f89;
}
}
}
@@ -360,7 +361,7 @@ const isPast = computed(() => isSlotPast(props.timeSlot.date, props.timeSlot.sta
.course-name {
font-size: 30rpx;
font-weight: 700;
color: #1a1a2e;
color: #3a2e2a;
letter-spacing: 1rpx;
}
@@ -385,9 +386,9 @@ const isPast = computed(() => isSlotPast(props.timeSlot.date, props.timeSlot.sta
}
.btn-book {
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
color: #fff;
box-shadow: 0 4rpx 16rpx rgba($primary-dark, 0.3);
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;
@@ -399,30 +400,30 @@ const isPast = computed(() => isSlotPast(props.timeSlot.date, props.timeSlot.sta
}
.badge-booked {
background: linear-gradient(135deg, $primary-selected-bg, $primary-border);
color: $primary-dark;
background: linear-gradient(135deg, rgba(247, 240, 235, 0.96), rgba(236, 225, 217, 0.98));
color: #8f6759;
}
.badge-expired {
background: #f0f0f0;
color: #999;
background: rgba(111, 96, 91, 0.08);
color: #9d8f89;
}
.badge-full {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
background: rgba(216, 91, 87, 0.12);
color: #c96763;
}
.badge-closed {
background: #f0f0f0;
color: #bbb;
background: rgba(111, 96, 91, 0.08);
color: #b5a8a1;
}
.cancel-link {
font-size: 22rpx;
color: #ef4444;
color: #c96763;
font-weight: 500;
text-decoration: underline;
text-decoration-color: rgba(239, 68, 68, 0.3);
text-decoration-color: rgba(201, 103, 99, 0.28);
}
</style>

View File

@@ -1,10 +1,10 @@
<template>
<view class="time-period-filter">
<view class="time-period-filter" :class="`time-period-filter--${variant}`">
<view
v-for="tab in tabs"
:key="tab.key ?? 'all'"
class="tab-item"
:class="{ active: modelValue === tab.key }"
:class="[`tab-item--${variant}`, { active: modelValue === tab.key }]"
@tap="handleChange(tab.key)"
>
<text class="tab-label">{{ tab.label }}</text>
@@ -25,14 +25,17 @@ interface Tab {
interface Props {
modelValue: PeriodKey
variant?: 'default' | 'booking'
}
defineProps<Props>()
const props = defineProps<Props>()
const emit = defineEmits<{
change: [period: PeriodKey]
'update:modelValue': [period: PeriodKey]
}>()
const variant = computed(() => props.variant ?? 'default')
const tabs = computed<Tab[]>(() => [
{ key: null, label: '全部' },
...Object.entries(TIME_PERIODS).map(([key, val]) => ({
@@ -55,6 +58,11 @@ function handleChange(key: PeriodKey) {
padding: 0 24rpx;
border-bottom: 1rpx solid $primary-border;
&.time-period-filter--booking {
background: rgba(252, 250, 248, 0.96);
border-bottom-color: rgba(192, 154, 137, 0.12);
}
.tab-item {
flex: 1;
display: flex;
@@ -87,6 +95,26 @@ function handleChange(key: PeriodKey) {
border-radius: 2rpx;
}
}
&.tab-item--booking {
.tab-label {
color: #9d8b83;
}
&.active {
.tab-label {
color: #8f6759;
}
&::after {
width: 48rpx;
height: 5rpx;
background: linear-gradient(90deg, #c8a899, #a87d6c);
border-radius: 999rpx;
box-shadow: 0 4rpx 10rpx rgba(168, 125, 108, 0.18);
}
}
}
}
}
</style>

View File

@@ -248,11 +248,11 @@ function handleLogin() {
gap: 10rpx;
padding: 6rpx 14rpx 6rpx 8rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, rgba(244, 250, 253, 0.98), rgba(219, 235, 243, 0.94));
border: 1rpx solid rgba(123, 165, 190, 0.22);
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(79, 123, 148, 0.16);
0 8rpx 20rpx rgba(143, 103, 89, 0.14);
}
&__member-icon {
@@ -263,17 +263,17 @@ function handleLogin() {
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle at 30% 30%, #ffffff 0%, #dfeef6 38%, #8fb6cb 100%);
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(77, 117, 140, 0.18);
0 3rpx 8rpx rgba(143, 103, 89, 0.16);
&::after {
content: '';
position: absolute;
inset: 3rpx;
border-radius: 50%;
border: 1.5rpx solid rgba(92, 132, 156, 0.35);
border: 1.5rpx solid rgba(143, 103, 89, 0.28);
}
}
@@ -283,7 +283,7 @@ function handleLogin() {
font-size: 20rpx;
line-height: 1;
font-weight: 800;
color: #4f6f82;
color: #8f6759;
letter-spacing: 1rpx;
}
@@ -291,7 +291,7 @@ function handleLogin() {
font-size: 20rpx;
line-height: 1;
font-weight: 700;
color: #537488;
color: #8f6759;
letter-spacing: 2rpx;
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,8 +10,8 @@
<!-- Date & period filters -->
<view class="filter-header">
<DateSelector v-model="selectedDate" @select="onDateSelect" />
<TimePeriodFilter v-model="selectedPeriod" @change="onPeriodChange" />
<DateSelector v-model="selectedDate" variant="booking" @select="onDateSelect" />
<TimePeriodFilter v-model="selectedPeriod" variant="booking" @change="onPeriodChange" />
</view>
<!-- Slot list -->
@@ -312,7 +312,9 @@ onMounted(async () => {
<style lang="scss" scoped>
.booking-page {
height: 100vh;
background: $primary-bg;
background:
radial-gradient(circle at top, rgba(255, 232, 218, 0.36), transparent 34%),
linear-gradient(180deg, #fbf7f3 0%, #f6efea 100%);
display: flex;
flex-direction: column;
overflow: hidden;
@@ -321,7 +323,7 @@ onMounted(async () => {
/* ── Status bar ───────────────────────────────────── */
.status-bar {
flex-shrink: 0;
background: #fff;
background: #fcfaf8;
}
/* ── Page header ──────────────────────────────────── */
@@ -331,20 +333,21 @@ onMounted(async () => {
display: flex;
align-items: center;
justify-content: center;
background: #fff;
background: #fcfaf8;
}
.page-title {
font-size: 34rpx;
font-weight: 600;
color: #1a1a2e;
font-weight: 700;
color: #3a2e2a;
letter-spacing: 1rpx;
}
/* ── Filter header ────────────────────────────────── */
.filter-header {
flex-shrink: 0;
background: #fff;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
background: rgba(252, 250, 248, 0.96);
box-shadow: 0 12rpx 30rpx rgba(120, 91, 79, 0.06);
}
/* ── Scroll container ──────────────────────────────── */
@@ -358,7 +361,7 @@ onMounted(async () => {
.slot-list {
display: flex;
flex-direction: column;
padding: 24rpx 0 0;
padding: 28rpx 0 0;
}
/* ── Date summary ──────────────────────────────────── */
@@ -368,8 +371,8 @@ onMounted(async () => {
.date-summary-text {
font-size: 24rpx;
color: #999;
font-weight: 400;
color: #9d8b83;
font-weight: 500;
}
/* ── Loading skeleton ──────────────────────────────── */
@@ -382,22 +385,22 @@ onMounted(async () => {
.skeleton-card {
height: 220rpx;
border-radius: 20rpx;
background: #fff;
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 2rpx 12rpx rgba(0, 0, 0, 0.03);
box-shadow: 0 16rpx 36rpx rgba(120, 91, 79, 0.08);
}
.skeleton-time {
width: 90rpx;
height: 80rpx;
border-radius: 12rpx;
background: linear-gradient(90deg, $primary-border 25%, $primary-light 50%, $primary-border 75%);
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;
@@ -414,7 +417,7 @@ onMounted(async () => {
width: 60%;
height: 28rpx;
border-radius: 8rpx;
background: linear-gradient(90deg, $primary-border 25%, $primary-light 50%, $primary-border 75%);
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;
}
@@ -423,7 +426,7 @@ onMounted(async () => {
width: 40%;
height: 20rpx;
border-radius: 6rpx;
background: linear-gradient(90deg, $primary-border 25%, $primary-light 50%, $primary-border 75%);
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;
}
@@ -432,7 +435,7 @@ onMounted(async () => {
width: 100rpx;
height: 60rpx;
border-radius: 20rpx;
background: linear-gradient(90deg, $primary-border 25%, $primary-light 50%, $primary-border 75%);
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;
@@ -466,14 +469,14 @@ onMounted(async () => {
&.outer {
width: 180rpx;
height: 180rpx;
border: 2rpx solid $primary-border;
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, $primary-light 0%, $primary-color 50%, $primary-dark 100%);
background: linear-gradient(135deg, #f6e9e1 0%, #ddc1b4 50%, #b98f7d 100%);
opacity: 0.6;
animation: breathe 3s ease-in-out infinite 0.5s;
}
@@ -484,7 +487,7 @@ onMounted(async () => {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: $primary-dark;
background: #a87d6c;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
@@ -493,7 +496,7 @@ onMounted(async () => {
.empty-text {
font-size: 32rpx;
color: $primary-dark;
color: #6f605b;
font-weight: 600;
letter-spacing: 2rpx;
margin-bottom: 16rpx;
@@ -501,7 +504,7 @@ onMounted(async () => {
.empty-sub {
font-size: 26rpx;
color: $primary-color;
color: #a18a82;
letter-spacing: 1rpx;
}

View File

@@ -14,7 +14,6 @@
:active-membership-count="activeMembershipCount"
:upcoming-booking-count="upcomingBookingCount"
@clear-cache="handleClearCache"
@about="handleAbout"
@require-login="handleLogin"
/>
@@ -129,13 +128,6 @@ function handleClearCache() {
})
}
function handleAbout() {
uni.showModal({
title: '关于我们',
content: 'Focus Core 普拉提工作室\n版本 1.0.0\n\n专注核心遇见更好的自己',
showCancel: false,
})
}
</script>
<style lang="scss" scoped>