commit 9df90c11768aeec4aaaa702805d341852e2cb806 Author: 赵忠林 <170083662@qq.com> Date: Thu Jul 16 01:18:42 2026 +0800 feat(address): 新增收货地址与售后申请功能 - 新增地址编辑页面,支持地址智能识别、地图选点、地区选择和默认地址设置 - 实现地址列表页面,支持地址查看、删除、设为默认及选择返回结算页 - 新增售后申请页面,支持退款类型选择、商品选择、原因填写、图片上传和提交审核 - 修复 passport 分包配置,移除不存在分包并补充缺失声明,避免 Taro 编译报错 - 新增地址类型定义,增强前端地址数据结构类型安全 - 优化页面交互体验,完善表单校验及错误提示逻辑 - 统一代码格式与命名规范,保持代码风格一致性 diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..b56158b --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,556 @@ + + + + + + + + + + { + "associatedIndex": 7 +} + + + + + + + + + + + + + + + + + + + + + <% } %> + + +
+ + diff --git a/src/pages/activity/detail/index.config.ts b/src/pages/activity/detail/index.config.ts new file mode 100644 index 0000000..c739d6b --- /dev/null +++ b/src/pages/activity/detail/index.config.ts @@ -0,0 +1,5 @@ +export default { + navigationBarTitleText: '活动详情', + enableShareAppMessage: true, + enableShareTimeline: true, +} diff --git a/src/pages/activity/detail/index.tsx b/src/pages/activity/detail/index.tsx new file mode 100644 index 0000000..e567775 --- /dev/null +++ b/src/pages/activity/detail/index.tsx @@ -0,0 +1,276 @@ +import React, { useState, useEffect, useRef } 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' +import { getCompressedImageUrl } from '@/utils/image' +import { useShare } from '@/hooks/useShare' +import SharePoster, { SharePosterHandle } from '@/components/SharePoster' + +definePageConfig({ + navigationBarTitleText: '活动详情', + enableShareAppMessage: true, + enableShareTimeline: true, +}) + +const ActivityDetailPage: React.FC = () => { + const { id } = Taro.getCurrentInstance().router?.params || {} + const [activity, setActivity] = useState(null) + const [loading, setLoading] = useState(true) + const [signedUp, setSignedUp] = useState(false) + const [submitting, setSubmitting] = useState(false) + + // 分享 / 朋友圈 + const posterRef = useRef(null) + const [posterPath, setPosterPath] = useState('') + useShare({ + title: activity?.name || '精彩活动推荐', + path: `/pages/activity/detail?id=${id}`, + query: `id=${id}`, + imageUrl: posterPath || activity?.image || undefined, + }) + + // 数据加载完成后异步生成分享海报(带小程序码) + useEffect(() => { + if (activity) { + posterRef.current + ?.generate({ + cover: activity.image, + title: activity.name || '精彩活动', + page: 'pages/activity/detail', + }) + .then(setPosterPath) + .catch(() => {}) + } + }, [activity]) + + 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 = { + 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 ( + + 加载中... + + ) + } + + if (!activity) { + return ( + + 活动不存在 + + ) + } + + const statusInfo = getStatusLabel(activity.status) + + return ( + + + {/* 活动头图 */} + {activity.image ? ( + + ) : ( + + 🎉 + + )} + + {/* 活动信息 */} + + + {activity.name} + + {statusInfo.label} + + + + + + 📅 + {formatTime(activity.startTime)} + + + 👥 + {activity.participants}人参与 + + {activity.maxParticipants && ( + + 🔢 + 限{activity.maxParticipants}人 + + )} + + + + {activity.typeName || activity.type} + {activity.description.split('\n')[0]} + + + + {/* 活动详情 */} + + 活动详情 + + {activity.description} + + + + {/* 活动规则 */} + {activity.rules && ( + + 活动规则 + + {activity.rules} + + + )} + + + + + {/* 底部操作栏 */} + + + 分享 + + + {activity.status === 'ongoing' && !signedUp && ( + + {submitting ? '处理中...' : '立即报名'} + + )} + + {activity.status === 'ongoing' && signedUp && ( + + {submitting ? '处理中...' : '取消报名'} + + )} + + {activity.status === 'upcoming' && ( + + 即将开始 + + )} + + {(activity.status === 'ended' || activity.status === 'cancelled') && ( + + 已结束 + + )} + + {/* 分享海报画布(离屏,用于生成带小程序码的海报图) */} + + + ) +} + +export default ActivityDetailPage diff --git a/src/pages/activity/list/index.config.ts b/src/pages/activity/list/index.config.ts new file mode 100644 index 0000000..6b10e16 --- /dev/null +++ b/src/pages/activity/list/index.config.ts @@ -0,0 +1,3 @@ +export default { + navigationBarTitleText: '活动列表', +} diff --git a/src/pages/activity/list/index.tsx b/src/pages/activity/list/index.tsx new file mode 100644 index 0000000..c6e4efe --- /dev/null +++ b/src/pages/activity/list/index.tsx @@ -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([]) + 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 = { + '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 = { + '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 ( + + {/* Tab 栏 */} + + {['全部', '进行中', '未开始', '已结束'].map((tab, index) => ( + setActiveTab(index)} + > + {tab} + {activeTab === index && ( + + )} + + ))} + + + {/* 活动列表 */} + + + {activities.length === 0 && !loading ? ( + + ) : ( + activities.map(activity => ( + (() => { + const statusInfo = getStatusLabel(activity.status) + return ( + Taro.navigateTo({ url: `/pages/activity/detail/index?id=${activity.id}` })}> + {/* 活动图片 */} + + 🎉 + + + {/* 活动信息 */} + + + {activity.name} + + {statusInfo.label} + + + {activity.description} + + + + 📅 + {formatTime(activity.startTime)} + + + 👥 + {activity.participants}人参与 + + + + + {/* 活动类型标签 */} + + + {getTypeLabel(activity.type)} + + + {activity.status === 'ongoing' && ( + handleSignUp(activity.id)} + > + 立即报名 + + )} + {activity.status === 'upcoming' && ( + Taro.navigateTo({ url: `/pages/activity/detail/index?id=${activity.id}` })} + > + 查看详情 + + )} + + + ) + })() + )) + )} + + + + + + ) +} + +export default ActivityListPage diff --git a/src/pages/after-sale/apply/index.config.ts b/src/pages/after-sale/apply/index.config.ts new file mode 100644 index 0000000..716b532 --- /dev/null +++ b/src/pages/after-sale/apply/index.config.ts @@ -0,0 +1,3 @@ +export default { + navigationBarTitleText: '申请售后', +} diff --git a/src/pages/after-sale/apply/index.tsx b/src/pages/after-sale/apply/index.tsx new file mode 100644 index 0000000..593e9f4 --- /dev/null +++ b/src/pages/after-sale/apply/index.tsx @@ -0,0 +1,251 @@ +import React, { useState } from 'react' +import { View, Text, Textarea, ScrollView, Radio, RadioGroup, Checkbox, Input } from '@tarojs/components' +import Taro from '@tarojs/taro' +import { applyAfterSale } from '@/api/shop/shopAfterSale' +import type { AfterSaleType } from '@/api/shop/shopAfterSale' + +definePageConfig({ + navigationBarTitleText: '申请售后', +}) + +const AfterSaleApplyPage: React.FC = () => { + const params = Taro.getCurrentInstance().router?.params || {} + const orderId = params.orderId || '' + const [orderGoods, setOrderGoods] = useState>([]) + const [saleType, setSaleType] = useState(1) // 1:退款, 2:退货退款, 3:换货 + const [submitting, setSubmitting] = useState(false) + const [reason, setReason] = useState('') + const [amount, setAmount] = useState('') + const [description, setDescription] = useState('') + const [images, setImages] = useState([]) + + // 退款原因选项 + const refundReasons = ['商品质量问题', '商品与描述不符', '商品破损/缺陷', '物流问题', '其他'] + const returnReasons = ['商品质量问题', '商品与描述不符', '商品破损/缺陷', '尺码/颜色不合适', '不喜欢/不想要', '其他'] + const exchangeReasons = ['商品质量问题', '商品与描述不符', '商品破损/缺陷', '尺码/颜色不合适', '其他'] + + // 获取当前原因列表 + const getCurrentReasons = () => { + if (saleType === 1) return refundReasons + if (saleType === 2) return returnReasons + return exchangeReasons + } + + // 处理商品选择 + const handleGoodsCheck = (id: number) => { + setOrderGoods(prev => + prev.map(g => g.id === id ? { ...g, checked: !g.checked } : g) + ) + } + + // 选择图片 + const handleChooseImage = () => { + if (images.length >= 6) { + Taro.showToast({ title: '最多上传6张图片', icon: 'none' }) + return + } + Taro.chooseImage({ + count: 6 - images.length, + success: (res) => { + setImages(prev => [...prev, ...res.tempFilePaths]) + } + }) + } + + // 删除图片 + const handleDeleteImage = (index: number) => { + setImages(prev => prev.filter((_, i) => i !== index)) + } + + // 提交申请 + const handleSubmit = () => { + const checkedGoods = orderGoods.filter(g => g.checked) + if (!reason) { + Taro.showToast({ title: '请选择原因', icon: 'none' }) + return + } + if (saleType === 1 && !amount) { + Taro.showToast({ title: '请输入退款金额', icon: 'none' }) + return + } + + const typeMap: Record = { + 1: 'refund', + 2: 'return', + 3: 'exchange', + } + + Taro.showModal({ + title: '确认提交', + content: '确定提交售后申请吗?', + confirmColor: '#0e932e', + success: async (res) => { + if (res.confirm) { + setSubmitting(true) + Taro.showLoading({ title: '提交中...' }) + try { + await applyAfterSale({ + orderId, + type: typeMap[saleType], + reason, + description, + amount: saleType === 1 ? Number(amount) : undefined, + evidenceImages: images, + goodsItems: checkedGoods.map(g => ({ goodsId: String(g.id), quantity: g.num })), + }) + Taro.hideLoading() + Taro.showToast({ title: '提交成功', icon: 'success' }) + setTimeout(() => Taro.navigateBack(), 1500) + } catch (err: any) { + Taro.hideLoading() + Taro.showToast({ title: err.message || '提交失败', icon: 'none' }) + } finally { + setSubmitting(false) + } + } + } + }) + } + + return ( + + + {/* 订单商品 */} + + 选择商品 + {orderGoods.map(goods => ( + handleGoodsCheck(goods.id)} + > + + + 🛍️ + + + {goods.name} + ¥{goods.price} × {goods.num} + + + ))} + + + {/* 售后类型 */} + + 售后类型 + setSaleType(parseInt(e.detail.value))}> + + + + 仅退款 + + + + 退货退款 + + + + 换货 + + + + + + {/* 退款金额 */} + {saleType === 1 && ( + + 退款金额 + + ¥ + setAmount(e.detail.value)} + placeholder='请输入退款金额' + className='flex-1' + /> + + 最多可退 ¥134.00 + + )} + + {/* 售后原因 */} + + 售后原因 + + {getCurrentReasons().map((r, index) => ( + setReason(r)} + > + + {r} + + + ))} + + + + {/* 问题描述 */} + + 问题描述 +