feat(registration): 优化经销商注册流程并增加地址定位功能

- 修改导航栏标题从“邀请注册”为“注册成为会员”
- 修复重复提交问题并移除不必要的submitting状态
- 增加昵称和头像的必填验证提示
- 添加用户角色缺失时的默认角色写入机制
- 集成地图选点功能,支持经纬度获取和地址解析
- 实现微信地址导入功能,自动填充基本信息
- 增加定位权限检查和错误处理机制
- 添加.gitignore规则忽略备份文件夹src__bak
- 移除已废弃的银行卡和客户管理页面代码
- 优化表单验证规则和错误提示信息
- 实现经销商注册成功后自动跳转到“我的”页面
- 添加用户信息缓存刷新机制确保角色信息同步
```
This commit is contained in:
2026-03-01 12:35:41 +08:00
parent 945351be91
commit eee4644d06
296 changed files with 28845 additions and 6664 deletions

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '查看进度'
})

View File

@@ -0,0 +1,292 @@
.progress-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 80px;
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 50vh;
.loading-text {
margin-top: 16px;
color: #666;
}
}
.after-sale-header {
background: white;
padding: 20px;
margin-bottom: 12px;
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.type-status {
display: flex;
align-items: center;
gap: 12px;
.type-text {
font-weight: 600;
color: #333;
}
.status-tag {
padding: 2px 8px;
border-radius: 12px;
}
}
}
.header-info {
.order-no,
.apply-time,
.amount {
display: block;
color: #666;
margin-bottom: 4px;
&:last-child {
margin-bottom: 0;
}
}
.amount {
color: #ff6b35;
font-weight: 500;
}
}
}
.progress-timeline {
background: white;
margin-bottom: 12px;
.timeline-header {
padding: 16px 20px 0;
border-bottom: 1px solid #f0f0f0;
.timeline-title {
font-weight: 500;
color: #333;
}
}
.timeline-list {
padding: 0 20px;
.timeline-item {
position: relative;
display: flex;
padding: 16px 0;
border-left: 2px solid #e8e8e8;
margin-left: 8px;
&.current {
border-left-color: #1890ff;
.timeline-dot {
background: #1890ff;
border-color: #1890ff;
}
.timeline-status {
color: #1890ff;
font-weight: 500;
}
}
&:last-child {
border-left-color: transparent;
}
.timeline-dot {
position: absolute;
left: -6px;
top: 20px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #e8e8e8;
border: 2px solid #e8e8e8;
}
.timeline-content {
flex: 1;
margin-left: 20px;
.timeline-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
.timeline-status {
font-weight: 500;
color: #333;
}
.timeline-time {
color: #999;
}
}
.timeline-description {
display: block;
color: #666;
line-height: 1.4;
margin-bottom: 4px;
}
.timeline-operator {
display: block;
color: #999;
margin-bottom: 2px;
}
.timeline-remark {
display: block;
color: #1890ff;
background: #f0f8ff;
padding: 4px 8px;
border-radius: 4px;
margin-top: 4px;
}
}
}
}
}
.evidence-section {
background: white;
margin-bottom: 12px;
padding: 16px 20px;
.section-title {
font-weight: 500;
color: #333;
margin-bottom: 12px;
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
.image-item {
width: 80px;
height: 80px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e8e8e8;
.evidence-image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
}
.progress-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 12px 16px;
border-top: 1px solid #f0f0f0;
z-index: 100;
.footer-buttons {
display: flex;
gap: 12px;
.nut-button {
flex: 1;
}
}
}
}
/* NutUI 组件样式覆盖 */
.progress-page {
.nut-cell-group {
margin-bottom: 12px;
.nut-cell-group__title {
padding: 12px 20px 6px;
font-weight: 500;
color: #333;
}
.nut-cell {
padding: 12px 20px;
.nut-cell__title {
color: #333;
}
.nut-cell__value {
color: #666;
text-align: right;
word-break: break-all;
line-height: 1.4;
}
}
}
.nut-tag {
border: none;
color: white;
font-weight: 500;
}
}
/* 适配不同屏幕尺寸 */
@media (max-width: 375px) {
.progress-page {
.after-sale-header {
padding: 16px;
.header-top {
.type-status {
gap: 8px;
.type-text {
}
}
}
}
.progress-timeline {
.timeline-list {
padding: 0 16px;
.timeline-item {
.timeline-content {
margin-left: 16px;
}
}
}
}
.evidence-section {
padding: 12px 16px;
.image-list {
.image-item {
width: 70px;
height: 70px;
}
}
}
}
}

View File

@@ -0,0 +1,388 @@
import React, { useState, useEffect } from 'react'
import Taro, { useRouter } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import {
Cell,
CellGroup,
Loading,
Empty,
Button,
Steps,
Step,
Tag,
Divider
} from '@nutui/nutui-react-taro'
import './index.scss'
// 售后类型
type AfterSaleType = 'refund' | 'return' | 'exchange' | 'repair'
// 售后状态
type AfterSaleStatus =
| 'pending' // 待审核
| 'approved' // 已同意
| 'rejected' // 已拒绝
| 'processing' // 处理中
| 'completed' // 已完成
| 'cancelled' // 已取消
// 售后进度记录
interface ProgressRecord {
id: string
time: string
status: string
description: string
operator?: string
remark?: string
images?: string[]
}
// 售后详情
interface AfterSaleDetail {
id: string
orderId: string
orderNo: string
type: AfterSaleType
status: AfterSaleStatus
reason: string
description: string
amount: number
applyTime: string
processTime?: string
completeTime?: string
rejectReason?: string
contactPhone?: string
evidenceImages: string[]
progressRecords: ProgressRecord[]
}
// 售后类型映射
const AFTER_SALE_TYPE_MAP = {
'refund': '退款',
'return': '退货',
'exchange': '换货',
'repair': '维修'
}
// 售后状态映射
const AFTER_SALE_STATUS_MAP = {
'pending': '待审核',
'approved': '已同意',
'rejected': '已拒绝',
'processing': '处理中',
'completed': '已完成',
'cancelled': '已取消'
}
// 状态颜色映射
const STATUS_COLOR_MAP = {
'pending': '#faad14',
'approved': '#52c41a',
'rejected': '#ff4d4f',
'processing': '#1890ff',
'completed': '#52c41a',
'cancelled': '#999'
}
const AfterSaleProgressPage: React.FC = () => {
const router = useRouter()
const { orderId, orderNo, type = 'refund' } = router.params
const [loading, setLoading] = useState(true)
const [afterSaleDetail, setAfterSaleDetail] = useState<AfterSaleDetail | null>(null)
const [error, setError] = useState<string>('')
useEffect(() => {
if (orderId) {
loadAfterSaleDetail()
}
}, [orderId])
// 加载售后详情
const loadAfterSaleDetail = async () => {
try {
setLoading(true)
setError('')
// 模拟API调用 - 实际项目中替换为真实API
const mockAfterSaleDetail: AfterSaleDetail = {
id: 'AS' + Date.now(),
orderId: orderId || '',
orderNo: orderNo || '',
type: type as AfterSaleType,
status: 'processing',
reason: '商品质量问题',
description: '收到的商品有明显瑕疵,希望申请退款',
amount: 9999,
applyTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
processTime: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
contactPhone: '138****5678',
evidenceImages: [
'https://via.placeholder.com/200x200',
'https://via.placeholder.com/200x200'
],
progressRecords: [
{
id: '1',
time: new Date().toISOString(),
status: '处理中',
description: '客服正在处理您的申请,请耐心等待',
operator: '客服小王',
remark: '预计1-2个工作日内完成处理'
},
{
id: '2',
time: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(),
status: '已审核',
description: '您的申请已通过审核,正在安排处理',
operator: '审核员张三'
},
{
id: '3',
time: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
status: '已受理',
description: '我们已收到您的申请,正在进行审核',
operator: '系统'
},
{
id: '4',
time: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
status: '已提交',
description: '您已成功提交售后申请',
operator: '用户'
}
]
}
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000))
setAfterSaleDetail(mockAfterSaleDetail)
} catch (error) {
console.error('加载售后详情失败:', error)
setError('加载售后详情失败,请重试')
} finally {
setLoading(false)
}
}
// 刷新进度
const refreshProgress = () => {
loadAfterSaleDetail()
}
// 撤销申请
const cancelApplication = async () => {
try {
const result = await Taro.showModal({
title: '撤销申请',
content: '确定要撤销售后申请吗?撤销后无法恢复',
confirmText: '确定撤销',
cancelText: '取消'
})
if (!result.confirm) {
return
}
Taro.showLoading({
title: '撤销中...'
})
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1500))
Taro.hideLoading()
Taro.showToast({
title: '撤销成功',
icon: 'success'
})
// 延迟返回上一页
setTimeout(() => {
Taro.navigateBack()
}, 1500)
} catch (error) {
Taro.hideLoading()
console.error('撤销申请失败:', error)
Taro.showToast({
title: '撤销失败,请重试',
icon: 'none'
})
}
}
// 联系客服
const contactService = () => {
Taro.showModal({
title: '联系客服',
content: '客服电话400-123-4567\n工作时间9:00-18:00\n\n您也可以通过在线客服获得帮助',
showCancel: false
})
}
// 格式化时间
const formatTime = (timeStr: string) => {
const date = new Date(timeStr)
return `${date.getMonth() + 1}-${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
// 格式化完整时间
const formatFullTime = (timeStr: string) => {
const date = new Date(timeStr)
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
if (loading) {
return (
<View className="progress-page">
<View className="loading-container">
<Loading type="spinner" />
<Text className="loading-text">...</Text>
</View>
</View>
)
}
if (error) {
return (
<View className="progress-page">
<Empty
description={error}
imageSize={80}
>
<Button type="primary" size="small" onClick={refreshProgress}>
</Button>
</Empty>
</View>
)
}
if (!afterSaleDetail) {
return (
<View className="progress-page">
<Empty
description="暂无售后信息"
imageSize={80}
>
<Button type="primary" size="small" onClick={() => Taro.navigateBack()}>
</Button>
</Empty>
</View>
)
}
return (
<View className="progress-page">
{/* 售后基本信息 */}
<View className="after-sale-header">
<View className="header-top">
<View className="type-status">
<Text className="type-text">
{AFTER_SALE_TYPE_MAP[afterSaleDetail.type]}
</Text>
<Tag
color={STATUS_COLOR_MAP[afterSaleDetail.status]}
className="status-tag"
>
{AFTER_SALE_STATUS_MAP[afterSaleDetail.status]}
</Tag>
</View>
<Button size="small" fill="outline" onClick={refreshProgress}>
</Button>
</View>
<View className="header-info">
<Text className="order-no">{afterSaleDetail.orderNo}</Text>
<Text className="apply-time">
{formatFullTime(afterSaleDetail.applyTime)}
</Text>
<Text className="amount">¥{afterSaleDetail.amount}</Text>
</View>
</View>
{/* 进度时间线 */}
<View className="progress-timeline">
<View className="timeline-header">
<Text className="timeline-title"></Text>
</View>
<View className="timeline-list">
{afterSaleDetail.progressRecords.map((record, index) => (
<View key={record.id} className={`timeline-item ${index === 0 ? 'current' : ''}`}>
<View className="timeline-dot" />
<View className="timeline-content">
<View className="timeline-info">
<Text className="timeline-status">{record.status}</Text>
<Text className="timeline-time">{formatTime(record.time)}</Text>
</View>
<Text className="timeline-description">{record.description}</Text>
{record.operator && (
<Text className="timeline-operator">{record.operator}</Text>
)}
{record.remark && (
<Text className="timeline-remark">{record.remark}</Text>
)}
</View>
</View>
))}
</View>
</View>
{/* 申请详情 */}
<CellGroup title="申请详情">
<Cell title="申请原因" value={afterSaleDetail.reason} />
<Cell title="问题描述" value={afterSaleDetail.description} />
{afterSaleDetail.contactPhone && (
<Cell title="联系电话" value={afterSaleDetail.contactPhone} />
)}
</CellGroup>
{/* 凭证图片 */}
{afterSaleDetail.evidenceImages.length > 0 && (
<View className="evidence-section">
<View className="section-title"></View>
<View className="image-list">
{afterSaleDetail.evidenceImages.map((image, index) => (
<View key={index} className="image-item">
<image
src={image}
mode="aspectFill"
className="evidence-image"
onClick={() => {
Taro.previewImage({
urls: afterSaleDetail.evidenceImages,
current: image
})
}}
/>
</View>
))}
</View>
</View>
)}
{/* 底部操作 */}
<View className="progress-footer">
<View className="footer-buttons">
<Button onClick={contactService}>
</Button>
{(afterSaleDetail.status === 'pending' || afterSaleDetail.status === 'approved') && (
<Button type="primary" onClick={cancelApplication}>
</Button>
)}
</View>
</View>
</View>
)
}
export default AfterSaleProgressPage