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,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>
);
}