feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构 - 新增地址编辑页面,支持地址智能识别和定位选点功能 - 地址编辑支持省市区选择及默认地址设置 - 新增地址列表页面,支持地址展示、删除、编辑和选择功能 - 实现售后申请页面,支持选择售后类型和退款原因 - 售后申请支持商品选择、退款金额计算和凭证上传 - 新增售后详情页面,支持售后状态展示及申请取消 - 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
3
src_bak/pages/store/booking/index.config.ts
Normal file
3
src_bak/pages/store/booking/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '穿线预约',
|
||||
}
|
||||
69
src_bak/pages/store/booking/index.scss
Normal file
69
src_bak/pages/store/booking/index.scss
Normal file
@@ -0,0 +1,69 @@
|
||||
/* 穿线预约页面样式 */
|
||||
|
||||
/* 底部悬浮按钮 */
|
||||
.bottom-button-container {
|
||||
z-index: 999;
|
||||
background-color: #ffffff;
|
||||
padding: 12px 16px;
|
||||
padding-bottom: 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bottom-btn-subtitle {
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle-text {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.subtitle-price {
|
||||
color: #ff6600;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bottom-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background-color: #0e932e;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
border-radius: 44rpx;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 88rpx;
|
||||
}
|
||||
|
||||
.bottom-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.bottom-btn[disabled] {
|
||||
background-color: #cccccc;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.bottom-btn[loading] {
|
||||
background-color: #0e932e;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 页面布局 */
|
||||
.booking-page {
|
||||
min-height: 100%;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.booking-scroll {
|
||||
flex: 1;
|
||||
}
|
||||
389
src_bak/pages/store/booking/index.tsx
Normal file
389
src_bak/pages/store/booking/index.tsx
Normal file
@@ -0,0 +1,389 @@
|
||||
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
|
||||
Reference in New Issue
Block a user