Files
ShaFaFanXin/前端/pages/user/booking-list.uvue

514 lines
10 KiB
Plaintext

<template>
<view class="page">
<!-- 状态筛选 -->
<view class="status-bar">
<scroll-view class="status-scroll" scroll-x>
<view class="status-list">
<view
class="status-item"
:class="{ 'status-active': currentStatus == item.value }"
v-for="item in statusList"
:key="item.value"
@click="selectStatus(item.value)"
>
<text
class="status-text"
:class="{ 'status-text-active': currentStatus == item.value }"
>{{ item.label }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 预约列表 -->
<scroll-view
class="list-scroll"
scroll-y
@scrolltolower="loadMore"
>
<view class="booking-list">
<view
class="booking-card"
v-for="item in bookingList"
:key="item.id"
@click="goToDetail(item.id)"
>
<view class="card-header">
<text class="booking-number">预约编号:{{ item.bookingNumber }}</text>
<view class="status-badge" :class="'status-' + item.status">
<text class="status-badge-text">{{ getStatusText(item.status) }}</text>
</view>
</view>
<view class="card-body">
<view class="info-row">
<text class="info-label">服务类型</text>
<text class="info-value">{{ item.serviceName }}</text>
</view>
<view class="info-row">
<text class="info-label">预约时间</text>
<text class="info-value">{{ formatDate(item.appointmentTime) }}</text>
</view>
<view class="info-row">
<text class="info-label">联系电话</text>
<text class="info-value">{{ item.contactPhone }}</text>
</view>
<view class="info-row">
<text class="info-label">服务地址</text>
<text class="info-value address">{{ item.address }}</text>
</view>
</view>
<view class="card-footer">
<text class="create-time">创建时间:{{ formatDate(item.createdAt) }}</text>
<view class="action-btns">
<view
class="action-btn cancel-btn"
v-if="item.status == 'pending' || item.status == 'confirmed'"
@click.stop="cancelBooking(item.id)"
>
<text class="btn-text">取消预约</text>
</view>
<view class="action-btn primary-btn" @click.stop="callService">
<text class="btn-text">联系客服</text>
</view>
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="load-status">
<text class="load-text" v-if="loading">加载中...</text>
<text class="load-text" v-else-if="noMore">没有更多了</text>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && bookingList.length == 0">
<text class="empty-icon">📋</text>
<text class="empty-text">暂无预约记录</text>
<view class="empty-btn" @click="goToBooking">
<text class="empty-btn-text">立即预约</text>
</view>
</view>
<!-- 底部间距 -->
<view class="bottom-space"></view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { getMyBookings, cancelBooking as cancelBookingApi } from '@/api/index.uts'
import { PAGE_SIZE } from '@/utils/config.uts'
// 预约状态类型
type StatusItem = {
value : string
label : string
}
// 预约项类型
type BookingItem = {
id : number
bookingNumber : string
serviceName : string
appointmentTime : string
contactPhone : string
address : string
status : string
createdAt : string
}
// 状态列表
const statusList = ref<StatusItem[]>([
{ value: 'all', label: '全部' },
{ value: 'pending', label: '待确认' },
{ value: 'confirmed', label: '已确认' },
{ value: 'in_progress', label: '进行中' },
{ value: 'completed', label: '已完成' },
{ value: 'cancelled', label: '已取消' }
])
// 当前状态
const currentStatus = ref('all')
// 预约列表
const bookingList = ref<BookingItem[]>([])
// 分页
const page = ref(1)
const total = ref(0)
// 加载状态
const loading = ref(false)
const noMore = ref(false)
// 选择状态
const selectStatus = (status : string) => {
if (currentStatus.value == status) return
currentStatus.value = status
page.value = 1
bookingList.value = []
noMore.value = false
fetchBookingList()
}
// 获取预约列表
const fetchBookingList = async () => {
if (loading.value || noMore.value) return
loading.value = true
try {
const params = {
page: page.value,
limit: PAGE_SIZE,
status: currentStatus.value != 'all' ? currentStatus.value : undefined
} as UTSJSONObject
const res = await getMyBookings(params)
if (res.code == 0 && res.data != null) {
const data = res.data as UTSJSONObject
const list = data['list'] as UTSJSONObject[] || []
total.value = data['total'] as number || 0
const newList = list.map((item) : BookingItem => {
const service = item['service'] as UTSJSONObject || {}
return {
id: item['id'] as number,
bookingNumber: item['bookingNumber'] as string,
serviceName: service['name'] as string || '',
appointmentTime: item['appointmentTime'] as string,
contactPhone: item['contactPhone'] as string,
address: item['address'] as string,
status: item['status'] as string,
createdAt: item['createdAt'] as string
} as BookingItem
})
if (page.value == 1) {
bookingList.value = newList
} else {
bookingList.value = [...bookingList.value, ...newList]
}
if (bookingList.value.length >= total.value) {
noMore.value = true
}
}
} catch (e) {
console.error('获取预约列表失败', e)
uni.showToast({
title: '加载失败',
icon: 'none'
})
}
loading.value = false
}
// 加载更多
const loadMore = () => {
if (!noMore.value && !loading.value) {
page.value++
fetchBookingList()
}
}
// 获取状态文本
const getStatusText = (status : string) : string => {
const map = {
pending: '待确认',
confirmed: '已确认',
in_progress: '进行中',
completed: '已完成',
cancelled: '已取消'
} as UTSJSONObject
return map[status] as string || status
}
// 格式化日期
const formatDate = (dateStr : string) : string => {
if (!dateStr) return ''
return dateStr.split('T')[0].replace(/-/g, '/')
}
// 取消预约
const cancelBooking = (id : number) => {
uni.showModal({
title: '提示',
content: '确定要取消这个预约吗?',
success: async (res) => {
if (res.confirm) {
try {
await cancelBookingApi(String(id))
uni.showToast({
title: '取消成功',
icon: 'success'
})
// 重新加载列表
page.value = 1
bookingList.value = []
noMore.value = false
fetchBookingList()
} catch (e) {
uni.showToast({
title: '取消失败',
icon: 'none'
})
}
}
}
})
}
// 跳转详情
const goToDetail = (id : number) => {
uni.showToast({
title: '详情功能开发中',
icon: 'none'
})
}
// 联系客服
const callService = () => {
uni.makePhoneCall({
phoneNumber: '400-888-8888',
fail: () => {
uni.showToast({
title: '拨打电话失败',
icon: 'none'
})
}
})
}
// 去预约
const goToBooking = () => {
uni.navigateTo({
url: '/pages/booking/index'
})
}
onLoad(() => {
fetchBookingList()
})
</script>
<style lang="scss">
.page {
flex: 1;
background-color: #f5f5f5;
}
/* 状态栏 */
.status-bar {
background-color: #ffffff;
padding: 24rpx 0;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #EBEEF5;
}
.status-scroll {
width: 100%;
}
.status-list {
flex-direction: row;
padding: 0 32rpx;
}
.status-item {
margin-right: 32rpx;
padding: 12rpx 24rpx;
border-radius: 32rpx;
background-color: #f5f5f5;
}
.status-active {
background-color: #D4A574;
}
.status-text {
font-size: 28rpx;
color: #666666;
}
.status-text-active {
color: #ffffff;
}
/* 列表 */
.list-scroll {
flex: 1;
}
.booking-list {
padding: 24rpx 32rpx;
}
.booking-card {
background-color: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
.card-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
padding-bottom: 24rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #EBEEF5;
}
.booking-number {
font-size: 28rpx;
color: #333333;
font-weight: 500;
}
.status-badge {
padding: 8rpx 16rpx;
border-radius: 8rpx;
}
.status-pending {
background-color: #FFF3E0;
}
.status-confirmed {
background-color: #E3F2FD;
}
.status-in_progress {
background-color: #E8F5E9;
}
.status-completed {
background-color: #F3E5F5;
}
.status-cancelled {
background-color: #FFEBEE;
}
.status-badge-text {
font-size: 24rpx;
color: #666666;
}
.card-body {
margin-bottom: 24rpx;
}
.info-row {
flex-direction: row;
margin-bottom: 16rpx;
}
.info-label {
width: 140rpx;
font-size: 28rpx;
color: #999999;
}
.info-value {
flex: 1;
font-size: 28rpx;
color: #333333;
}
.address {
line-height: 1.6;
}
.card-footer {
padding-top: 24rpx;
border-top-width: 1rpx;
border-top-style: solid;
border-top-color: #EBEEF5;
}
.create-time {
font-size: 24rpx;
color: #999999;
margin-bottom: 16rpx;
}
.action-btns {
flex-direction: row;
justify-content: flex-end;
}
.action-btn {
padding: 12rpx 32rpx;
border-radius: 8rpx;
margin-left: 16rpx;
}
.cancel-btn {
background-color: #f5f5f5;
}
.primary-btn {
background-color: #D4A574;
}
.btn-text {
font-size: 28rpx;
color: #333333;
}
.primary-btn .btn-text {
color: #ffffff;
}
/* 加载状态 */
.load-status {
padding: 32rpx;
align-items: center;
}
.load-text {
font-size: 28rpx;
color: #999999;
}
/* 空状态 */
.empty-state {
padding: 120rpx 32rpx;
align-items: center;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 32rpx;
}
.empty-text {
font-size: 28rpx;
color: #999999;
margin-bottom: 48rpx;
}
.empty-btn {
padding: 16rpx 48rpx;
background-color: #D4A574;
border-radius: 48rpx;
}
.empty-btn-text {
font-size: 28rpx;
color: #ffffff;
}
.bottom-space {
height: 32rpx;
}
</style>