Files
xinlong-shop-taro/src_bak/pages/store/booking/index.tsx
赵忠林 1fa58040f3 feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构
- 新增地址编辑页面,支持地址智能识别和定位选点功能
- 地址编辑支持省市区选择及默认地址设置
- 新增地址列表页面,支持地址展示、删除、编辑和选择功能
- 实现售后申请页面,支持选择售后类型和退款原因
- 售后申请支持商品选择、退款金额计算和凭证上传
- 新增售后详情页面,支持售后状态展示及申请取消
- 优化页面加载和用户交互体验,增加错误提示和权限处理
2026-07-01 12:11:56 +08:00

390 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react'
import { View, Text, ScrollView, Input } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useUser } from '@/hooks/useUser'
import { createShopBooking } from '@/api/shop/shopBooking'
import { getShopStore } from '@/api/shop/shopStore'
import type { ShopStore } from '@/api/shop/shopStore/model'
import BottomButton from '@/components/common/BottomButton'
import './index.scss'
import dayjs from 'dayjs'
definePageConfig({
navigationBarTitleText: '穿线预约',
})
const BookingPage: React.FC = () => {
const { isLoggedIn } = useUser()
const { storeId: initStoreId } = Taro.getCurrentInstance().router?.params || {}
// 门店信息
const [storeInfo, setStoreInfo] = useState<ShopStore | null>(null)
// 表单数据
const [date, setDate] = useState('')
const [time, setTime] = useState('')
const [serviceType, setServiceType] = useState('')
const [contactName, setContactName] = useState('')
const [contactPhone, setContactPhone] = useState('')
const [remark, setRemark] = useState('')
const [submitting, setSubmitting] = useState(false)
// 日期范围今天起30天
const dateRange = (() => {
const dates: string[] = []
const today = dayjs()
for (let i = 0; i < 30; i++) {
dates.push(today.add(i, 'day').format('YYYY-MM-DD'))
}
return dates
})()
// 全部可选时间段
const allTimeSlots = [
'09:00-10:00', '10:00-11:00', '11:00-12:00',
'14:00-15:00', '15:00-16:00', '16:00-17:00', '17:00-18:00',
]
// 根据选中日期过滤已过去的时间段
const timeSlots = (() => {
if (!date) return allTimeSlots
const today = dayjs().format('YYYY-MM-DD')
if (date !== today) return allTimeSlots
// 今天:过滤掉已结束的时间段
const now = dayjs()
return allTimeSlots.filter(slot => {
const endTime = slot.split('-')[1] // 取结束时间如 "10:00"
const [h, m] = endTime.split(':').map(Number)
const slotEnd = dayjs().hour(h).minute(m).second(0)
return slotEnd.isAfter(now)
})
})()
// 服务类型
const serviceTypes = [
{ label: '穿线服务', value: '穿线服务', price: 30 },
{ label: '穿线+手胶', value: '穿线+手胶', price: 50 },
{ label: '穿线+毛巾胶', value: '穿线+毛巾胶', price: 60 },
{ label: '其他', value: '其他', price: 0 },
]
const selectedPrice = serviceTypes.find(s => s.value === serviceType)?.price || 0
// 切换日期时,清除已选时间(如果当前时间在新日期不可用)
const handleDateChange = (dateStr: string) => {
setDate(dateStr)
// 检查当前选中的时间在新的 timeSlots 中是否存在
const today = dayjs().format('YYYY-MM-DD')
if (dateStr === today) {
const now = dayjs()
const availableSlots = allTimeSlots.filter(slot => {
const endTime = slot.split('-')[1]
const [h, m] = endTime.split(':').map(Number)
const slotEnd = dayjs().hour(h).minute(m).second(0)
return slotEnd.isAfter(now)
})
if (time && !availableSlots.includes(time)) {
setTime('')
}
}
}
// 初始化
useEffect(() => {
if (!isLoggedIn) {
Taro.navigateTo({ url: '/passport/login' })
return
}
if (initStoreId) {
fetchStoreInfo(Number(initStoreId))
}
// 挂载回调,供门店列表页选中后回传
const pages = Taro.getCurrentPages()
const curPage = pages[pages.length - 1] as any
curPage.setSelectedStore = (store: ShopStore) => {
setStoreInfo(store)
}
}, [isLoggedIn])
const fetchStoreInfo = async (id: number) => {
try {
const data = await getShopStore(id)
setStoreInfo(data)
} catch (e) {
console.error('获取门店信息失败:', e)
}
}
// 跳转到门店选择列表
const handleSelectStore = () => {
Taro.navigateTo({ url: '/pages/store/list/index?selectMode=1' })
}
const formatDateDisplay = (dateStr: string) => {
const d = dayjs(dateStr)
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
return `${d.month() + 1}${d.date()}${weekDays[d.day()]}`
}
const validateForm = () => {
if (!storeInfo) {
Taro.showToast({ title: '请选择预约门店', icon: 'none' })
return false
}
if (!date) {
Taro.showToast({ title: '请选择预约日期', icon: 'none' })
return false
}
if (!time) {
Taro.showToast({ title: '请选择预约时间段', icon: 'none' })
return false
}
if (!serviceType) {
Taro.showToast({ title: '请选择服务类型', icon: 'none' })
return false
}
if (!contactName.trim()) {
Taro.showToast({ title: '请输入联系人姓名', icon: 'none' })
return false
}
if (!contactPhone.trim()) {
Taro.showToast({ title: '请输入手机号', icon: 'none' })
return false
}
if (!/^1[3-9]\d{9}$/.test(contactPhone)) {
Taro.showToast({ title: '手机号格式不正确', icon: 'none' })
return false
}
// 校验预约时间不能是过去
const endTime = time.split('-')[1]
const [h, m] = endTime.split(':').map(Number)
const bookingDateTime = dayjs(date).hour(h).minute(m).second(0)
if (bookingDateTime.isBefore(dayjs())) {
Taro.showToast({ title: '预约时间不能是过去的时间', icon: 'none' })
return false
}
return true
}
const handleSubmit = () => {
if (!validateForm()) return
const confirmContent = `门店:${storeInfo!.name}\n日期${formatDateDisplay(date)}\n时段${time}\n服务${serviceType}${selectedPrice > 0 ? `(¥${selectedPrice}` : ''}\n联系人${contactName}\n电话${contactPhone}`
Taro.showModal({
title: '确认预约',
content: confirmContent,
confirmText: '确认预约',
confirmColor: '#0e932e',
success: (res) => {
if (res.confirm) submitBooking()
},
})
}
const submitBooking = async () => {
try {
setSubmitting(true)
await createShopBooking({
storeId: storeInfo!.id || 0,
storeName: storeInfo!.name || '',
serviceId: 0,
serviceName: serviceType,
bookingDate: date,
bookingTime: time,
status: 'pending',
price: selectedPrice,
remark,
contactName,
contactPhone,
address: storeInfo!.address || '',
})
Taro.showToast({ title: '预约成功', icon: 'success' })
setTimeout(() => Taro.navigateBack(), 1500)
} catch (e: any) {
Taro.showToast({ title: e.message || '预约失败', icon: 'none' })
} finally {
setSubmitting(false)
}
}
return (
<View className='booking-page'>
<ScrollView scrollY className='booking-scroll'>
{/* 选择门店 */}
<View className='bg-white mx-3 mt-3 rounded-xl overflow-hidden'>
<View
className='flex items-center px-4 py-3'
onClick={handleSelectStore}
>
<Text className='text-sm font-medium text-gray-700 mr-3'></Text>
<View className='flex-1 flex items-center justify-between'>
{storeInfo ? (
<View className='flex-1'>
<Text className='text-sm text-gray-800 font-medium block'>{storeInfo.name}</Text>
{storeInfo.address && (
<Text className='text-xs text-gray-400 mt-0.5 block'>{storeInfo.address}</Text>
)}
{storeInfo.businessHours && (
<Text className='text-xs text-gray-400 mt-0.5 block'>🕐 {storeInfo.businessHours}</Text>
)}
</View>
) : (
<Text className='text-sm text-gray-400 flex-1'></Text>
)}
<Text className='text-gray-300 ml-2 text-base'></Text>
</View>
</View>
{/* 门店操作快捷按钮(已选门店时显示) */}
{storeInfo && (
<View className='flex border-t border-gray-50'>
{storeInfo.phone && (
<View
className='flex-1 py-2 flex items-center justify-center gap-1'
onClick={() => Taro.makePhoneCall({ phoneNumber: storeInfo.phone! })}
>
<Text className='text-xs text-blue-500'>📞 </Text>
</View>
)}
{storeInfo.lngAndLat && (
<View
className='flex-1 py-2 flex items-center justify-center gap-1 border-l border-gray-50'
onClick={() => {
const parts = storeInfo.lngAndLat!.split(',')
if (parts.length === 2) {
Taro.openLocation({
longitude: parseFloat(parts[0]),
latitude: parseFloat(parts[1]),
name: storeInfo.name || '',
address: storeInfo.address || '',
})
}
}}
>
<Text className='text-xs text-green-500'>🗺 </Text>
</View>
)}
<View
className='flex-1 py-2 flex items-center justify-center gap-1 border-l border-gray-50'
onClick={handleSelectStore}
>
<Text className='text-xs text-orange-500'>🔄 </Text>
</View>
</View>
)}
</View>
{/* 选择日期 */}
<View className='bg-white mx-3 mt-3 p-3 rounded-xl'>
<Text className='text-sm font-medium text-gray-700 mb-3 block'></Text>
<ScrollView scrollX className='whitespace-nowrap'>
<View className='flex gap-2'>
{dateRange.map(dateStr => (
<View
key={dateStr}
className={`inline-flex flex-col items-center px-3 py-2 rounded-lg min-w-20 ${
date === dateStr ? 'bg-green-500' : 'bg-gray-50'
}`}
onClick={() => handleDateChange(dateStr)}
>
<Text className={`text-xs ${date === dateStr ? 'text-white' : 'text-gray-600'}`}>
{formatDateDisplay(dateStr)}
</Text>
</View>
))}
</View>
</ScrollView>
</View>
{/* 选择时间 */}
<View className='bg-white mx-3 mt-3 p-3 rounded-xl'>
<Text className='text-sm font-medium text-gray-700 mb-3 block'></Text>
<View className='flex flex-wrap gap-2'>
{timeSlots.map(slot => (
<View
key={slot}
className={`px-4 py-2 rounded-lg ${
time === slot ? 'bg-green-500' : 'bg-gray-50'
}`}
onClick={() => setTime(slot)}
>
<Text className={`text-sm ${time === slot ? 'text-white' : 'text-gray-600'}`}>
{slot}
</Text>
</View>
))}
</View>
</View>
{/* 服务类型 */}
<View className='bg-white mx-3 mt-3 p-3 rounded-xl'>
<Text className='text-sm font-medium text-gray-700 mb-3 block'></Text>
<View className='flex flex-wrap gap-2'>
{serviceTypes.map(service => (
<View
key={service.value}
className={`px-3 py-2 rounded-full border ${
serviceType === service.value
? 'bg-green-50 border-green-500'
: 'bg-gray-50 border-gray-200'
}`}
onClick={() => setServiceType(service.value)}
>
<Text className={`text-sm ${serviceType === service.value ? 'text-green-600' : 'text-gray-600'}`}>
{service.label}
{service.price > 0 ? ` ¥${service.price}` : ''}
</Text>
</View>
))}
</View>
</View>
{/* 联系人信息 */}
<View className='bg-white mx-3 mt-3 p-3 rounded-xl'>
<Text className='text-sm font-medium text-gray-700 mb-3 block'></Text>
<View className='mb-3'>
<Text className='text-xs text-gray-500 mb-1 block'></Text>
<Input
value={contactName}
onInput={(e) => setContactName(e.detail.value)}
placeholder='请输入姓名'
className='bg-gray-50 rounded-lg p-3 text-sm'
/>
</View>
<View>
<Text className='text-xs text-gray-500 mb-1 block'></Text>
<Input
type='number'
value={contactPhone}
onInput={(e) => setContactPhone(e.detail.value)}
placeholder='请输入手机号'
className='bg-gray-50 rounded-lg p-3 text-sm'
maxlength={11}
/>
</View>
</View>
{/* 备注 */}
<View className='bg-white mx-3 mt-3 p-3 rounded-xl mb-24'>
<Text className='text-sm font-medium text-gray-700 mb-3 block'></Text>
<Input
value={remark}
onInput={(e) => setRemark(e.detail.value)}
placeholder='如有特殊需求请注明(球拍型号、穿线磅数等)'
className='bg-gray-50 rounded-lg p-3 text-sm'
style={{ minHeight: '80px' }}
/>
</View>
</ScrollView>
<BottomButton
text='确认预约'
onClick={handleSubmit}
loading={submitting}
disabled={submitting}
subtitle={serviceType && selectedPrice > 0 ? `费用:¥${selectedPrice}(到店支付)` : undefined}
/>
</View>
)
}
export default BookingPage