增加品牌方案管理
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="detail-root">
|
||||
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||||
<view class="header-wrap" :style="headerStyle">
|
||||
<view class="nav-bar" :style="navStyle">
|
||||
<text class="back-btn" @click="goBack">‹</text>
|
||||
<text class="nav-title">课程详情</text>
|
||||
</view>
|
||||
@@ -10,7 +10,7 @@
|
||||
<view class="cover-area">
|
||||
<image v-if="course.coverImage" class="cover-img" :src="course.coverImage" mode="aspectFill"
|
||||
@error="course.coverError = true" />
|
||||
<view v-if="!course.coverImage || course.coverError" class="cover-bg">
|
||||
<view v-if="!course.coverImage || course.coverError" class="cover-bg" :style="{ background: coverGradient }">
|
||||
<text class="cover-text">{{ course.typeName ? course.typeName.slice(0, 2) : '课程' }}</text>
|
||||
</view>
|
||||
<view class="status-tag" :class="course.statusClass" v-if="course.statusText">{{ course.statusText }}</view>
|
||||
@@ -117,7 +117,7 @@
|
||||
<text class="bottom-desc">/ 次</text>
|
||||
</view>
|
||||
<button class="booking-btn" :class="{ disabled: !course.canBook }" :disabled="!course.canBook"
|
||||
@click="handleBooking">
|
||||
@click="handleBooking" :style="course.canBook ? bookingBtnStyle : ''">
|
||||
{{ course.btnText }}
|
||||
</button>
|
||||
</view>
|
||||
@@ -128,6 +128,7 @@
|
||||
const courseApi = require('../../api/course')
|
||||
const bookingApi = require('../../api/booking')
|
||||
const store = require('../../store/index')
|
||||
const brandStore = require('../../store/brand')
|
||||
const { resolveCoverUrl } = require('../../utils/request')
|
||||
|
||||
export default {
|
||||
@@ -166,7 +167,8 @@
|
||||
coverImage: '',
|
||||
coverError: false
|
||||
},
|
||||
realMemberCount: 0
|
||||
realMemberCount: 0,
|
||||
brandConfig: brandStore.config
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
@@ -183,6 +185,24 @@
|
||||
this.navBarHeight = navBarHeight
|
||||
this.totalHeaderHeight = statusBarHeight + navBarHeight
|
||||
if (options.id) this.loadCourseDetail(options.id)
|
||||
uni.$on('brand:updated', (config) => { this.brandConfig = config })
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('brand:updated')
|
||||
},
|
||||
computed: {
|
||||
coverGradient() {
|
||||
return 'linear-gradient(135deg, ' + this.brandConfig.primaryColor + ', ' + this.brandConfig.secondaryColor + ')'
|
||||
},
|
||||
headerStyle() {
|
||||
return 'padding-top:' + this.statusBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor
|
||||
},
|
||||
navStyle() {
|
||||
return 'height:' + this.navBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor
|
||||
},
|
||||
bookingBtnStyle() {
|
||||
return 'background:' + this.brandConfig.primaryColor + ';color:' + this.brandConfig.secondaryColor
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadCourseDetail(id) {
|
||||
|
||||
@@ -1,32 +1,41 @@
|
||||
<template>
|
||||
<view class="home-page">
|
||||
<!-- 状态栏 + 胶囊按钮占位 -->
|
||||
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||||
<view class="header-wrap" :style="headerStyle">
|
||||
<view class="nav-bar" :style="navStyle">
|
||||
<text class="nav-title">✯ 今日训练</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="content" scroll-y :style="{ height: 'calc(100vh - ' + totalHeaderHeight + 'px)' }">
|
||||
<view class="content-inner">
|
||||
<!-- 品牌展示 -->
|
||||
<view class="brand-section">
|
||||
<image class="brand-logo" :src="brandConfig.logoUrl || '/static/logo.png'" mode="aspectFit" />
|
||||
<view class="brand-text">
|
||||
<text class="brand-name">{{ brandConfig.brandName }}</text>
|
||||
<text class="brand-slogan">{{ brandConfig.slogan }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人信息卡片(深色)+ 轮播图 -->
|
||||
<view class="greeting-card">
|
||||
<view class="greeting-card" :style="greetingCardStyle">
|
||||
<view class="greeting-header" @click="goToProfile">
|
||||
<view class="member-info">
|
||||
<text class="member-name">{{ memberInfo.nickname ? '你好,' + memberInfo.nickname : '欢迎您!点击前往更新您的信息。' }}</text>
|
||||
</view>
|
||||
<view class="checkin-badge" v-if="memberInfo.checkedInToday">
|
||||
<view class="checkin-badge" v-if="memberInfo.checkedInToday" :style="checkinBadgeStyle">
|
||||
<text class="checkin-icon">✓</text>
|
||||
<text>今日已签到</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stats">
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{ memberInfo.totalSignDays }}</text>
|
||||
<text class="stat-number" :style="{ color: brandConfig.primaryColor }">{{ memberInfo.totalSignDays }}</text>
|
||||
<text class="stat-label">累计签到(天)</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{ memberInfo.pendingBookings }}</text>
|
||||
<text class="stat-number" :style="{ color: brandConfig.primaryColor }">{{ memberInfo.pendingBookings }}</text>
|
||||
<text class="stat-label">待上团课</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -46,7 +55,7 @@
|
||||
<view class="recommend-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">✯ 今日推荐</text>
|
||||
<text class="section-more" @click="goToSearch">查看全部 ›</text>
|
||||
<text class="section-more" :style="{ color: brandConfig.primaryColor }" @click="goToSearch">查看全部 ›</text>
|
||||
</view>
|
||||
<view class="recommend-list">
|
||||
<view v-for="(course, idx) in recommendCourses" :key="idx" class="recommend-card"
|
||||
@@ -56,7 +65,7 @@
|
||||
<text class="card-name">{{ course.courseName }}</text>
|
||||
<text class="card-reason">{{ course.recommendReason }}</text>
|
||||
<view class="card-bottom">
|
||||
<text class="card-price">{{ course.priceLabel }}</text>
|
||||
<text class="card-price" :style="{ color: brandConfig.primaryColor }">{{ course.priceLabel }}</text>
|
||||
<text class="card-count">{{ course.bookedCount }}人已预约</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -75,6 +84,7 @@
|
||||
const memberApi = require('../../api/member')
|
||||
const courseApi = require('../../api/course')
|
||||
const bannerApi = require('../../api/banner')
|
||||
const brandStore = require('../../store/brand')
|
||||
const { resolveCoverUrl } = require('../../utils/request')
|
||||
|
||||
export default {
|
||||
@@ -91,7 +101,8 @@
|
||||
pendingBookings: 0
|
||||
},
|
||||
banners: [],
|
||||
recommendCourses: []
|
||||
recommendCourses: [],
|
||||
brandConfig: brandStore.config
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
@@ -107,6 +118,29 @@
|
||||
this.statusBarHeight = statusBarHeight
|
||||
this.navBarHeight = navBarHeight
|
||||
this.totalHeaderHeight = statusBarHeight + navBarHeight
|
||||
uni.$on('brand:updated', (config) => { this.brandConfig = config })
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('brand:updated')
|
||||
},
|
||||
computed: {
|
||||
headerStyle() {
|
||||
return 'padding-top:' + this.statusBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor
|
||||
},
|
||||
navStyle() {
|
||||
return 'height:' + this.navBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor
|
||||
},
|
||||
greetingCardStyle() {
|
||||
return {
|
||||
backgroundColor: this.brandConfig.secondaryColor,
|
||||
backgroundImage: this.brandConfig.backgroundImageUrl ? 'url(' + this.brandConfig.backgroundImageUrl + ')' : 'none',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
}
|
||||
},
|
||||
checkinBadgeStyle() {
|
||||
return { backgroundColor: 'rgba(' + this.brandConfig.primaryColorRgb + ',0.2)', color: this.brandConfig.primaryColor }
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
if (!store.isLoggedIn) {
|
||||
@@ -114,9 +148,11 @@
|
||||
return
|
||||
}
|
||||
this.loadData()
|
||||
brandStore.fetchConfig()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.loadData().finally(() => { uni.stopPullDownRefresh() })
|
||||
Promise.all([this.loadData(), brandStore.fetchConfig()])
|
||||
.finally(() => { uni.stopPullDownRefresh() })
|
||||
},
|
||||
methods: {
|
||||
goToCourseDetail(id) { uni.navigateTo({ url: '/pages/course-detail/course-detail?id=' + id }) },
|
||||
@@ -193,6 +229,13 @@
|
||||
.nav-title { font-size: 18px; font-weight: 700; color: #FFFFFF; }
|
||||
.content-inner { padding: 16px; }
|
||||
|
||||
/* 品牌展示 */
|
||||
.brand-section { display: flex; align-items: center; background: #FFFFFF; border-radius: 16px; padding: 16px 18px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
|
||||
.brand-logo { width: 48px; height: 48px; border-radius: 10px; margin-right: 14px; flex-shrink: 0; }
|
||||
.brand-text { display: flex; flex-direction: column; }
|
||||
.brand-name { font-size: 18px; font-weight: 700; color: #1E1E1E; }
|
||||
.brand-slogan { font-size: 12px; color: #9E9E9E; margin-top: 4px; }
|
||||
|
||||
/* 个人信息卡片 */
|
||||
.greeting-card { background: #1A1A1A; border-radius: 20px; padding: 18px 20px 0; color: #FFFFFF; margin-bottom: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); overflow: hidden; }
|
||||
.greeting-header { display: flex; justify-content: space-between; align-items: flex-start; }
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
<template>
|
||||
<view class="login-page" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="login-page" :style="{ paddingTop: statusBarHeight + 'px', backgroundColor: brandConfig.secondaryColor }">
|
||||
<view class="brand-area">
|
||||
<view class="logo-icon">
|
||||
<text class="logo-symbol">✦</text>
|
||||
<view class="logo-wrap" :style="{ backgroundColor: brandConfig.logoUrl ? 'transparent' : 'rgba(' + brandConfig.primaryColorRgb + ',0.15)' }">
|
||||
<image v-if="brandConfig.logoUrl" class="logo-img" :src="brandConfig.logoUrl" mode="aspectFit" />
|
||||
<text v-else class="logo-symbol" :style="{ color: brandConfig.primaryColor }">✦</text>
|
||||
</view>
|
||||
<text class="app-name">Novalon 健身房</text>
|
||||
<text class="app-slogan">专业预约 · 科学训练</text>
|
||||
<text class="app-name">{{ brandConfig.brandName }}</text>
|
||||
<text class="app-slogan" v-if="brandConfig.slogan">{{ brandConfig.slogan }}</text>
|
||||
</view>
|
||||
|
||||
<view class="login-btn-area">
|
||||
<button class="wechat-login-btn" @click="handleLogin">
|
||||
<text class="wechat-symbol">微</text>
|
||||
<text class="btn-text">微信一键登录</text>
|
||||
<button class="wechat-login-btn" @click="handleLogin"
|
||||
:style="{ background: brandConfig.primaryColor }">
|
||||
<text class="wechat-symbol" :style="{ color: brandConfig.secondaryColor }">微</text>
|
||||
<text class="btn-text" :style="{ color: brandConfig.secondaryColor }">微信一键登录</text>
|
||||
</button>
|
||||
|
||||
<view class="agreement-row">
|
||||
<view class="checkbox-wrapper" @click="toggleAgree">
|
||||
<view class="checkbox" :class="{ checked: agreed }">
|
||||
<text v-if="agreed" class="check-mark">✓</text>
|
||||
<view class="checkbox" :class="{ checked: agreed }" :style="agreed ? agreedStyle : ''">
|
||||
<text v-if="agreed" class="check-mark" :style="{ color: brandConfig.secondaryColor }">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="agreement-text">
|
||||
已阅读并同意
|
||||
<text class="link">《用户协议》</text>
|
||||
<text class="link" :style="{ color: brandConfig.primaryColor }">《用户协议》</text>
|
||||
和
|
||||
<text class="link">《隐私政策》</text>
|
||||
<text class="link" :style="{ color: brandConfig.primaryColor }">《隐私政策》</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -50,13 +52,15 @@
|
||||
<script>
|
||||
const authApi = require('../../api/auth')
|
||||
const store = require('../../store/index')
|
||||
const brandStore = require('../../store/brand')
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
statusBarHeight: 0,
|
||||
agreed: false,
|
||||
loading: false
|
||||
loading: false,
|
||||
brandConfig: brandStore.config
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
@@ -66,6 +70,15 @@
|
||||
if (store.isLoggedIn) {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
uni.$on('brand:updated', (config) => { this.brandConfig = config })
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('brand:updated')
|
||||
},
|
||||
computed: {
|
||||
agreedStyle() {
|
||||
return 'background:' + this.brandConfig.primaryColor + ';border-color:' + this.brandConfig.secondaryColor
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleAgree() { this.agreed = !this.agreed },
|
||||
@@ -119,32 +132,33 @@
|
||||
.login-page { min-height: 100vh; background: #F5F7FA; display: flex; flex-direction: column; align-items: center; padding: 0 32px; overflow-x: hidden; }
|
||||
|
||||
.brand-area { display: flex; flex-direction: column; align-items: center; margin-top: 60px; margin-bottom: 80px; }
|
||||
.logo-icon { width: 80px; height: 80px; background: rgba(0,230,118,0.15); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 20px; }
|
||||
.logo-symbol { font-size: 36px; color: #00C853; }
|
||||
.app-name { font-size: 28px; font-weight: 700; color: #1E1E1E; letter-spacing: 1px; }
|
||||
.app-slogan { font-size: 14px; color: #7A7E84; margin-top: 8px; letter-spacing: 2px; }
|
||||
.logo-wrap { width: 120px; height: 120px; background: rgba(0,230,118,0.15); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 24px; overflow: hidden; }
|
||||
.logo-img { width: 100%; height: 100%; }
|
||||
.logo-symbol { font-size: 52px; color: #00C853; }
|
||||
.app-name { font-size: 28px; font-weight: 700; color: #FFFFFF; letter-spacing: 1px; }
|
||||
.app-slogan { font-size: 14px; color: rgba(255,255,255,0.6); margin-top: 8px; letter-spacing: 2px; }
|
||||
|
||||
.login-btn-area { width: 100%; display: flex; flex-direction: column; align-items: center; }
|
||||
.wechat-login-btn { width: 100%; height: 52px; background: #00E676; border-radius: 40px; display: flex; align-items: center; justify-content: center; border: none; padding: 0; box-shadow: 0 4px 16px rgba(0,230,118,0.35); }
|
||||
.wechat-login-btn::after { border: none; }
|
||||
.wechat-symbol { font-size: 18px; font-weight: 700; color: #1A1A1A; margin-right: 8px; }
|
||||
.btn-text { font-size: 17px; font-weight: 700; color: #1A1A1A; }
|
||||
.login-btn-area { width: 100%; display: flex; flex-direction: column; align-items: center; }
|
||||
.wechat-login-btn { width: 100%; height: 52px; background: #00E676; border-radius: 40px; display: flex; align-items: center; justify-content: center; border: none; padding: 0; box-shadow: 0 4px 16px rgba(0,230,118,0.35); }
|
||||
.wechat-login-btn::after { border: none; }
|
||||
.wechat-symbol { font-size: 18px; font-weight: 700; color: #1A1A1A; margin-right: 8px; }
|
||||
.btn-text { font-size: 17px; font-weight: 700; color: #1A1A1A; }
|
||||
|
||||
.agreement-row { display: flex; align-items: center; margin-top: 16px; }
|
||||
.checkbox-wrapper { margin-right: 6px; }
|
||||
.checkbox { width: 18px; height: 18px; border-radius: 50%; border: 2px solid #BDBDBD; display: flex; align-items: center; justify-content: center; }
|
||||
.checkbox.checked { background: #00E676; border-color: #00E676; }
|
||||
.check-mark { font-size: 12px; color: #1A1A1A; font-weight: 700; }
|
||||
.agreement-text { font-size: 12px; color: #7A7E84; }
|
||||
.link { color: #00C853; }
|
||||
.agreement-row { display: flex; align-items: center; margin-top: 16px; }
|
||||
.checkbox-wrapper { margin-right: 6px; }
|
||||
.checkbox { width: 18px; height: 18px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.3); display: flex; align-items: center; justify-content: center; }
|
||||
.checkbox.checked { background: #00E676; border-color: #00E676; }
|
||||
.check-mark { font-size: 12px; color: #1A1A1A; font-weight: 700; }
|
||||
.agreement-text { font-size: 12px; color: rgba(255,255,255,0.5); }
|
||||
.link { color: #00E676; }
|
||||
|
||||
.other-login { margin-top: 60px; width: 100%; display: flex; flex-direction: column; align-items: center; }
|
||||
.divider-text { font-size: 13px; color: #BDBDBD; margin-bottom: 20px; }
|
||||
.other-icons { display: flex; }
|
||||
.other-item { display: flex; flex-direction: column; align-items: center; }
|
||||
.other-icon-circle { width: 48px; height: 48px; background: #FFFFFF; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 12px rgba(0,0,0,0.06); }
|
||||
.other-symbol { font-size: 22px; color: #7A7E84; }
|
||||
.other-label { font-size: 12px; color: #7A7E84; }
|
||||
.other-login { margin-top: 60px; width: 100%; display: flex; flex-direction: column; align-items: center; }
|
||||
.divider-text { font-size: 13px; color: rgba(255,255,255,0.3); margin-bottom: 20px; }
|
||||
.other-icons { display: flex; }
|
||||
.other-item { display: flex; flex-direction: column; align-items: center; }
|
||||
.other-icon-circle { width: 48px; height: 48px; background: rgba(255,255,255,0.1); border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 12px rgba(0,0,0,0.2); }
|
||||
.other-symbol { font-size: 22px; color: rgba(255,255,255,0.6); }
|
||||
.other-label { font-size: 12px; color: rgba(255,255,255,0.4); }
|
||||
|
||||
.footer-tip { position: absolute; bottom: 60px; font-size: 12px; color: #BDBDBD; }
|
||||
.footer-tip { position: absolute; bottom: 60px; font-size: 12px; color: rgba(255,255,255,0.25); }
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="my-courses-page">
|
||||
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||||
<view class="header-wrap" :style="headerStyle">
|
||||
<view class="nav-bar" :style="navStyle">
|
||||
<text class="nav-title">⌕ 我的课程</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -53,7 +53,7 @@
|
||||
<view v-if="!loading && bookings.length === 0" class="empty-state">
|
||||
<text class="empty-symbol">⊙</text>
|
||||
<text class="empty-text">暂无预约课程</text>
|
||||
<text class="empty-sub" @click="goToSearch">前往预约团课 ›</text>
|
||||
<text class="empty-sub" :style="{ color: brandConfig.primaryColor }" @click="goToSearch">前往预约团课 ›</text>
|
||||
</view>
|
||||
|
||||
<view class="bottom-safe"></view>
|
||||
@@ -65,6 +65,7 @@
|
||||
<script>
|
||||
const bookingApi = require('../../api/booking')
|
||||
const store = require('../../store/index')
|
||||
const brandStore = require('../../store/brand')
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -75,7 +76,8 @@
|
||||
loading: true,
|
||||
cancelling: null,
|
||||
signing: null,
|
||||
bookings: []
|
||||
bookings: [],
|
||||
brandConfig: brandStore.config
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
@@ -91,13 +93,26 @@
|
||||
this.statusBarHeight = statusBarHeight
|
||||
this.navBarHeight = navBarHeight
|
||||
this.totalHeaderHeight = statusBarHeight + navBarHeight
|
||||
uni.$on('brand:updated', (config) => { this.brandConfig = config })
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('brand:updated')
|
||||
},
|
||||
computed: {
|
||||
headerStyle() {
|
||||
return 'padding-top:' + this.statusBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor
|
||||
},
|
||||
navStyle() {
|
||||
return 'height:' + this.navBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
if (!store.isLoggedIn) return
|
||||
this.loadBookings()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.loadBookings().finally(() => { uni.stopPullDownRefresh() })
|
||||
Promise.all([this.loadBookings(), brandStore.fetchConfig()])
|
||||
.finally(() => { uni.stopPullDownRefresh() })
|
||||
},
|
||||
methods: {
|
||||
goToSearch() { uni.switchTab({ url: '/pages/search/search' }) },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="profile-page">
|
||||
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||||
<view class="header-wrap" :style="headerStyle">
|
||||
<view class="nav-bar" :style="navStyle">
|
||||
<text class="nav-title">● 个人中心</text>
|
||||
<text class="settings-symbol" @click="goToSettings">⚙</text>
|
||||
</view>
|
||||
@@ -14,7 +14,7 @@
|
||||
<image class="big-avatar-img" :src="avatarSrc" mode="aspectFill" @error="onAvatarError" />
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{ userInfo.nickname }}</text>
|
||||
<view class="user-tag">
|
||||
<view class="user-tag" :style="{ color: brandConfig.primaryColor }">
|
||||
<text>会员号:{{ userInfo.memberNo }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -40,14 +40,14 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-card">
|
||||
<view class="stats-card" :style="{ backgroundColor: brandConfig.secondaryColor }">
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{ userInfo.totalSignInDays }}</text>
|
||||
<text class="stat-number" :style="{ color: brandConfig.primaryColor }">{{ userInfo.totalSignInDays }}</text>
|
||||
<text class="stat-label">累计签到(天)</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-number">{{ userInfo.monthSignInCount }}</text>
|
||||
<text class="stat-number" :style="{ color: brandConfig.primaryColor }">{{ userInfo.monthSignInCount }}</text>
|
||||
<text class="stat-label">本月签到</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -55,10 +55,10 @@
|
||||
<view class="sign-section">
|
||||
<view class="sign-header">
|
||||
<view class="section-title-row">
|
||||
<text class="section-symbol">☰</text>
|
||||
<text class="section-symbol" :style="{ color: brandConfig.primaryColor }">☰</text>
|
||||
<text class="section-title">最近签到</text>
|
||||
</view>
|
||||
<text class="section-more" @click="viewAllSignIn">查看全部</text>
|
||||
<text class="section-more" @click="viewAllSignIn" :style="{ color: brandConfig.primaryColor }">查看全部</text>
|
||||
</view>
|
||||
<view v-for="(record, idx) in signInRecords" :key="idx" class="sign-item">
|
||||
<view class="sign-left">
|
||||
@@ -79,10 +79,10 @@
|
||||
<view class="sign-section">
|
||||
<view class="sign-header">
|
||||
<view class="section-title-row">
|
||||
<text class="section-symbol">✓</text>
|
||||
<text class="section-symbol" :style="{ color: brandConfig.primaryColor }">✓</text>
|
||||
<text class="section-title">团课签到记录</text>
|
||||
</view>
|
||||
<text class="section-more">最近{{ courseCheckInRecords.length }}条</text>
|
||||
<text class="section-more" :style="{ color: brandConfig.primaryColor }">最近{{ courseCheckInRecords.length }}条</text>
|
||||
</view>
|
||||
<view v-for="(record, idx) in courseCheckInRecords" :key="idx" class="sign-item">
|
||||
<view class="sign-left">
|
||||
@@ -92,7 +92,7 @@
|
||||
<view class="sign-right">
|
||||
<text class="sign-type">{{ record.courseName }}</text>
|
||||
<text class="sign-source">{{ record.location }}</text>
|
||||
<text class="sign-status success">已签到</text>
|
||||
<text class="sign-status success" :style="signStatusSuccessStyle">已签到</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="courseCheckInRecords.length === 0" class="empty-hint">
|
||||
@@ -131,7 +131,8 @@
|
||||
</scroll-view>
|
||||
<view class="edit-modal-footer">
|
||||
<button class="edit-btn cancel" @click="closeEdit">取消</button>
|
||||
<button class="edit-btn confirm" :disabled="saving" @click="saveProfile">
|
||||
<button class="edit-btn confirm" :disabled="saving" @click="saveProfile"
|
||||
:style="confirmBtnStyle">
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</button>
|
||||
</view>
|
||||
@@ -144,6 +145,7 @@
|
||||
const store = require('../../store/index')
|
||||
const memberApi = require('../../api/member')
|
||||
const bookingApi = require('../../api/booking')
|
||||
const brandStore = require('../../store/brand')
|
||||
const defaultAvatar = require('../../common/img/20200414210134_qbeyi.jpg')
|
||||
|
||||
export default {
|
||||
@@ -169,7 +171,8 @@
|
||||
{ label: '女', value: 'FEMALE' }
|
||||
],
|
||||
today: '',
|
||||
showDefaultAvatar: false
|
||||
showDefaultAvatar: false,
|
||||
brandConfig: brandStore.config
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
@@ -188,13 +191,53 @@
|
||||
// 设置今天日期,限制生日选择不晚于今天
|
||||
const d = new Date()
|
||||
this.today = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
|
||||
uni.$on('brand:updated', (config) => { this.brandConfig = config })
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('brand:updated')
|
||||
},
|
||||
computed: {
|
||||
headerStyle() {
|
||||
return 'padding-top:' + this.statusBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor
|
||||
},
|
||||
navStyle() {
|
||||
return 'height:' + this.navBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor
|
||||
},
|
||||
confirmBtnStyle() {
|
||||
return 'background:' + this.brandConfig.primaryColor + ';color:' + this.brandConfig.secondaryColor
|
||||
},
|
||||
avatarSrc() {
|
||||
if (this.showDefaultAvatar) return defaultAvatar
|
||||
return this.userInfo.avatar || defaultAvatar
|
||||
},
|
||||
genderLabel() {
|
||||
const g = this.userInfo.gender
|
||||
if (g === 1 || g === 'MALE' || g === '男') return '男'
|
||||
if (g === 2 || g === 'FEMALE' || g === '女') return '女'
|
||||
return '未设置'
|
||||
},
|
||||
genderIndex() {
|
||||
const genderMap = { 0: 'UNKNOWN', 1: 'MALE', 2: 'FEMALE' }
|
||||
const genderStr = typeof this.editForm.gender === 'string'
|
||||
? this.editForm.gender
|
||||
: genderMap[this.editForm.gender] || 'UNKNOWN'
|
||||
return Math.max(0, this.genderOptions.findIndex(o => o.value === genderStr))
|
||||
},
|
||||
signStatusSuccessStyle() {
|
||||
return {
|
||||
background: 'rgba(' + this.brandConfig.primaryColorRgb + ',0.15)',
|
||||
color: this.brandConfig.primaryColor
|
||||
}
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
if (!store.isLoggedIn) return
|
||||
this.loadData()
|
||||
brandStore.fetchConfig()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.loadData().finally(() => { uni.stopPullDownRefresh() })
|
||||
Promise.all([this.loadData(), brandStore.fetchConfig()])
|
||||
.finally(() => { uni.stopPullDownRefresh() })
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
@@ -331,25 +374,6 @@
|
||||
onAvatarError() { this.showDefaultAvatar = true },
|
||||
goToSettings() { uni.showToast({ title: '设置页面', icon: 'none' }) },
|
||||
viewAllSignIn() { uni.showToast({ title: '全部签到记录', icon: 'none' }) }
|
||||
},
|
||||
computed: {
|
||||
avatarSrc() {
|
||||
if (this.showDefaultAvatar) return defaultAvatar
|
||||
return this.userInfo.avatar || defaultAvatar
|
||||
},
|
||||
genderLabel() {
|
||||
const g = this.userInfo.gender
|
||||
if (g === 1 || g === 'MALE' || g === '男') return '男'
|
||||
if (g === 2 || g === 'FEMALE' || g === '女') return '女'
|
||||
return '未设置'
|
||||
},
|
||||
genderIndex() {
|
||||
const genderMap = { 0: 'UNKNOWN', 1: 'MALE', 2: 'FEMALE' }
|
||||
const genderStr = typeof this.editForm.gender === 'string'
|
||||
? this.editForm.gender
|
||||
: genderMap[this.editForm.gender] || 'UNKNOWN'
|
||||
return Math.max(0, this.genderOptions.findIndex(o => o.value === genderStr))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<view class="search-page">
|
||||
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar" :style="{ height: navBarHeight + 'px' }">
|
||||
<view class="header-wrap" :style="headerStyle">
|
||||
<view class="nav-bar" :style="navStyle">
|
||||
<text class="nav-title">⌕ 团课搜索</text>
|
||||
</view>
|
||||
<view class="search-area">
|
||||
<view class="search-area" :style="{ backgroundColor: brandConfig.secondaryColor }">
|
||||
<view class="search-box">
|
||||
<text class="search-symbol">⌕</text>
|
||||
<input class="search-input" v-model="searchKeyword" placeholder="搜索课程名称、教练、场地..."
|
||||
@@ -31,7 +31,8 @@
|
||||
|
||||
<view class="period-row">
|
||||
<view v-for="(period, idx) in periods" :key="idx" class="period-item"
|
||||
:class="{ active: currentPeriod === period.value }" @click="selectPeriod(period.value)">
|
||||
:class="{ active: currentPeriod === period.value }" @click="selectPeriod(period.value)"
|
||||
:style="currentPeriod === period.value ? activeBrandStyle : ''">
|
||||
<text>{{ period.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -40,18 +41,20 @@
|
||||
|
||||
<view class="type-section">
|
||||
<view class="type-all-row">
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'all' }" @click="switchFilter('all')">全部</view>
|
||||
<view class="filter-tab" :class="{ active: currentFilter === 'all' }" @click="switchFilter('all')"
|
||||
:style="currentFilter === 'all' ? activeBrandStyle : ''">全部</view>
|
||||
</view>
|
||||
<view class="type-grid" :class="{ expanded: typesExpanded }">
|
||||
<view v-for="(row, ri) in typeRows" :key="ri" class="type-row" :class="{ 'row-visible': isRowVisible(ri) }">
|
||||
<view v-for="(tab, ti) in row" :key="ti" class="filter-tab"
|
||||
:class="{ active: currentFilter === tab.value }" @click="selectType(tab)">
|
||||
:class="{ active: currentFilter === tab.value }" @click="selectType(tab)"
|
||||
:style="currentFilter === tab.value ? activeBrandStyle : ''">
|
||||
{{ tab.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="typeRows.length > 1" class="type-expand-row">
|
||||
<text class="type-expand-btn" @click="typesExpanded = !typesExpanded">
|
||||
<text class="type-expand-btn" @click="typesExpanded = !typesExpanded" :style="{ color: brandConfig.primaryColor }">
|
||||
{{ typesExpanded ? '收起 ▲' : '展开更多 ▾' }}
|
||||
</text>
|
||||
</view>
|
||||
@@ -61,7 +64,8 @@
|
||||
<text class="result-count">共 {{ filteredCourses.length }} 个课程</text>
|
||||
<view class="sort-options">
|
||||
<text v-for="(sort, idx) in sortOptions" :key="idx" class="sort-item"
|
||||
:class="{ active: currentSort === sort.value }" @click="switchSort(sort.value)">
|
||||
:class="{ active: currentSort === sort.value }" @click="switchSort(sort.value)"
|
||||
:style="currentSort === sort.value ? 'color:' + brandConfig.primaryColor : ''">
|
||||
{{ sort.label }}
|
||||
</text>
|
||||
</view>
|
||||
@@ -88,20 +92,20 @@
|
||||
<view class="course-body">
|
||||
<view class="course-top">
|
||||
<text class="course-name">{{ course.courseName }}</text>
|
||||
<text class="course-price" :class="{ free: course.storedValueAmount === 0 }">
|
||||
<text class="course-price" :class="{ free: course.storedValueAmount === 0 }" :style="{ color: brandConfig.primaryColor }">
|
||||
{{ course.storedValueAmount > 0 ? '¥' + course.storedValueAmount + '/次' : '免费' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="course-labels" v-if="course.labels && course.labels.length">
|
||||
<text v-for="(label, li) in course.labels" :key="li" class="course-label"
|
||||
:style="{ background: label.color + '1a', color: label.color }">{{ label.labelName }}</text>
|
||||
:style="label._style">{{ label.labelName }}</text>
|
||||
</view>
|
||||
|
||||
<view class="course-meta">
|
||||
<text class="meta-icon">🕓</text>
|
||||
<text class="course-date">{{ course.shortDate }}</text>
|
||||
<text class="course-time">{{ course.shortTime }} - {{ course.endShortTime }}</text>
|
||||
<text class="course-duration" v-if="course.duration">{{ course.duration }}</text>
|
||||
<text class="course-duration" v-if="course.duration" :style="{ color: brandConfig.primaryColor }">{{ course.duration }}</text>
|
||||
</view>
|
||||
<view class="course-meta">
|
||||
<text class="meta-icon">📍</text>
|
||||
@@ -110,7 +114,8 @@
|
||||
<view class="course-bottom">
|
||||
<text class="course-capacity">已预约 {{ course.currentMembers }}/{{ course.maxMembers }}人</text>
|
||||
<button class="btn-book" :class="{ disabled: !course.canBook }" :disabled="!course.canBook"
|
||||
@click.stop="bookCourse(course)">
|
||||
@click.stop="bookCourse(course)"
|
||||
:style="course.canBook ? activeBrandStyle : ''">
|
||||
{{ course.canBook ? '立即预约' : course.statusLabel }}
|
||||
</button>
|
||||
</view>
|
||||
@@ -133,6 +138,7 @@
|
||||
const courseApi = require('../../api/course')
|
||||
const bookingApi = require('../../api/booking')
|
||||
const store = require('../../store/index')
|
||||
const brandStore = require('../../store/brand')
|
||||
const { resolveCoverUrl } = require('../../utils/request')
|
||||
|
||||
export default {
|
||||
@@ -161,7 +167,8 @@
|
||||
{ label: '热度', value: 'popular' }
|
||||
],
|
||||
courses: [],
|
||||
activeBookedCourseIds: new Set() // 用户当前已预约(status=0)的课程ID集合
|
||||
activeBookedCourseIds: new Set(), // 用户当前已预约(status=0)的课程ID集合
|
||||
brandConfig: brandStore.config
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
@@ -179,15 +186,28 @@
|
||||
this.totalHeaderHeight = statusBarHeight + navBarHeight
|
||||
this.loadCourses()
|
||||
this.loadTypes()
|
||||
uni.$on('brand:updated', (config) => { this.brandConfig = config })
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('brand:updated')
|
||||
},
|
||||
onShow() {
|
||||
this.loadActiveBookings()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
Promise.all([this.loadCourses(), this.loadTypes(), this.loadActiveBookings()])
|
||||
Promise.all([this.loadCourses(), this.loadTypes(), this.loadActiveBookings(), brandStore.fetchConfig()])
|
||||
.finally(() => { uni.stopPullDownRefresh() })
|
||||
},
|
||||
computed: {
|
||||
headerStyle() {
|
||||
return 'padding-top:' + this.statusBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor
|
||||
},
|
||||
navStyle() {
|
||||
return 'height:' + this.navBarHeight + 'px;background-color:' + this.brandConfig.secondaryColor
|
||||
},
|
||||
activeBrandStyle() {
|
||||
return 'background:' + this.brandConfig.primaryColor + ';color:' + this.brandConfig.secondaryColor
|
||||
},
|
||||
// 将类型列表按每行 COLS_PER_ROW 个拆分为二维数组
|
||||
typeRows() {
|
||||
const tabs = this.courseTypes.map(t => ({
|
||||
@@ -266,7 +286,7 @@
|
||||
status: statusStr,
|
||||
statusLabel: statusLabel,
|
||||
storedValueAmount: c.storedValueAmount != null ? c.storedValueAmount : 0,
|
||||
labels: this.getTypeLabels(c.courseType),
|
||||
labels: this.getTypeLabels(c.courseType).map(l => ({ ...l, _style: { background: l.color + '1a', color: l.color } })),
|
||||
coverImage: resolveCoverUrl(c.coverImage),
|
||||
}
|
||||
})
|
||||
@@ -305,6 +325,9 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
labelStyle(label) {
|
||||
return { background: label.color + '1a', color: label.color }
|
||||
},
|
||||
// 从已加载的课程类型中匹配标签
|
||||
getTypeLabels(courseType) {
|
||||
const type = this.getTypeInfo(courseType)
|
||||
|
||||
Reference in New Issue
Block a user