Compare commits

..

14 Commits

Author SHA1 Message Date
gqt e78101f690 feat(orders): 前端支持部分退款状态展示(warning样式+订单tab) 2026-08-14 17:43:48 +08:00
gqt 5bd4d7dcea fix(private-detail): 恢复退款弹窗规则表格的说明列 2026-08-14 17:00:09 +08:00
gqt 0b694ff6db refactor(private-detail): 退款弹窗去掉规则描述文字,仅保留时间档位和比例 2026-08-14 16:58:30 +08:00
gqt e4a05379ce fix(private-detail): 退款档位标签按配置动态计算(修复6-12/0-6显示为0-12的问题) 2026-08-14 16:55:39 +08:00
gqt 7765d3dae1 feat(private-detail): 新增取消预订按钮+阶梯退款弹窗 2026-08-14 16:39:34 +08:00
gqt 3832a686d8 feat(booking): 新增包场退款报价+取消预订 API + can_cancel mapper 2026-08-14 16:38:37 +08:00
gqt 7dc0314565 feat(private): 订场页改为 订场/我的预定 Tab切换 + 历史列表只显示近10天+未来
- 顶部Tab从羽毛球改为 订场/我的预定
- 我的预定: 卡片列表展示已预约记录,点击跳转详情
- 我的预定只显示最近10天到未来的已预约记录
- 移除未使用的 openTime/closeTime/historyLoaded
- 修复 -radius-md 变量不存在的问题
2026-08-14 15:56:59 +08:00
gqt 30c2b5ffea feat(private): 包场预定天数改用后端动态配置(getPrivateBookingConfig API + index.vue动态读取) 2026-08-14 14:58:03 +08:00
gqt 2ca2ead6da feat: 冰场馆商城页/私教页改为展示后台配置的全屏图片 2026-07-30 12:41:39 +08:00
gqt 00f1bcd0fa feat: 订场页5秒自动刷新 + 支付页仅场地预约支持余额支付(修正business_type) + 清理冗余代码 2026-07-29 13:17:22 +08:00
gqt 1a3137bd12 feat: 商城tab改名(储值卡->会员卡/会员卡->课程套餐) + 我的会员卡->我的课程套餐 + 约课tab会员卡->课程 2026-07-29 12:56:25 +08:00
gqt 4872f180dc feat: 首页新增课程列表区块 + 全部课程列表页 + 课程详情页 2026-07-28 14:56:47 +08:00
gqt 605d463a00 feat: 恢复余额支付 + 门票暂停购买状态展示 + 静默处理门票产品404 2026-07-28 14:12:48 +08:00
gqt d88ef1756f feat: 订场页余额<=0可看3天 + 我的页下拉刷新/进入刷新 + 门票tab改名 + 商城页进入/切tab刷新 + 底部tab顺序调整 2026-07-28 14:03:07 +08:00
24 changed files with 923 additions and 128 deletions
@@ -62,6 +62,7 @@ export const toPrivateBooking = (backendBooking = {}) => ({
cancelled_at: backendBooking.cancelled_at || null, cancelled_at: backendBooking.cancelled_at || null,
cancel_by: backendBooking.cancel_by || '', cancel_by: backendBooking.cancel_by || '',
cancel_reason: backendBooking.cancel_reason || '', cancel_reason: backendBooking.cancel_reason || '',
can_cancel: backendBooking.can_cancel === true,
created_at: backendBooking.created_at || '', created_at: backendBooking.created_at || '',
order: toOrderBrief(backendBooking.order) order: toOrderBrief(backendBooking.order)
}) })
@@ -94,5 +95,16 @@ export const bookingApi = {
return request({ return request({
url: `/api/bookings/private/${id}` url: `/api/bookings/private/${id}`
}).then(toPrivateBooking) }).then(toPrivateBooking)
},
getPrivateBookingRefundQuote(id) {
return request({
url: `/api/bookings/private/${id}/refund-quote`
})
},
cancelPrivateBooking(id) {
return request({
url: `/api/bookings/private/${id}/cancel`,
method: 'POST'
}).then(toPrivateBooking)
} }
} }
@@ -0,0 +1,28 @@
import { request } from './request'
import { toPageQuery, toPagination } from './mappers'
const toCourseBrief = (backendCourse = {}) => ({
id: backendCourse.id ?? null,
title: backendCourse.title || '',
cover_image: backendCourse.cover_image || '',
updated_at: backendCourse.updated_at || ''
})
const toCourseDetail = (backendCourse = {}) => ({
...toCourseBrief(backendCourse),
detail: backendCourse.detail || ''
})
export const courseInfoApi = {
getCourses(params = {}) {
return request({
url: '/api/courses',
data: toPageQuery(params)
}).then((data) => toPagination(data, toCourseBrief))
},
getCourseDetail(id) {
return request({
url: `/api/courses/${id}`
}).then(toCourseDetail)
}
}
@@ -81,7 +81,18 @@ const toPrivateBookingGrid = (backendData = {}) => ({
slot_prices: backendData.slot_prices || {} slot_prices: backendData.slot_prices || {}
}) })
const toPrivateBookingConfig = (backendData = {}) => ({
member_advance_days: toNumber(backendData.member_advance_days, 7),
non_member_advance_days: toNumber(backendData.non_member_advance_days, 3)
})
export const privateBookingApi = { export const privateBookingApi = {
getPrivateBookingConfig() {
return request({
url: '/api/bookings/private/config'
}).then(toPrivateBookingConfig)
},
getPrivateBookingOptions(params = {}) { getPrivateBookingOptions(params = {}) {
return request({ return request({
url: '/api/bookings/private/options', url: '/api/bookings/private/options',
@@ -86,10 +86,12 @@ export function request(options = {}) {
} }
const error = normalizeError(res, `请求错误 ${res.statusCode}`) const error = normalizeError(res, `请求错误 ${res.statusCode}`)
if (!options.silent) {
uni.showToast({ uni.showToast({
title: error.message, title: error.message,
icon: 'none' icon: 'none'
}) })
}
reject(error) reject(error)
}, },
fail: (err) => { fail: (err) => {
@@ -39,7 +39,8 @@ const toTicketOrder = (backendData = {}) => ({
export const ticketApi = { export const ticketApi = {
getTicketProduct() { getTicketProduct() {
return request({ return request({
url: '/api/tickets/product' url: '/api/tickets/product',
silent: true
}).then(toTicketProduct) }).then(toTicketProduct)
}, },
getTicketAccount(params = {}) { getTicketAccount(params = {}) {
@@ -1,6 +1,6 @@
export const statusBadgeClass = (status) => { export const statusBadgeClass = (status) => {
if (['paid', 'booked', 'available', 'success', 'published'].includes(status)) return 'success' if (['paid', 'booked', 'available', 'success', 'published'].includes(status)) return 'success'
if (['pending_pay', 'pending'].includes(status)) return 'warning' if (['pending_pay', 'pending', 'part_refunded'].includes(status)) return 'warning'
if (['cancelled', 'closed', 'expired', 'refunded'].includes(status)) return 'danger' if (['cancelled', 'closed', 'expired', 'refunded'].includes(status)) return 'danger'
return 'muted' return 'muted'
} }
@@ -13,5 +13,11 @@ export const contentApi = {
return request({ return request({
url: `/api/contents/activities/${id}` url: `/api/contents/activities/${id}`
}).then(toActivityDetail) }).then(toActivityDetail)
},
getPageImages() {
return request({
url: '/api/contents/page-images'
})
} }
} }
+23 -9
View File
@@ -48,7 +48,8 @@
{ {
"path": "pages/profile/index", "path": "pages/profile/index",
"style": { "style": {
"navigationBarTitleText": "我的" "navigationBarTitleText": "我的",
"enablePullDownRefresh": true
} }
}, },
{ {
@@ -94,6 +95,19 @@
"navigationBarTitleText": "课程详情" "navigationBarTitleText": "课程详情"
} }
}, },
{
"path": "pages/comprehensive/courses/list",
"style": {
"navigationBarTitleText": "全部课程",
"enablePullDownRefresh": true
}
},
{
"path": "pages/comprehensive/courses/info-detail",
"style": {
"navigationBarTitleText": "课程详情"
}
},
{ {
"path": "pages/comprehensive/courses/membership-detail", "path": "pages/comprehensive/courses/membership-detail",
"style": { "style": {
@@ -311,10 +325,10 @@
"selectedIconPath": "static/comprehensive/tab/home-active.png" "selectedIconPath": "static/comprehensive/tab/home-active.png"
}, },
{ {
"pagePath": "pages/private/index", "pagePath": "pages/courses/index",
"text": "订场", "text": "约课",
"iconPath": "static/comprehensive/tab/private.png", "iconPath": "static/comprehensive/tab/course.png",
"selectedIconPath": "static/comprehensive/tab/private-active.png" "selectedIconPath": "static/comprehensive/tab/course-active.png"
}, },
{ {
"pagePath": "pages/mall/index", "pagePath": "pages/mall/index",
@@ -323,10 +337,10 @@
"selectedIconPath": "static/comprehensive/tab/mall-active.png" "selectedIconPath": "static/comprehensive/tab/mall-active.png"
}, },
{ {
"pagePath": "pages/courses/index", "pagePath": "pages/private/index",
"text": "约课", "text": "订场",
"iconPath": "static/comprehensive/tab/course.png", "iconPath": "static/comprehensive/tab/private.png",
"selectedIconPath": "static/comprehensive/tab/course-active.png" "selectedIconPath": "static/comprehensive/tab/private-active.png"
}, },
{ {
"pagePath": "pages/profile/index", "pagePath": "pages/profile/index",
@@ -63,8 +63,40 @@
</view> </view>
</view> </view>
<view v-if="booking.status === 'pending_pay'" class="fixed-action-bar"> <view v-if="booking.status === 'pending_pay' || (booking.status === 'booked' && booking.can_cancel)" class="fixed-action-bar">
<button class="primary-button action-btn" @tap="goPay">去支付</button> <button v-if="booking.status === 'booked' && booking.can_cancel" class="ghost-button action-btn" @tap="onCancelBooking">取消预订</button>
<button v-if="booking.status === 'pending_pay'" class="primary-button action-btn" @tap="goPay">去支付</button>
</view>
<view v-if="refundPopupVisible" class="refund-popup-mask" @tap="closeRefundPopup">
<view class="refund-popup" @tap.stop>
<view class="refund-popup-header">
<text class="refund-popup-title">取消预订</text>
<text class="refund-popup-close" @tap="closeRefundPopup">×</text>
</view>
<view class="refund-popup-body" v-if="refundQuote">
<view class="refund-rules">
<view class="refund-rules-header">
<text>距开始时间</text>
<text>退款比例</text>
<text>说明</text>
</view>
<view v-for="(rule, idx) in refundQuote.rules" :key="idx" class="refund-rule-row" :class="{ active: rule.description === refundQuote.applicable_tier_description }">
<text>{{ formatHoursBefore(rule.hours_before_start, idx) }}</text>
<text>{{ rule.refund_percentage }}%</text>
<text>{{ rule.description }}</text>
</view>
</view>
<view class="refund-amount-box">
<text class="refund-amount-label">本次退款金额</text>
<text class="refund-amount-value">¥{{ refundQuote.refund_amount }}</text>
</view>
</view>
<view class="refund-popup-footer">
<button class="ghost-button refund-btn" @tap="closeRefundPopup">再想想</button>
<button class="primary-button refund-btn" @tap="confirmCancelBooking" :disabled="refundLoading">确认取消</button>
</view>
</view>
</view> </view>
</view> </view>
</template> </template>
@@ -94,6 +126,49 @@ onLoad(async (query) => {
const goPay = () => { const goPay = () => {
uni.navigateTo({ url: `/pages/comprehensive/pay/index?order_id=${booking.value.order_id}` }) uni.navigateTo({ url: `/pages/comprehensive/pay/index?order_id=${booking.value.order_id}` })
} }
const refundPopupVisible = ref(false)
const refundQuote = ref(null)
const refundLoading = ref(false)
const onCancelBooking = async () => {
try {
const data = await bookingApi.getPrivateBookingRefundQuote(bookingId.value)
refundQuote.value = data
refundPopupVisible.value = true
} catch {
uni.showToast({ title: '获取退款信息失败', icon: 'none' })
}
}
const closeRefundPopup = () => {
refundPopupVisible.value = false
refundQuote.value = null
}
const confirmCancelBooking = async () => {
if (refundLoading.value) return
refundLoading.value = true
try {
await bookingApi.cancelPrivateBooking(bookingId.value)
refundPopupVisible.value = false
const amount = refundQuote.value?.refund_amount || '0'
uni.showToast({ title: `取消成功,退款¥${amount}`, icon: 'none' })
booking.value = await bookingApi.getPrivateBookingDetail(bookingId.value)
} catch {
uni.showToast({ title: '取消失败,请稍后重试', icon: 'none' })
} finally {
refundLoading.value = false
}
}
const formatHoursBefore = (hours, idx) => {
const h = Number(hours)
const rules = refundQuote.value?.rules || []
if (idx === 0 || h >= 24) return `>${h}小时`
const prev = Number(rules[idx - 1]?.hours_before_start ?? h)
return `${h}-${prev}小时`
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -198,4 +273,131 @@ const goPay = () => {
.action-btn { .action-btn {
flex: 1; flex: 1;
} }
.refund-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: flex-end;
}
.refund-popup {
width: 100%;
background: $bg-color-soft;
border-radius: $border-radius-lg $border-radius-lg 0 0;
overflow: hidden;
}
.refund-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: $spacing-md $spacing-lg;
background: #fff;
border-bottom: 1rpx solid $bg-color-hover;
}
.refund-popup-title {
font-size: 32rpx;
font-weight: 900;
color: $text-color-main;
}
.refund-popup-close {
font-size: 40rpx;
color: $text-color-muted;
padding: 0 $spacing-sm;
}
.refund-popup-body {
padding: $spacing-md $spacing-lg;
}
.refund-rules {
margin-bottom: $spacing-md;
background: #fff;
border-radius: $border-radius-base;
overflow: hidden;
}
.refund-rules-header {
display: flex;
padding: $spacing-sm $spacing-md;
background: $bg-color-hover;
font-size: 22rpx;
font-weight: 800;
color: $text-color-muted;
text {
flex: 1;
text-align: center;
&:first-child { flex: 0.8; }
&:last-child { flex: 1.4; }
}
}
.refund-rule-row {
display: flex;
padding: $spacing-sm $spacing-md;
border-bottom: 1rpx solid $bg-color-hover;
font-size: 22rpx;
color: $text-color-muted;
text {
flex: 1;
text-align: center;
&:first-child { flex: 0.8; }
&:last-child { flex: 1.4; text-align: left; }
}
&.active {
background: rgba(64, 158, 255, 0.08);
color: $color-primary;
font-weight: 800;
}
}
.refund-rule-row:last-child {
border-bottom: none;
}
.refund-amount-box {
display: flex;
align-items: center;
justify-content: space-between;
padding: $spacing-md;
background: #fff;
border-radius: $border-radius-base;
margin-bottom: $spacing-sm;
}
.refund-amount-label {
font-size: 26rpx;
color: $text-color-muted;
}
.refund-amount-value {
font-size: 36rpx;
font-weight: 900;
color: $color-danger;
}
.refund-popup-footer {
display: flex;
gap: $spacing-md;
padding: $spacing-md $spacing-lg $spacing-lg;
padding-bottom: calc(#{$spacing-lg} + env(safe-area-inset-bottom));
background: #fff;
}
.refund-btn {
flex: 1;
}
</style> </style>
@@ -2,8 +2,8 @@
<view class="courses-page"> <view class="courses-page">
<block> <block>
<view class="tab-bar"> <view class="tab-bar">
<view class="tab-item" :class="{ active: activeTab === 'membership' }" @tap="switchTab('membership')">会员卡</view> <view class="tab-item" :class="{ active: activeTab === 'membership' }" @tap="switchTab('membership')">课程</view>
<view class="tab-item" :class="{ active: activeTab === 'tickets' }" @tap="switchTab('tickets')">门票</view> <view class="tab-item" :class="{ active: activeTab === 'tickets' }" @tap="switchTab('tickets')">篮球门票</view>
</view> </view>
<template v-if="activeTab === 'tickets'"> <template v-if="activeTab === 'tickets'">
@@ -12,7 +12,6 @@
<view class="product-head"> <view class="product-head">
<view> <view>
<text class="product-title">篮球门票</text> <text class="product-title">篮球门票</text>
<text class="product-subtitle">购买后进入会员权益账户,到馆由前台核销。</text>
</view> </view>
</view> </view>
<view class="price-block"> <view class="price-block">
@@ -53,6 +52,25 @@
</button> </button>
</view> </view>
</view> </view>
<view v-else-if="ticketPaused" class="ticket-section">
<view class="card ticket-paused-card">
<text class="ticket-paused-title">门票暂停购买</text>
<text class="ticket-paused-desc">门票暂未开放购买,请稍后再来</text>
</view>
<view v-if="ticketRecords.length" class="ticket-records-section">
<view class="section-header">
<text class="section-title">我的门票</text>
</view>
<view v-for="record in ticketRecords" :key="record.id" class="card ticket-record-card">
<view class="ticket-record-top">
<text class="ticket-record-no">订单号 {{ record.order_no }}</text>
<text class="ticket-status-badge" :class="record.status">{{ record.status_label }}</text>
</view>
<text class="ticket-record-date">{{ formatDate(record.created_at) }}</text>
</view>
</view>
</view>
<view v-else class="load-more-tip">加载中...</view> <view v-else class="load-more-tip">加载中...</view>
</template> </template>
@@ -119,6 +137,7 @@ const isLoadingMore = ref(false)
let loadRequestId = 0 let loadRequestId = 0
const ticketProduct = ref(null) const ticketProduct = ref(null)
const ticketPaused = ref(false)
const ticketQty = ref(1) const ticketQty = ref(1)
const ticketSubmitting = ref(false) const ticketSubmitting = ref(false)
const ticketRecords = ref([]) const ticketRecords = ref([])
@@ -200,8 +219,11 @@ const loadTicketProduct = async () => {
if (ticketProduct.value) return if (ticketProduct.value) return
try { try {
ticketProduct.value = await ticketApi.getTicketProduct() ticketProduct.value = await ticketApi.getTicketProduct()
} catch { ticketPaused.value = false
// ignore } catch (error) {
if (error?.code === 40400 || error?.statusCode === 404) {
ticketPaused.value = true
}
} }
} }
@@ -263,6 +285,7 @@ const goDetail = (item) => {
const refresh = async () => { const refresh = async () => {
if (activeTab.value === 'tickets') { if (activeTab.value === 'tickets') {
ticketProduct.value = null ticketProduct.value = null
ticketPaused.value = false
ticketRecords.value = [] ticketRecords.value = []
await Promise.all([loadTicketProduct(), loadTicketRecords()]) await Promise.all([loadTicketProduct(), loadTicketRecords()])
} else { } else {
@@ -478,6 +501,25 @@ defineExpose({ refresh, loadMore })
margin-bottom: $spacing-md; margin-bottom: $spacing-md;
} }
.ticket-paused-card {
text-align: center;
padding: $spacing-xl $spacing-md;
}
.ticket-paused-title {
display: block;
color: $text-color-main;
font-size: 34rpx;
font-weight: 900;
}
.ticket-paused-desc {
display: block;
margin-top: $spacing-xs;
color: $text-color-muted;
font-size: 24rpx;
}
.product-head, .product-head,
.amount-row, .amount-row,
.stepper-row { .stepper-row {
@@ -0,0 +1,67 @@
<template>
<view class="article-detail container" v-if="course">
<text class="title">{{ course.title }}</text>
<text class="date">{{ formatDate(course.updated_at) }}</text>
<image v-if="course.cover_image" :src="course.cover_image" mode="aspectFill" class="cover" />
<view class="content-card card">
<rich-text :nodes="course.detail"></rich-text>
</view>
</view>
<view v-else class="loading-state container">
<text>加载中...</text>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { courseInfoApi } from '@/modules/comprehensive/api/courseInfo'
import { formatDate } from '@/modules/comprehensive/utils/date'
const course = ref(null)
onLoad(async (query) => {
course.value = await courseInfoApi.getCourseDetail(query.id)
})
</script>
<style lang="scss" scoped>
.title,
.date {
display: block;
}
.title {
color: $text-color-main;
font-size: 42rpx;
font-weight: 900;
line-height: 1.35;
}
.date {
margin-top: $spacing-sm;
color: $text-color-muted;
font-size: 24rpx;
}
.cover {
width: 100%;
height: 360rpx;
margin: $spacing-lg 0;
border-radius: $border-radius-lg;
background: $bg-color-hover;
}
.content-card {
color: $text-color-regular;
font-size: 28rpx;
line-height: 1.8;
}
.loading-state {
padding: 200rpx 0;
text-align: center;
color: $text-color-light;
font-size: 24rpx;
}
</style>
@@ -0,0 +1,126 @@
<template>
<view class="course-list-page container">
<view class="page-title-block">
<text class="page-title">全部课程</text>
</view>
<view class="article-list">
<view v-for="course in courses" :key="course.id" class="article-card card" @tap="goDetail(course.id)">
<image :src="course.cover_image" mode="aspectFill" class="cover" />
<view class="article-info">
<text class="title">{{ course.title }}</text>
<text class="date">{{ formatDate(course.updated_at) }}</text>
</view>
</view>
</view>
<view v-if="isLoading" class="load-more-tip">加载中...</view>
<view v-else-if="courses.length === 0" class="empty-state">暂无课程</view>
<view v-else-if="!hasMore" class="load-more-tip">已加载全部</view>
</view>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { courseInfoApi } from '@/modules/comprehensive/api/courseInfo'
import { formatDate } from '@/modules/comprehensive/utils/date'
const courses = ref([])
const page = ref(1)
const pageSize = 20
const total = ref(0)
const isLoading = ref(false)
const isLoadingMore = ref(false)
const hasMore = computed(() => courses.value.length < total.value)
onMounted(() => loadCourses(true))
onPullDownRefresh(() => {
loadCourses(true).finally(() => uni.stopPullDownRefresh())
})
onReachBottom(() => {
if (!hasMore.value || isLoadingMore.value) return
page.value += 1
loadCourses(false)
})
const loadCourses = async (reset) => {
if (reset) {
page.value = 1
isLoading.value = true
} else {
isLoadingMore.value = true
}
try {
const data = await courseInfoApi.getCourses({ page: page.value, pageSize })
courses.value = reset ? data.items : [...courses.value, ...data.items]
total.value = data.total
} finally {
isLoading.value = false
isLoadingMore.value = false
}
}
const goDetail = (id) => {
uni.navigateTo({ url: `/pages/comprehensive/courses/info-detail?id=${id}` })
}
</script>
<style lang="scss" scoped>
.page-title-block {
margin-bottom: $spacing-md;
}
.page-title,
.title,
.date {
display: block;
}
.page-title {
color: $text-color-main;
font-size: 42rpx;
font-weight: 900;
}
.article-card {
display: flex;
gap: $spacing-md;
margin-bottom: $spacing-md;
}
.cover {
width: 190rpx;
height: 136rpx;
border-radius: $border-radius-base;
background: $bg-color-hover;
flex-shrink: 0;
}
.article-info {
flex: 1;
min-width: 0;
}
.title {
color: $text-color-main;
font-size: 30rpx;
font-weight: 900;
line-height: 1.35;
}
.date {
margin-top: $spacing-md;
color: $text-color-muted;
font-size: 24rpx;
}
.empty-state {
padding: 160rpx 0;
text-align: center;
color: $text-color-light;
font-size: 24rpx;
}
</style>
@@ -38,6 +38,24 @@
<view v-else class="empty-placeholder">暂无公告活动</view> <view v-else class="empty-placeholder">暂无公告活动</view>
</view> </view>
<view class="section-container">
<view class="section-header">
<text class="section-title">课程</text>
<text v-if="hasMoreCourses" class="section-more" @tap="go('/pages/comprehensive/courses/list')">查看全部</text>
</view>
<view v-if="courses.length" class="activity-grid">
<view v-for="course in courses" :key="course.id" class="activity-card" @tap="go(`/pages/comprehensive/courses/info-detail?id=${course.id}`)">
<image :src="course.cover_image" mode="aspectFill" class="activity-cover" />
<view class="activity-info">
<text class="activity-title">{{ course.title }}</text>
<text class="activity-date">{{ formatDate(course.updated_at) }}</text>
</view>
</view>
</view>
<view v-else class="empty-placeholder">暂无课程</view>
</view>
<VenueSwitchHeader venue-name="综合场馆" /> <VenueSwitchHeader venue-name="综合场馆" />
</view> </view>
</view> </view>
@@ -46,20 +64,28 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { contentApi } from '@/modules/comprehensive/api/content' import { contentApi } from '@/modules/comprehensive/api/content'
import { courseInfoApi } from '@/modules/comprehensive/api/courseInfo'
import { formatDate } from '@/modules/comprehensive/utils/date' import { formatDate } from '@/modules/comprehensive/utils/date'
import VenueSwitchHeader from '@/components/VenueSwitchHeader.vue' import VenueSwitchHeader from '@/components/VenueSwitchHeader.vue'
const banners = ref([]) const banners = ref([])
const articles = ref([]) const articles = ref([])
const courses = ref([])
const hasMoreCourses = ref(false)
onMounted(() => { onMounted(() => {
loadPage() loadPage()
}) })
const loadPage = async () => { const loadPage = async () => {
const home = await contentApi.getHomeContent({ banner_limit: 5, article_limit: 3 }) const [home, courseData] = await Promise.all([
contentApi.getHomeContent({ banner_limit: 5, article_limit: 3 }),
courseInfoApi.getCourses({ page: 1, pageSize: 5 })
])
banners.value = home.banners || [] banners.value = home.banners || []
articles.value = home.articles || [] articles.value = home.articles || []
courses.value = courseData.items || []
hasMoreCourses.value = courseData.total > courses.value.length
} }
const go = (url) => { const go = (url) => {
@@ -131,6 +157,11 @@ defineExpose({ refresh: loadPage })
background-color: $text-color-main; background-color: $text-color-main;
} }
.section-more {
color: $text-color-muted;
font-size: 24rpx;
}
.activity-grid { .activity-grid {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -4,16 +4,16 @@
<view <view
class="tab-item" class="tab-item"
:class="{ active: activeTab === 'stored_value' }" :class="{ active: activeTab === 'stored_value' }"
@tap="activeTab = 'stored_value'" @tap="switchTab('stored_value')"
> >
储值卡 会员卡
</view> </view>
<view <view
class="tab-item" class="tab-item"
:class="{ active: activeTab === 'membership' }" :class="{ active: activeTab === 'membership' }"
@tap="activeTab = 'membership'" @tap="switchTab('membership')"
> >
会员卡 课程套餐
</view> </view>
</view> </view>
@@ -80,7 +80,18 @@ const goDetail = (item) => {
}) })
} }
defineExpose({ refresh: loadPage }) const switchTab = (tab) => {
if (activeTab.value === tab) return
activeTab.value = tab
loadPage()
}
const refresh = async () => {
await session.init()
await loadPage()
}
defineExpose({ refresh })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -23,12 +23,24 @@
<view class="card method-card"> <view class="card method-card">
<text class="card-title">支付方式</text> <text class="card-title">支付方式</text>
<view class="method-item active"> <view
v-if="!isWechatOnlyOrder"
class="method-item"
:class="{ active: payMethod === 'balance', disabled: !canUseBalance }"
@tap="selectMethod('balance')"
>
<view>
<text class="method-title">余额支付</text>
<text class="method-sub">当前余额 ¥{{ balance.balance }}</text>
</view>
<text class="method-check">{{ payMethod === 'balance' ? '已选' : '' }}</text>
</view>
<view class="method-item" :class="{ active: payMethod === 'wechat' }" @tap="selectMethod('wechat')">
<view> <view>
<text class="method-title">微信支付</text> <text class="method-title">微信支付</text>
<text class="method-sub">使用微信支付完成订单</text> <text class="method-sub">使用微信支付完成订单</text>
</view> </view>
<text class="method-check">已选</text> <text class="method-check">{{ payMethod === 'wechat' ? '已选' : '' }}</text>
</view> </view>
</view> </view>
@@ -49,19 +61,27 @@
<script setup> <script setup>
import { computed, onUnmounted, ref } from 'vue' import { computed, onUnmounted, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { memberApi } from '@/modules/comprehensive/api/member'
import { orderApi } from '@/modules/comprehensive/api/order' import { orderApi } from '@/modules/comprehensive/api/order'
import { formatDateTime } from '@/modules/comprehensive/utils/date' import { formatDateTime } from '@/modules/comprehensive/utils/date'
import { toAmountNumber } from '@/modules/comprehensive/utils/money'
const orderId = ref('') const orderId = ref('')
const order = ref(null) const order = ref(null)
const balance = ref({ balance: '0.00' })
const payMethod = ref('balance')
const isSubmitting = ref(false) const isSubmitting = ref(false)
const nowTimestamp = ref(Date.now()) const nowTimestamp = ref(Date.now())
let countdownTimer = null let countdownTimer = null
const canPay = computed(() => order.value?.status === 'pending_pay') const isWechatOnlyOrder = computed(() => order.value?.business_type !== 'private_booking')
const canUseBalance = computed(() => {
return toAmountNumber(balance.value.balance) >= toAmountNumber(order.value?.amount)
})
const canPay = computed(() => order.value?.status === 'pending_pay' && (payMethod.value !== 'balance' || canUseBalance.value))
const payButtonText = computed(() => { const payButtonText = computed(() => {
if (order.value?.status !== 'pending_pay') return '订单不可支付' if (order.value?.status !== 'pending_pay') return '订单不可支付'
return '微信支付' return payMethod.value === 'balance' ? '余额支付' : '微信支付'
}) })
const paymentRemainingSeconds = computed(() => { const paymentRemainingSeconds = computed(() => {
if (!order.value?.expire_at) return 0 if (!order.value?.expire_at) return 0
@@ -87,6 +107,12 @@ onUnmounted(() => {
const loadPage = async () => { const loadPage = async () => {
order.value = await orderApi.getOrderDetail(orderId.value) order.value = await orderApi.getOrderDetail(orderId.value)
if (isWechatOnlyOrder.value) {
payMethod.value = 'wechat'
} else {
balance.value = await memberApi.getBalance()
payMethod.value = canUseBalance.value ? 'balance' : 'wechat'
}
updateCountdownTimer() updateCountdownTimer()
} }
@@ -108,13 +134,26 @@ const stopCountdown = () => {
const padTime = (value) => String(value).padStart(2, '0') const padTime = (value) => String(value).padStart(2, '0')
const selectMethod = (method) => {
if (method === 'balance' && !canUseBalance.value) {
uni.showToast({ title: '余额不足', icon: 'none' })
return
}
payMethod.value = method
}
const pay = async () => { const pay = async () => {
if (!canPay.value || isSubmitting.value) return if (!canPay.value || isSubmitting.value) return
isSubmitting.value = true isSubmitting.value = true
try { try {
if (payMethod.value === 'balance') {
await orderApi.payOrderByBalance(order.value.id)
uni.showToast({ title: '支付成功', icon: 'success' })
} else {
const paymentData = await orderApi.payOrderByWechat(order.value.id) const paymentData = await orderApi.payOrderByWechat(order.value.id)
await requestWechatPayment(paymentData.wechat) await requestWechatPayment(paymentData.wechat)
uni.showToast({ title: '支付已提交', icon: 'success' }) uni.showToast({ title: '支付已提交', icon: 'success' })
}
await loadPage() await loadPage()
setTimeout(() => { setTimeout(() => {
uni.redirectTo({ url: `/pages/comprehensive/orders/detail?id=${order.value.id}` }) uni.redirectTo({ url: `/pages/comprehensive/orders/detail?id=${order.value.id}` })
@@ -218,6 +257,10 @@ const requestWechatPayment = (wechatPayload) => new Promise((resolve, reject) =>
color: $color-primary; color: $color-primary;
} }
.method-item.disabled {
opacity: 0.45;
}
.method-title, .method-title,
.method-sub { .method-sub {
display: block; display: block;
@@ -2,14 +2,15 @@
<view class="private-page"> <view class="private-page">
<view class="tab-bar"> <view class="tab-bar">
<view <view
v-for="item in typeOptions" v-for="tab in mainTabs"
:key="item.value" :key="tab.value"
class="tab-item" class="tab-item"
:class="{ active: courtType === item.value }" :class="{ active: activeTab === tab.value }"
@tap="selectType(item.value)" @tap="switchTab(tab.value)"
>{{ item.label }}</view> >{{ tab.label }}</view>
</view> </view>
<template v-if="activeTab === 'booking'">
<scroll-view scroll-x class="date-bar" :show-scrollbar="false"> <scroll-view scroll-x class="date-bar" :show-scrollbar="false">
<view class="date-list"> <view class="date-list">
<view <view
@@ -86,6 +87,22 @@
<button class="primary-button submit-btn" :disabled="!canConfirm" @tap="goConfirm">预约</button> <button class="primary-button submit-btn" :disabled="!canConfirm" @tap="goConfirm">预约</button>
</template> </template>
</view> </view>
</template>
<template v-else-if="activeTab === 'history'">
<scroll-view scroll-y class="history-scroll" @scrolltolower="loadHistory()">
<view v-for="item in historyList" :key="item.id" class="history-card" @tap="goBookingDetail(item)">
<view class="history-card-top">
<text class="history-court">{{ historyCourtNames(item) }}</text>
<text class="history-status">{{ item.status_label }}</text>
</view>
<text class="history-time">{{ historyTimeText(item) }}</text>
<text class="history-amount">¥{{ item.order?.amount || '--' }}</text>
</view>
<view v-if="historyLoading" class="history-tip">加载中...</view>
<view v-else-if="historyList.length === 0" class="history-empty">暂无预定记录</view>
</scroll-view>
</template>
</view> </view>
</template> </template>
@@ -94,6 +111,8 @@ import { computed, onMounted, ref } from 'vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { privateBookingApi } from '@/modules/comprehensive/api/privateBooking' import { privateBookingApi } from '@/modules/comprehensive/api/privateBooking'
import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session' import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session'
import { bookingApi } from '@/modules/comprehensive/api/booking'
import { formatTimeRange } from '@/modules/comprehensive/utils/date'
import { toAmountNumber } from '@/modules/comprehensive/utils/money' import { toAmountNumber } from '@/modules/comprehensive/utils/money'
const HEADER_HEIGHT = 75 const HEADER_HEIGHT = 75
@@ -101,9 +120,11 @@ const CELL_HEIGHT = 107
const TIME_COL_WIDTH = 128 const TIME_COL_WIDTH = 128
const COURT_COL_WIDTH = 149 const COURT_COL_WIDTH = 149
const typeOptions = [ const mainTabs = [
{ label: '羽毛球', value: 'badminton' } { label: '订场', value: 'booking' },
{ label: '我的预定', value: 'history' }
] ]
const activeTab = ref('booking')
const session = useComprehensiveSessionStore() const session = useComprehensiveSessionStore()
const courtType = ref('badminton') const courtType = ref('badminton')
@@ -115,11 +136,21 @@ const selectedCells = ref({})
const now = ref(dayjs()) const now = ref(dayjs())
let loadRequestId = 0 let loadRequestId = 0
const historyList = ref([])
const historyLoading = ref(false)
const historyPage = ref(1)
const historyPageSize = 20
const historyHasMore = ref(true)
const bookingConfig = ref({ member_advance_days: 7, non_member_advance_days: 3 })
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
const hasPositiveBalance = computed(() => toAmountNumber(session.member?.balance) > 0) const hasPositiveBalance = computed(() => toAmountNumber(session.member?.balance) > 0)
const dateOptions = computed(() => { const dateOptions = computed(() => {
const today = dayjs() const today = dayjs()
const dateCount = hasPositiveBalance.value ? 7 : 1 const dateCount = hasPositiveBalance.value
? bookingConfig.value.member_advance_days
: bookingConfig.value.non_member_advance_days
return Array.from({ length: dateCount }).map((_, i) => { return Array.from({ length: dateCount }).map((_, i) => {
const d = today.add(i, 'day') const d = today.add(i, 'day')
return { return {
@@ -130,9 +161,6 @@ const dateOptions = computed(() => {
}) })
}) })
const openTime = computed(() => normalizeClock(grid.value?.open_time))
const closeTime = computed(() => normalizeClock(grid.value?.close_time))
const occupationMap = computed(() => { const occupationMap = computed(() => {
const map = {} const map = {}
if (!grid.value) return map if (!grid.value) return map
@@ -253,10 +281,59 @@ async function loadGrid() {
} }
} }
function selectType(type) { function switchTab(tab) {
if (courtType.value === type) return if (activeTab.value === tab) return
courtType.value = type activeTab.value = tab
loadGrid() if (tab === 'history') {
loadHistory(true)
}
}
async function loadHistory(reset = false) {
if (historyLoading.value) return
if (reset) {
historyPage.value = 1
historyHasMore.value = true
}
if (!historyHasMore.value) return
historyLoading.value = true
try {
const data = await bookingApi.getPrivateBookings({
page: historyPage.value,
pageSize: historyPageSize
})
const tenDaysAgo = dayjs().subtract(10, 'day')
const booked = data.items.filter(
(item) => item.status === 'booked' && item.start_at && dayjs(item.start_at) >= tenDaysAgo
)
if (reset) {
historyList.value = booked
} else {
historyList.value = [...historyList.value, ...booked]
}
historyHasMore.value = data.page * data.page_size < data.total
if (historyHasMore.value) historyPage.value++
} catch {
if (reset) historyList.value = []
} finally {
historyLoading.value = false
}
}
function historyCourtNames(item) {
return item.courts?.map((c) => c.name).join('、') || '包场预约'
}
function historyTimeText(item) {
if (!item.start_at || !item.end_at) return '-'
const start = dayjs(item.start_at)
return `${start.month() + 1}/${start.date()} ${formatTimeRange(item.start_at, item.end_at)}`
}
function goBookingDetail(item) {
uni.navigateTo({
url: `/pages/comprehensive/bookings/private-detail?id=${item.id}`
})
} }
function selectDate(date) { function selectDate(date) {
@@ -360,15 +437,26 @@ function normalizeClock(value) {
async function refresh() { async function refresh() {
await session.init() await session.init()
if (!hasPositiveBalance.value && selectedDate.value !== dayjs().format('YYYY-MM-DD')) { try {
selectedDate.value = dayjs().format('YYYY-MM-DD') bookingConfig.value = await privateBookingApi.getPrivateBookingConfig()
} catch {
// 获取配置失败时使用默认值 7/3,不阻塞页面
}
if (!hasPositiveBalance.value) {
const todayStr = dayjs().format('YYYY-MM-DD')
const maxOffset = bookingConfig.value.non_member_advance_days - 1
const maxDateStr = dayjs().add(maxOffset, 'day').format('YYYY-MM-DD')
if (selectedDate.value < todayStr || selectedDate.value > maxDateStr) {
selectedDate.value = todayStr
}
} }
now.value = dayjs() now.value = dayjs()
await loadGrid() await loadGrid()
} }
defineExpose({ defineExpose({
refresh refresh,
loadGrid
}) })
</script> </script>
@@ -678,4 +766,65 @@ defineExpose({
color: $text-color-light; color: $text-color-light;
font-size: 24rpx; font-size: 24rpx;
} }
.history-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: $text-color-light;
font-size: 24rpx;
}
.history-scroll {
flex: 1;
min-height: 0;
}
.history-card {
background: $bg-color-main;
border-radius: $border-radius-base;
padding: $spacing-md;
margin: $spacing-sm $spacing-md;
display: flex;
flex-direction: column;
gap: $spacing-xs;
}
.history-card-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.history-court {
font-size: 30rpx;
font-weight: 800;
color: $text-color-main;
}
.history-status {
font-size: 22rpx;
font-weight: 700;
color: #52c41a;
}
.history-time {
font-size: 26rpx;
font-weight: 600;
color: $text-color-regular;
}
.history-amount {
font-size: 32rpx;
font-weight: 900;
color: $color-primary;
}
.history-tip {
text-align: center;
padding: $spacing-md;
color: $text-color-light;
font-size: 24rpx;
}
</style> </style>
@@ -77,7 +77,7 @@
</template> </template>
<script setup> <script setup>
import { computed, onMounted, ref } from 'vue' import { computed, ref } from 'vue'
import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session' import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session'
import { memberApi } from '@/modules/comprehensive/api/member' import { memberApi } from '@/modules/comprehensive/api/member'
import { formatAmount } from '@/modules/comprehensive/utils/money' import { formatAmount } from '@/modules/comprehensive/utils/money'
@@ -88,21 +88,11 @@ const balance = ref('0.00')
const balanceText = computed(() => formatAmount(balance.value)) const balanceText = computed(() => formatAmount(balance.value))
const menuItems = [ const menuItems = [
{ title: '我的会员卡', url: '/pages/comprehensive/membership/cards', theme: 'profile-theme', icon: 'profile' }, { title: '我的课程套餐', url: '/pages/comprehensive/membership/cards', theme: 'profile-theme', icon: 'profile' },
{ title: '我的订单', url: '/pages/comprehensive/profile/orders', theme: 'order-theme', icon: 'order' }, { title: '我的订单', url: '/pages/comprehensive/profile/orders', theme: 'order-theme', icon: 'order' },
{ title: '余额流水', url: '/pages/comprehensive/profile/balance', theme: 'order-theme', icon: 'order' } { title: '余额流水', url: '/pages/comprehensive/profile/balance', theme: 'order-theme', icon: 'order' }
] ]
onMounted(async () => {
await session.init()
try {
const data = await memberApi.getBalance()
balance.value = data.balance
} catch {
balance.value = '0.00'
}
})
const login = () => { const login = () => {
uni.navigateTo({ url: '/pages/comprehensive/profile/login' }) uni.navigateTo({ url: '/pages/comprehensive/profile/login' })
} }
@@ -116,7 +106,17 @@ const go = (url) => {
uni.navigateTo({ url }) uni.navigateTo({ url })
} }
defineExpose({ onPageShow: () => {} }) const refresh = async () => {
await session.init()
try {
const data = await memberApi.getBalance()
balance.value = data.balance
} catch {
balance.value = '0.00'
}
}
defineExpose({ onPageShow: refresh, refresh })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -43,6 +43,7 @@ const statusOptions = [
{ label: '全部', value: '' }, { label: '全部', value: '' },
{ label: '待支付', value: 'pending_pay' }, { label: '待支付', value: 'pending_pay' },
{ label: '已支付', value: 'paid' }, { label: '已支付', value: 'paid' },
{ label: '部分退款', value: 'part_refunded' },
{ label: '已关闭', value: 'closed' }, { label: '已关闭', value: 'closed' },
{ label: '已退款', value: 'refunded' } { label: '已退款', value: 'refunded' }
] ]
@@ -192,6 +193,11 @@ loadOrders(true)
color: $color-warning; color: $color-warning;
} }
.status-tag.part_refunded {
background: $color-warning-light;
color: $color-warning;
}
.status-tag.closed { .status-tag.closed {
background: $bg-color-hover; background: $bg-color-hover;
color: $text-color-muted; color: $text-color-muted;
+33 -27
View File
@@ -1,44 +1,50 @@
<template> <template>
<view class="dev-page"> <view class="image-page">
<view class="dev-content"> <image
<text class="dev-icon">🚧</text> v-if="imageUrl"
<text class="dev-title">正在开发中</text> class="full-image"
<text class="dev-desc">此功能即将上线,敬请期待</text> :src="imageUrl"
</view> mode="widthFix"
:show-menu-by-longpress="true"
/>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'
import { contentApi } from '@/modules/ice/api/content'
const imageUrl = ref('')
const loadData = async () => {
try {
const data = await contentApi.getPageImages()
imageUrl.value = data.image_1 || ''
} catch (e) {
// ignore
}
}
defineExpose({
refresh() {
loadData()
}
})
loadData()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.dev-page { .image-page {
min-height: 100vh; min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: $bg-color-soft; background: $bg-color-soft;
}
.dev-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: $spacing-md;
} }
.dev-icon { .full-image {
font-size: 80rpx; width: 100%;
} display: block;
.dev-title {
color: $text-color-main;
font-size: 36rpx;
font-weight: 800;
}
.dev-desc {
color: $text-color-muted;
font-size: 26rpx;
} }
</style> </style>
+32 -28
View File
@@ -1,47 +1,51 @@
<template> <template>
<view class="dev-page"> <view class="image-page">
<view class="dev-content"> <image
<text class="dev-icon">🚧</text> v-if="imageUrl"
<text class="dev-title">正在开发中</text> class="full-image"
<text class="dev-desc">此功能即将上线,敬请期待</text> :src="imageUrl"
</view> mode="widthFix"
:show-menu-by-longpress="true"
/>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'
import { contentApi } from '@/modules/ice/api/content'
const imageUrl = ref('')
const loadData = async () => {
try {
const data = await contentApi.getPageImages()
imageUrl.value = data.image_2 || ''
} catch (e) {
// ignore
}
}
defineExpose({ defineExpose({
refresh() {} refresh() {
loadData()
},
loadGrid() {}
}) })
loadData()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.dev-page { .image-page {
min-height: 100vh; min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: $bg-color-soft; background: $bg-color-soft;
}
.dev-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: $spacing-md;
} }
.dev-icon { .full-image {
font-size: 80rpx; width: 100%;
} display: block;
.dev-title {
color: $text-color-main;
font-size: 36rpx;
font-weight: 800;
}
.dev-desc {
color: $text-color-muted;
font-size: 26rpx;
} }
</style> </style>
+2 -2
View File
@@ -165,7 +165,7 @@
<view class="svg-card-chip" /> <view class="svg-card-chip" />
</view> </view>
</view> </view>
<text class="item-title">我的会员卡</text> <text class="item-title">我的课程套餐</text>
</view> </view>
<view class="item-right"> <view class="item-right">
<view class="count-badge" v-if="member.cardCount > 0"> <view class="count-badge" v-if="member.cardCount > 0">
@@ -497,7 +497,7 @@ const formatDate = (dateStr) => {
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日` return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
} }
defineExpose({ onPageShow: refreshSession }) defineExpose({ onPageShow: refreshSession, refresh: refreshSession })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
+10 -1
View File
@@ -7,13 +7,22 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { onPullDownRefresh } from '@dcloudio/uni-app' import { onPullDownRefresh, onShow } from '@dcloudio/uni-app'
import { getStoredVenue } from '@/utils/venue' import { getStoredVenue } from '@/utils/venue'
import ComprehensiveMall from '@/pages/comprehensive/mall/index.vue' import ComprehensiveMall from '@/pages/comprehensive/mall/index.vue'
import IceMall from '@/pages/ice/mall/index.vue' import IceMall from '@/pages/ice/mall/index.vue'
const isComprehensive = getStoredVenue() === 'comprehensive' const isComprehensive = getStoredVenue() === 'comprehensive'
const contentRef = ref() const contentRef = ref()
let isFirstShow = true
onShow(() => {
if (isFirstShow) {
isFirstShow = false
return
}
contentRef.value?.refresh?.()
})
onPullDownRefresh(async () => { onPullDownRefresh(async () => {
await contentRef.value?.refresh?.() await contentRef.value?.refresh?.()
+23 -3
View File
@@ -7,7 +7,7 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app' import { onShow, onHide } from '@dcloudio/uni-app'
import { getStoredVenue } from '@/utils/venue' import { getStoredVenue } from '@/utils/venue'
import ComprehensivePrivate from '@/pages/comprehensive/private/index.vue' import ComprehensivePrivate from '@/pages/comprehensive/private/index.vue'
import IcePrivate from '@/pages/ice/private/index.vue' import IcePrivate from '@/pages/ice/private/index.vue'
@@ -15,13 +15,33 @@ import IcePrivate from '@/pages/ice/private/index.vue'
const isComprehensive = getStoredVenue() === 'comprehensive' const isComprehensive = getStoredVenue() === 'comprehensive'
const contentRef = ref() const contentRef = ref()
let isFirstShow = true let isFirstShow = true
let autoRefreshTimer = null
const startAutoRefresh = () => {
stopAutoRefresh()
autoRefreshTimer = setInterval(() => {
contentRef.value?.loadGrid?.()
}, 5000)
}
const stopAutoRefresh = () => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
autoRefreshTimer = null
}
}
onShow(() => { onShow(() => {
if (isFirstShow) { if (isFirstShow) {
isFirstShow = false isFirstShow = false
return } else {
}
contentRef.value?.refresh?.() contentRef.value?.refresh?.()
}
startAutoRefresh()
})
onHide(() => {
stopAutoRefresh()
}) })
</script> </script>
+6 -1
View File
@@ -7,7 +7,7 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app' import { onShow, onPullDownRefresh } from '@dcloudio/uni-app'
import { getStoredVenue } from '@/utils/venue' import { getStoredVenue } from '@/utils/venue'
import ComprehensiveProfile from '@/pages/comprehensive/profile/index.vue' import ComprehensiveProfile from '@/pages/comprehensive/profile/index.vue'
import IceProfile from '@/pages/ice/profile/index.vue' import IceProfile from '@/pages/ice/profile/index.vue'
@@ -18,6 +18,11 @@ const contentRef = ref()
onShow(() => { onShow(() => {
contentRef.value?.onPageShow?.() contentRef.value?.onPageShow?.()
}) })
onPullDownRefresh(async () => {
await contentRef.value?.refresh?.()
uni.stopPullDownRefresh()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>