feat(user): 新增收货地址管理及售后申请页面

- 新增地址类型定义,增强前端地址数据结构
- 新增地址编辑页面,支持地址智能识别和定位选点功能
- 地址编辑支持省市区选择及默认地址设置
- 新增地址列表页面,支持地址展示、删除、编辑和选择功能
- 实现售后申请页面,支持选择售后类型和退款原因
- 售后申请支持商品选择、退款金额计算和凭证上传
- 新增售后详情页面,支持售后状态展示及申请取消
- 优化页面加载和用户交互体验,增加错误提示和权限处理
This commit is contained in:
2026-07-01 12:11:56 +08:00
parent bf6ed504cc
commit 1fa58040f3
636 changed files with 58878 additions and 716 deletions

View File

@@ -0,0 +1,200 @@
.confirm-page {
display: flex;
flex-direction: column;
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 160rpx;
}
/* 地址卡片 */
.address-card {
position: relative;
background: #fff;
margin: 24rpx 24rpx 0;
border-radius: 16rpx;
padding: 24rpx 48rpx 24rpx 24rpx;
}
.addr-info {
display: flex;
align-items: center;
gap: 24rpx;
margin-bottom: 8rpx;
}
.addr-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.addr-phone {
font-size: 26rpx;
color: #666;
}
.addr-detail {
display: block;
font-size: 24rpx;
color: #666;
}
.addr-empty {
display: block;
font-size: 28rpx;
color: #ff6b35;
text-align: center;
padding: 8rpx 0;
}
.addr-arrow {
position: absolute;
right: 24rpx;
top: 50%;
transform: translateY(-50%);
font-size: 28rpx;
color: #ccc;
}
/* 权益包信息 */
.pkg-info-card {
background: #fff;
margin: 16rpx 24rpx 0;
border-radius: 16rpx;
overflow: hidden;
display: flex;
}
.pkg-img {
width: 200rpx;
height: 180rpx;
flex-shrink: 0;
}
.pkg-detail {
flex: 1;
padding: 20rpx 24rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.pkg-name {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.pkg-desc {
display: block;
font-size: 22rpx;
color: #999;
line-height: 1.5;
margin-bottom: 8rpx;
}
.pkg-cost-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.cost-item {
font-size: 28rpx;
font-weight: bold;
}
.cost-item.points { color: #ff6b35; }
.cost-item.balance { color: #ff6b35; }
.cost-item.free { color: #07c160; }
/* 须知 */
.notice-card {
background: #fff;
margin: 16rpx 24rpx 0;
border-radius: 16rpx;
padding: 24rpx;
}
.notice-title {
display: block;
font-size: 26rpx;
font-weight: bold;
color: #333;
margin-bottom: 12rpx;
}
.notice-item {
display: block;
font-size: 22rpx;
color: #999;
line-height: 1.8;
}
/* 我的资产 */
.assets-card {
background: #fff;
margin: 16rpx 24rpx 0;
border-radius: 16rpx;
padding: 24rpx;
}
.assets-title {
display: block;
font-size: 26rpx;
font-weight: bold;
color: #333;
margin-bottom: 16rpx;
}
.assets-row {
display: flex;
gap: 48rpx;
}
.asset-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.asset-value {
font-size: 36rpx;
font-weight: bold;
color: #ff6b35;
line-height: 1.2;
}
.asset-label {
font-size: 22rpx;
color: #999;
margin-top: 6rpx;
}
/* 底部按钮 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 24rpx;
box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.08);
}
.confirm-btn {
padding: 28rpx 0;
text-align: center;
background: linear-gradient(135deg, #ff6b35 0%, #ff9a56 100%);
color: #fff;
font-size: 32rpx;
font-weight: bold;
border-radius: 48rpx;
}
.confirm-btn.disabled {
opacity: 0.5;
}

View File

@@ -0,0 +1,178 @@
import { View, Text, Image } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import { useState, useEffect } from 'react';
import { getBenefitPackageDetail, exchangeBenefitPackage } from '@/api/shop/shopMemberBenefit';
import { getDefaultAddress } from '@/api/shop/shopUserAddress';
import { getUserBalance } from '@/api/system/user/balance';
import './index.scss';
definePageConfig({
navigationBarTitleText: '确认兑换',
});
export default function BenefitExchangeConfirmPage() {
const router = useRouter();
const pkgId = Number(router.params.id);
const [pkg, setPkg] = useState<any>(null);
const [address, setAddress] = useState<any>(null);
const [userBalance, setUserBalance] = useState<string>('--');
const [userPoints, setUserPoints] = useState<string>('--');
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
loadPackage();
loadAddress();
loadUserBalance();
}, []);
const loadPackage = async () => {
const res = await getBenefitPackageDetail(pkgId);
if (res.code === 200) setPkg(res.data);
};
const loadAddress = async () => {
const res = await getDefaultAddress();
if (res.code === 200 && res.data) setAddress(res.data);
};
const loadUserBalance = async () => {
try {
const data = await getUserBalance();
setUserBalance(data.balance || '0');
setUserPoints(String(data.points || 0));
} catch {
// ignore
}
};
const handleSelectAddress = () => {
Taro.navigateTo({ url: '/pages/user/address-list?select=1' });
};
const handleExchange = async () => {
if (!pkg) return;
if (pkg.needShip && !address) {
Taro.showToast({ title: '请先选择收货地址', icon: 'none' });
return;
}
Taro.showModal({
title: '确认兑换',
content: `确认兑换「${pkg.name}」?`,
success: async (res) => {
if (!res.confirm) return;
setSubmitting(true);
try {
const res2 = await exchangeBenefitPackage({
packageId: pkgId,
addressId: address?.id,
});
if (res2.code === 200) {
Taro.showToast({ title: '兑换申请成功', icon: 'success' });
setTimeout(() => {
Taro.redirectTo({ url: '/pages/user/benefit-exchange-list/index' });
}, 1500);
} else {
Taro.showToast({ title: res2.message || '兑换失败', icon: 'none' });
}
} catch (e) {
Taro.showToast({ title: '网络错误', icon: 'none' });
} finally {
setSubmitting(false);
}
},
});
};
if (!pkg) return null;
return (
<View className="confirm-page">
{/* 收货地址 */}
{pkg.needShip === 1 && (
<View className="address-card" onClick={handleSelectAddress}>
{address ? (
<>
<View className="addr-info">
<Text className="addr-name">{address.name}</Text>
<Text className="addr-phone">{address.phone}</Text>
</View>
<Text className="addr-detail">
{address.province}{address.city}{address.district}{address.detail}
</Text>
</>
) : (
<Text className="addr-empty">+ </Text>
)}
<Text className="addr-arrow">&gt;</Text>
</View>
)}
{/* 权益包信息 */}
<View className="pkg-info-card">
{pkg.coverImage && (
<Image className="pkg-img" src={pkg.coverImage} mode="aspectFill" />
)}
<View className="pkg-detail">
<Text className="pkg-name">{pkg.name}</Text>
{pkg.description && (
<Text className="pkg-desc">{pkg.description}</Text>
)}
<View className="pkg-cost-row">
{pkg.pointsCost > 0 && (
<Text className="cost-item points">{pkg.pointsCost} </Text>
)}
{pkg.balanceCost > 0 && (
<Text className="cost-item balance">¥{pkg.balanceCost}</Text>
)}
{!pkg.pointsCost && !pkg.balanceCost && (
<Text className="cost-item free"></Text>
)}
</View>
</View>
</View>
{/* 我的资产 */}
<View className="assets-card">
<Text className="assets-title"></Text>
<View className="assets-row">
{pkg.pointsCost > 0 && (
<View className="asset-item">
<Text className="asset-value">{userPoints}</Text>
<Text className="asset-label"></Text>
</View>
)}
{pkg.balanceCost > 0 && (
<View className="asset-item">
<Text className="asset-value">{userBalance}</Text>
<Text className="asset-label"></Text>
</View>
)}
</View>
</View>
{/* 须知 */}
<View className="notice-card">
<Text className="notice-title"></Text>
<Text className="notice-item">· </Text>
<Text className="notice-item">· </Text>
<Text className="notice-item">· </Text>
{pkg.validDays && (
<Text className="notice-item">· {pkg.validDays}</Text>
)}
</View>
{/* 底部按钮 */}
<View className="bottom-bar">
<View
className={`confirm-btn ${submitting ? 'disabled' : ''}`}
onClick={!submitting ? handleExchange : undefined}
>
{submitting ? '提交中...' : '确认兑换'}
</View>
</View>
</View>
);
}