feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构 - 新增地址编辑页面,支持地址智能识别和定位选点功能 - 地址编辑支持省市区选择及默认地址设置 - 新增地址列表页面,支持地址展示、删除、编辑和选择功能 - 实现售后申请页面,支持选择售后类型和退款原因 - 售后申请支持商品选择、退款金额计算和凭证上传 - 新增售后详情页面,支持售后状态展示及申请取消 - 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
3
src_bak/pages/activity/detail/index.config.ts
Normal file
3
src_bak/pages/activity/detail/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '活动详情',
|
||||
}
|
||||
245
src_bak/pages/activity/detail/index.tsx
Normal file
245
src_bak/pages/activity/detail/index.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { getShopActivity, signUpActivity, cancelSignUpActivity, getSignUpStatus } from '@/api/shop/shopActivity'
|
||||
import type { ShopActivity, ActivityStatus } from '@/api/shop/shopActivity/model'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '活动详情',
|
||||
})
|
||||
|
||||
const ActivityDetailPage: React.FC = () => {
|
||||
const { id } = Taro.getCurrentInstance().router?.params || {}
|
||||
const [activity, setActivity] = useState<ShopActivity | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [signedUp, setSignedUp] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchDetail(Number(id))
|
||||
fetchSignUpStatus(Number(id))
|
||||
}
|
||||
}, [id])
|
||||
|
||||
const fetchDetail = async (activityId: number) => {
|
||||
try {
|
||||
const data = await getShopActivity(activityId)
|
||||
setActivity(data)
|
||||
} catch (err: any) {
|
||||
Taro.showToast({ title: err.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSignUpStatus = async (activityId: number) => {
|
||||
try {
|
||||
const res = await getSignUpStatus(activityId)
|
||||
setSignedUp(res.signedUp)
|
||||
} catch {
|
||||
// 未登录或接口不可用时忽略
|
||||
}
|
||||
}
|
||||
|
||||
// 报名活动
|
||||
const handleSignUp = () => {
|
||||
if (!activity) return
|
||||
Taro.showModal({
|
||||
title: '确认报名',
|
||||
content: '确定报名参加该活动吗?',
|
||||
confirmColor: '#0e932e',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await signUpActivity({ activityId: activity.id, userId: 0 })
|
||||
setSignedUp(true)
|
||||
setActivity(prev => prev ? { ...prev, participants: prev.participants + 1 } : prev)
|
||||
Taro.showToast({ title: '报名成功', icon: 'success' })
|
||||
} catch (err: any) {
|
||||
Taro.showToast({ title: err.message || '报名失败', icon: 'none' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 取消报名
|
||||
const handleCancelSignUp = () => {
|
||||
if (!activity) return
|
||||
Taro.showModal({
|
||||
title: '取消报名',
|
||||
content: '确定取消报名吗?',
|
||||
confirmColor: '#ef4444',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await cancelSignUpActivity(activity.id)
|
||||
setSignedUp(false)
|
||||
setActivity(prev => prev ? { ...prev, participants: Math.max(0, prev.participants - 1) } : prev)
|
||||
Taro.showToast({ title: '已取消报名', icon: 'success' })
|
||||
} catch (err: any) {
|
||||
Taro.showToast({ title: err.message || '取消失败', icon: 'none' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 分享活动
|
||||
const handleShare = () => {
|
||||
Taro.showToast({ title: '分享功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
// 获取状态标签
|
||||
const getStatusLabel = (status: ActivityStatus) => {
|
||||
const map: Record<ActivityStatus, { label: string; color: string }> = {
|
||||
upcoming: { label: '未开始', color: 'text-blue-500' },
|
||||
ongoing: { label: '进行中', color: 'text-green-500' },
|
||||
ended: { label: '已结束', color: 'text-gray-400' },
|
||||
cancelled: { label: '已取消', color: 'text-gray-400' },
|
||||
}
|
||||
return map[status] || { label: '未知', color: 'text-gray-400' }
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr: string) => {
|
||||
const date = new Date(timeStr)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
|
||||
<Text className='text-sm text-gray-400'>加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!activity) {
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex items-center justify-center'>
|
||||
<Text className='text-sm text-gray-400'>活动不存在</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const statusInfo = getStatusLabel(activity.status)
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50 flex flex-col'>
|
||||
<ScrollView scrollY className='flex-1'>
|
||||
{/* 活动头图 */}
|
||||
{activity.image ? (
|
||||
<Image src={activity.image} className='w-full' style={{ height: '192px' }} mode='aspectFill' />
|
||||
) : (
|
||||
<View className='w-full flex items-center justify-center' style={{ height: '192px', background: 'linear-gradient(to right, #fb923c, #ef4444)' }}>
|
||||
<Text className='text-6xl'>🎉</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 活动信息 */}
|
||||
<View className='bg-white p-4 mb-3'>
|
||||
<View className='flex items-center gap-2 mb-2'>
|
||||
<Text className='text-xl font-bold text-gray-800 flex-1'>{activity.name}</Text>
|
||||
<View className={`px-2 py-1 rounded ${activity.status === 'ongoing' ? 'bg-green-50' : activity.status === 'upcoming' ? 'bg-blue-50' : 'bg-gray-50'}`}>
|
||||
<Text className={`text-xs ${statusInfo.color}`}>{statusInfo.label}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='flex items-center gap-3 text-xs text-gray-500 mb-3'>
|
||||
<View className='flex items-center gap-1'>
|
||||
<Text>📅</Text>
|
||||
<Text>{formatTime(activity.startTime)}</Text>
|
||||
</View>
|
||||
<View className='flex items-center gap-1'>
|
||||
<Text>👥</Text>
|
||||
<Text>{activity.participants}人参与</Text>
|
||||
</View>
|
||||
{activity.maxParticipants && (
|
||||
<View className='flex items-center gap-1'>
|
||||
<Text>🔢</Text>
|
||||
<Text>限{activity.maxParticipants}人</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className='bg-orange-50 rounded-lg p-3'>
|
||||
<Text className='text-sm text-orange-500 font-medium'>{activity.typeName || activity.type}</Text>
|
||||
<Text className='text-xs text-gray-600 mt-1 block leading-5'>{activity.description.split('\n')[0]}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 活动详情 */}
|
||||
<View className='bg-white p-4 mb-3'>
|
||||
<Text className='text-base font-medium text-gray-800 mb-3 block'>活动详情</Text>
|
||||
<Text className='text-sm text-gray-700 leading-7 block whitespace-pre-wrap'>
|
||||
{activity.description}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 活动规则 */}
|
||||
{activity.rules && (
|
||||
<View className='bg-white p-4 mb-3'>
|
||||
<Text className='text-base font-medium text-gray-800 mb-3 block'>活动规则</Text>
|
||||
<Text className='text-sm text-gray-700 leading-7 block whitespace-pre-wrap'>
|
||||
{activity.rules}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className='h-20' />
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部操作栏 */}
|
||||
<View className='bg-white p-3 shadow-lg flex gap-2' style={{ paddingBottom: '20px' }}>
|
||||
<View
|
||||
className='flex-1 bg-gray-100 text-center py-3 rounded-full'
|
||||
onClick={handleShare}
|
||||
>
|
||||
<Text className='text-sm text-gray-700'>分享</Text>
|
||||
</View>
|
||||
|
||||
{activity.status === 'ongoing' && !signedUp && (
|
||||
<View
|
||||
className='flex-1 bg-orange-500 text-center py-3 rounded-full'
|
||||
onClick={submitting ? undefined : handleSignUp}
|
||||
style={{ opacity: submitting ? 0.6 : 1 }}
|
||||
>
|
||||
<Text className='text-sm text-white font-bold'>{submitting ? '处理中...' : '立即报名'}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{activity.status === 'ongoing' && signedUp && (
|
||||
<View
|
||||
className='flex-1 bg-gray-400 text-center py-3 rounded-full'
|
||||
onClick={submitting ? undefined : handleCancelSignUp}
|
||||
style={{ opacity: submitting ? 0.6 : 1 }}
|
||||
>
|
||||
<Text className='text-sm text-white font-bold'>{submitting ? '处理中...' : '取消报名'}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{activity.status === 'upcoming' && (
|
||||
<View className='flex-1 bg-blue-500 text-center py-3 rounded-full'>
|
||||
<Text className='text-sm text-white font-bold'>即将开始</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{(activity.status === 'ended' || activity.status === 'cancelled') && (
|
||||
<View className='flex-1 bg-gray-300 text-center py-3 rounded-full'>
|
||||
<Text className='text-sm text-white font-bold'>已结束</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActivityDetailPage
|
||||
3
src_bak/pages/activity/list/index.config.ts
Normal file
3
src_bak/pages/activity/list/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '活动列表',
|
||||
}
|
||||
220
src_bak/pages/activity/list/index.tsx
Normal file
220
src_bak/pages/activity/list/index.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { pageShopActivity, signUpActivity } from '@/api/shop/shopActivity'
|
||||
import type { ShopActivity, ActivityStatus, ActivityType } from '@/api/shop/shopActivity'
|
||||
import EmptyState from '@/components/common/EmptyState'
|
||||
import LoadMore from '@/components/common/LoadMore'
|
||||
|
||||
definePageConfig({
|
||||
navigationBarTitleText: '活动列表',
|
||||
})
|
||||
|
||||
const ActivityListPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
const [activities, setActivities] = useState<ShopActivity[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
setActivities([])
|
||||
setPage(1)
|
||||
setFinished(false)
|
||||
loadList(1)
|
||||
}, [activeTab])
|
||||
|
||||
const loadList = async (p: number) => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
// 根据 tab 筛选状态
|
||||
let status: ActivityStatus | undefined
|
||||
if (activeTab === 1) status = 'ongoing'
|
||||
else if (activeTab === 2) status = 'upcoming'
|
||||
else if (activeTab === 3) status = 'ended'
|
||||
|
||||
const res = await pageShopActivity({
|
||||
page: p,
|
||||
limit: 10,
|
||||
status,
|
||||
})
|
||||
|
||||
if (res?.list) {
|
||||
if (p === 1) {
|
||||
setActivities(res.list)
|
||||
} else {
|
||||
setActivities(prev => [...prev, ...res.list])
|
||||
}
|
||||
setFinished(res.list.length < 10)
|
||||
setPage(p)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载活动列表失败', err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const handleLoadMore = () => {
|
||||
if (!finished && !loading) {
|
||||
loadList(page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 报名活动
|
||||
const handleSignUp = async (id: number) => {
|
||||
Taro.showModal({
|
||||
title: '确认报名',
|
||||
content: '确定报名参加该活动吗?',
|
||||
confirmColor: '#0e932e',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await signUpActivity({ activityId: id, userId: 0 })
|
||||
Taro.showToast({ title: '报名成功', icon: 'success' })
|
||||
} catch (err) {
|
||||
console.error('报名失败', err)
|
||||
Taro.showToast({ title: '报名失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取状态标签
|
||||
const getStatusLabel = (status: ActivityStatus) => {
|
||||
const map: Record<ActivityStatus, { label: string; color: string }> = {
|
||||
'upcoming': { label: '未开始', color: 'text-blue-500' },
|
||||
'ongoing': { label: '进行中', color: 'text-green-500' },
|
||||
'ended': { label: '已结束', color: 'text-gray-400' },
|
||||
'cancelled': { label: '已取消', color: 'text-gray-400' },
|
||||
}
|
||||
return map[status] || { label: '未知', color: 'text-gray-400' }
|
||||
}
|
||||
|
||||
// 获取活动类型名称
|
||||
const getTypeLabel = (type: ActivityType) => {
|
||||
const map: Record<ActivityType, string> = {
|
||||
'discount': '限时折扣',
|
||||
'full_reduction': '满减活动',
|
||||
'seckill': '秒杀活动',
|
||||
'group_buy': '团购活动',
|
||||
'flash_sale': '闪购活动',
|
||||
}
|
||||
return map[type] || '其他'
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr: string) => {
|
||||
const date = new Date(timeStr)
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const hour = date.getHours().toString().padStart(2, '0')
|
||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='min-h-screen bg-gray-50'>
|
||||
{/* Tab 栏 */}
|
||||
<View className='bg-white flex'>
|
||||
{['全部', '进行中', '未开始', '已结束'].map((tab, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className={`flex-1 text-center py-3 relative ${
|
||||
activeTab === index ? 'text-orange-500 font-medium' : 'text-gray-600'
|
||||
}`}
|
||||
onClick={() => setActiveTab(index)}
|
||||
>
|
||||
<Text className='text-sm'>{tab}</Text>
|
||||
{activeTab === index && (
|
||||
<View className='absolute bottom-0 w-8 h-px bg-orange-500 rounded' style={{ left: '50%', transform: 'translateX(-50%)' }} />
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 活动列表 */}
|
||||
<ScrollView
|
||||
scrollY
|
||||
className='flex-1'
|
||||
onScrollToLower={handleLoadMore}
|
||||
lowerThreshold={100}
|
||||
>
|
||||
<View className='p-3'>
|
||||
{activities.length === 0 && !loading ? (
|
||||
<EmptyState text='暂无活动' />
|
||||
) : (
|
||||
activities.map(activity => (
|
||||
(() => {
|
||||
const statusInfo = getStatusLabel(activity.status)
|
||||
return (
|
||||
<View key={activity.id} className='bg-white rounded-xl p-4 mb-3 shadow-sm' onClick={() => Taro.navigateTo({ url: `/pages/activity/detail/index?id=${activity.id}` })}>
|
||||
{/* 活动图片 */}
|
||||
<View className='w-full rounded-lg flex items-center justify-center mb-3' style={{ height: '160px', background: 'linear-gradient(to right, #fb923c, #ef4444)' }}>
|
||||
<Text className='text-5xl'>🎉</Text>
|
||||
</View>
|
||||
|
||||
{/* 活动信息 */}
|
||||
<View className='mb-2'>
|
||||
<View className='flex items-center gap-2 mb-1'>
|
||||
<Text className='text-base font-bold text-gray-800 flex-1'>{activity.name}</Text>
|
||||
<View className={`px-2 py-1 rounded ${activity.status === 'ongoing' ? 'bg-green-50' : activity.status === 'upcoming' ? 'bg-blue-50' : 'bg-gray-50'}`}>
|
||||
<Text className={`text-xs ${statusInfo.color}`}>{statusInfo.label}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text className='text-sm text-gray-600 mb-2 block'>{activity.description}</Text>
|
||||
|
||||
<View className='flex items-center gap-3 text-xs text-gray-400'>
|
||||
<View className='flex items-center gap-1'>
|
||||
<Text>📅</Text>
|
||||
<Text>{formatTime(activity.startTime)}</Text>
|
||||
</View>
|
||||
<View className='flex items-center gap-1'>
|
||||
<Text>👥</Text>
|
||||
<Text>{activity.participants}人参与</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 活动类型标签 */}
|
||||
<View className='flex items-center justify-between'>
|
||||
<View className='bg-orange-100 px-2 py-1 rounded'>
|
||||
<Text className='text-xs text-orange-500'>{getTypeLabel(activity.type)}</Text>
|
||||
</View>
|
||||
|
||||
{activity.status === 'ongoing' && (
|
||||
<View
|
||||
className='bg-orange-500 px-4 py-1 rounded-full'
|
||||
onClick={() => handleSignUp(activity.id)}
|
||||
>
|
||||
<Text className='text-xs text-white'>立即报名</Text>
|
||||
</View>
|
||||
)}
|
||||
{activity.status === 'upcoming' && (
|
||||
<View
|
||||
className='bg-blue-500 px-4 py-1 rounded-full'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/activity/detail/index?id=${activity.id}` })}
|
||||
>
|
||||
<Text className='text-xs text-white'>查看详情</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})()
|
||||
))
|
||||
)}
|
||||
|
||||
<LoadMore loading={loading} finished={finished} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActivityListPage
|
||||
Reference in New Issue
Block a user