Browse Source

商品退款, 退款结果页

master
DU 6 months ago
parent
commit
c6b30a6b6b
  1. 1
      src/app.config.ts
  2. 57
      src/components/GoodsInfoCard/index.scss
  3. 72
      src/components/GoodsInfoCard/index.tsx
  4. 43
      src/pagesUser/goodsCons/index.scss
  5. 42
      src/pagesUser/goodsCons/index.tsx
  6. 64
      src/pagesUser/goodsOrderDetail/index.scss
  7. 105
      src/pagesUser/goodsOrderDetail/index.tsx
  8. 5
      src/pagesUser/goodsRefundResult/index.config.ts
  9. 112
      src/pagesUser/goodsRefundResult/index.scss
  10. 181
      src/pagesUser/goodsRefundResult/index.tsx

1
src/app.config.ts

@ -26,6 +26,7 @@ export default defineAppConfig({ @@ -26,6 +26,7 @@ export default defineAppConfig({
'unsettledOrders/index', // 未结算账单
'goodsCons/index', // 商品消费
'goodsOrderDetail/index', // 商品消费详情 (已支付,未支付)
'goodsRefundResult/index', // 商品退款结果(成功,失败)
'helpCenter/index', // 帮助中心
'agreement/index', // 协议
'editProfile/index', // 编辑资料

57
src/components/GoodsInfoCard/index.scss

@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
.goods_info_card_component {
display: flex;
align-items: flex-start;
// 商品图片
.goods_image {
width: 208px;
height: 208px;
border-radius: 8px;
flex-shrink: 0;
}
// 商品信息
.goods_info {
flex: 1;
margin-left: 30px;
display: flex;
flex-direction: column;
padding: 10px 0;
.goods_name {
font-size: 30px;
color: #000000;
font-weight: 600;
line-height: 1.4;
margin-bottom: 10px;
}
// 价格行
.price_row {
display: flex;
align-items: center;
justify-content: space-between;
// 有间距的情况用于订单详情页
// &.with_separator {
margin-top: 56px; // 实现 padding-top: 66px 的效果66-10=56
// }
.price_text {
font-size: 30px;
font-weight: 500;
.price_label {
color: #000000;
font-weight: 400;
}
}
// 右侧按钮区域
.right_button_area {
margin-left: 20px;
flex-shrink: 0;
}
}
}
}

72
src/components/GoodsInfoCard/index.tsx

@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
/**
*
* 退
*/
import { View, Text } from '@tarojs/components'
import { Image } from '@taroify/core'
import { ReactNode } from 'react'
import './index.scss'
export interface GoodsInfoCardProps {
// 基础信息
image: string // 商品图片
name: string // 商品名称
// 金额显示
priceLabel?: string // 金额标签(如 "退款:")
price: number // 金额
priceColor?: string // 金额颜色(默认 #FF3A24)
// 右侧操作按钮(可选)
rightButton?: ReactNode // 自定义右侧按钮
// 样式定制
containerClassName?: string // 容器额外类名
showPriceSeparator?: boolean // 是否显示价格和名称之间的间距(默认 true)
}
const GoodsInfoCard: React.FC<GoodsInfoCardProps> = ({
image,
name,
priceLabel,
price,
priceColor = '#FF3A24',
rightButton,
containerClassName = '',
showPriceSeparator = true
}) => {
return (
<View className={`goods_info_card_component ${containerClassName}`}>
{/* 商品图片 */}
<Image
className="goods_image"
src={image}
mode="aspectFill"
width={208}
height={208}
/>
{/* 商品信息 */}
<View className="goods_info">
<Text className="goods_name">{name}</Text>
{/* 价格/退款金额行 */}
<View className={`price_row ${showPriceSeparator ? 'with_separator' : ''}`}>
<Text className="price_text" style={{ color: priceColor }}>
{priceLabel && <Text className="price_label">{priceLabel} </Text>}
¥ {price.toFixed(2)}
</Text>
{/* 右侧按钮区域 */}
{rightButton && (
<View className="right_button_area">
{rightButton}
</View>
)}
</View>
</View>
</View>
)
}
export default GoodsInfoCard

43
src/pagesUser/goodsCons/index.scss

@ -156,51 +156,12 @@ @@ -156,51 +156,12 @@
.refund_card {
// 商品行
.goods_row {
display: flex;
align-items: flex-start;
margin-bottom: 20px;
.goods_img {
width: 208px;
height: 208px;
border-radius: 8px;
flex-shrink: 0;
&.placeholder {
background-color: #F7F8FA;
display: flex;
align-items: center;
justify-content: center;
border: 1PX dashed #DCDEE0;
.placeholder_icon {
width: 80px;
height: 80px;
background-color: #DCDEE0;
border-radius: 4px;
}
}
}
.goods_info_box {
flex: 1;
margin-left: 30px;
display: flex;
flex-direction: column;
// 退款商品信息特殊样式
.refund_goods_info {
.goods_name {
font-size: 30px;
color: #000000;
font-weight: 600;
line-height: 1.4;
margin-bottom: 10px;
}
.refund_amount {
font-size: 30px;
color: #000000;
margin-top: auto;
padding-top: 66px;
}
}
}

42
src/pagesUser/goodsCons/index.tsx

@ -1,12 +1,13 @@ @@ -1,12 +1,13 @@
/**
*
*/
import { View, Text, Button, } from '@tarojs/components'
import { View, Text, Button } from '@tarojs/components'
import { useState } from 'react'
import Taro, { useLoad } from '@tarojs/taro'
import { Tabs, Divider,Image } from '@taroify/core'
import { Tabs, Divider } from '@taroify/core'
import { ArrowDown, Arrow } from '@taroify/icons'
import EmptyData from '@/components/EmptyData'
import GoodsInfoCard from '@/components/GoodsInfoCard'
import FilterPopup from '../orderList/components/FilterPopup'
import './index.scss'
@ -165,6 +166,14 @@ const GoodsCons = () => { @@ -165,6 +166,14 @@ const GoodsCons = () => {
})
}
// 退款订单点击
const handleRefundClick = (orderId: string, goodsId: string, refundStatus: 'success' | 'failed') => {
console.log('退款详情', orderId, goodsId, refundStatus)
Taro.navigateTo({
url: `/pagesUser/goodsRefundResult/index?orderId=${orderId}&goodsId=${goodsId}&type=${refundStatus}`
})
}
// 立即支付
const handlePay = (orderId: string, e: any) => {
e.stopPropagation() // 阻止冒泡到卡片点击事件
@ -230,28 +239,19 @@ const GoodsCons = () => { @@ -230,28 +239,19 @@ const GoodsCons = () => {
<View
key={`${order.id}-${goods.id}`}
className="order-card refund_card"
onClick={() => handleOrderClick(order.id)}
onClick={() => handleRefundClick(order.id, goods.id, goods.refundStatus)}
>
{/* 商品信息 */}
<View className="goods_row">
{/* 商品图片 */}
<Image
width="208rpx"
height="208rpx"
className="goods_img"
src={goods.image}
mode="aspectFill"
placeholder="加载中..."
fallback="加载失败"
/>
{/* 商品名称和退款金额 */}
<View className="goods_info_box">
<Text className="goods_name">{goods.name}</Text>
<Text className="refund_amount">退: ¥ {goods.refundAmount.toFixed(2)}</Text>
</View>
<GoodsInfoCard
image={goods.image}
name={goods.name}
priceLabel="退款:"
price={goods.refundAmount}
priceColor="#000000"
showPriceSeparator={false}
containerClassName="refund_goods_info"
/>
</View>
{/* 退款状态栏 */}

64
src/pagesUser/goodsOrderDetail/index.scss

@ -12,50 +12,44 @@ @@ -12,50 +12,44 @@
margin-bottom: 30px;
.goods_item {
display: flex;
align-items: flex-start;
// 商品项之间的间距由 style 控制
.goods_image {
width: 208px;
height: 208px;
border-radius: 8px;
flex-shrink: 0;
}
// 退款按钮样式
.refund_btn {
padding: 15px 30px;
border-radius: 50px;
transition: all 0.3s;
.goods_info {
flex: 1;
margin-left: 30px;
display: flex;
flex-direction: column;
.refund_text {
font-size: 25px;
}
.goods_name {
font-size: 30px;
color: #000000;
line-height: 1.4;
margin-bottom: 10px;
// 可申请状态绿色
&.available {
background: #03C175;
.refund_text {
color: #FFFFFF;
}
}
.price_container {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 66px;
// 已申请状态浅绿色不可点击
&.applied {
background: #B8E6D5;
pointer-events: none;
.goods_price {
font-size: 30px;
color: #FF3A24;
font-weight: 500;
.refund_text {
color: #FFFFFF;
}
}
.refund_btn {
padding: 10px 30px;
background: #F2F3F5;
border-radius: 25px;
// 超时状态灰色不可点击
&.timeout {
background: #F2F3F5;
pointer-events: none;
.refund_text {
font-size: 25px;
color: #666666;
}
.refund_text {
color: #C8C9CC;
}
}
}

105
src/pagesUser/goodsOrderDetail/index.tsx

@ -5,15 +5,21 @@ import { View, Text, Button, Textarea } from '@tarojs/components' @@ -5,15 +5,21 @@ import { View, Text, Button, Textarea } from '@tarojs/components'
import { useState } from 'react'
import Taro, { useLoad } from '@tarojs/taro'
import { Image, Popup } from '@taroify/core'
import GoodsInfoCard from '@/components/GoodsInfoCard'
import './index.scss'
// 退款超时时间(单位:小时)
const REFUND_TIMEOUT_HOURS = 24
// 商品信息接口
interface GoodsItem {
id: string
image: string // 商品图片
name: string // 商品名称
price: number // 商品价格
canRefund?: boolean // 是否可退款(仅已支付订单)
image: string // 商品图片
name: string // 商品名称
price: number // 商品价格
canRefund?: boolean // 是否可退款(仅已支付订单)
createTime?: number // 创建时间戳(毫秒)
refundStatus?: 'none' | 'applied' // 退款状态:none-未申请,applied-已申请
}
// 订单详情接口
@ -42,27 +48,41 @@ const OrderDetail = () => { @@ -42,27 +48,41 @@ const OrderDetail = () => {
// 模拟订单详情数据
const [orderDetail] = useState<OrderDetail>({
id: '1',
status: 'unpaid',
status: 'paid',
goods: [
{
id: '1',
image: 'https://img01.yzcdn.cn/vant/cat.jpeg',
name: '美汁源Minute Maid果粒橙 橙汁饮料 1.25l/瓶',
price: 12.50
price: 12.50,
canRefund: true,
createTime: Date.now() - 1000 * 60 * 60 * 10, // 10小时前(未超时,可申请)
refundStatus: 'none'
},
{
id: '2',
image: 'https://img01.yzcdn.cn/vant/cat.jpeg',
name: '可口可乐 迷你装 200ml/罐',
price: 3.00,
canRefund: true
canRefund: true,
createTime: Date.now() - 1000 * 60 * 60 * 10, // 10小时前(未超时,已申请)
refundStatus: 'applied'
},
{
id: '3',
image: 'https://img01.yzcdn.cn/vant/cat.jpeg',
name: '雪碧 330ml/瓶',
price: 4.00,
canRefund: true,
createTime: Date.now() - 1000 * 60 * 60 * 30, // 30小时前(已超时)
refundStatus: 'none'
}
],
storeName: 'XXX店',
address: 'XXX市XXXXXXXXXXX市XXXXXXXXXXX市XXXXXXXX',
orderNo: 'wjekwejrwkjsf1321445255',
amount: 15.50,
goodsCount: 2,
amount: 19.50,
goodsCount: 3,
merchantPhone: '16688889999',
paymentTime: '2025-01-01 10:00:00',
paymentMethod: '微信支付'
@ -81,12 +101,45 @@ const OrderDetail = () => { @@ -81,12 +101,45 @@ const OrderDetail = () => {
}
})
// 判断商品是否超时不可退款
const isRefundTimeout = (createTime?: number) => {
if (!createTime) return false
const now = Date.now()
const diffHours = (now - createTime) / (1000 * 60 * 60)
return diffHours > REFUND_TIMEOUT_HOURS
}
// 获取退款按钮状态
const getRefundBtnStatus = (goods: GoodsItem) => {
if (!goods.canRefund) return 'hidden' // 不可退款,隐藏按钮
if (goods.refundStatus === 'applied') return 'applied' // 已申请
if (isRefundTimeout(goods.createTime)) return 'timeout' // 超时
return 'available' // 可申请
}
// 获取退款按钮文本
const getRefundBtnText = (status: string) => {
switch (status) {
case 'applied':
return '已申请'
case 'timeout':
return '申请退款'
default:
return '申请退款'
}
}
// 申请退款
const handleRefund = (goodsId: string) => {
console.log('申请退款', goodsId)
// 查找商品
const goods = orderDetail.goods.find(item => item.id === goodsId)
if (goods) {
const status = getRefundBtnStatus(goods)
// 只有状态为 available 时才能点击
if (status !== 'available') {
return
}
setCurrentRefundGoods(goods)
setRefundReason('')
setRefundPopupOpen(true)
@ -146,25 +199,23 @@ const OrderDetail = () => { @@ -146,25 +199,23 @@ const OrderDetail = () => {
<View className="goods_card">
{orderDetail.goods.map((goods, index) => (
<View key={goods.id} className="goods_item" style={{ marginTop: index > 0 ? '30px' : '0' }}>
<Image
className="goods_image"
src={goods.image}
mode="aspectFill"
width={208}
height={208}
/>
<View className="goods_info">
<Text className="goods_name">{goods.name}</Text>
<View className="price_container">
<Text className="goods_price">¥ {goods.price.toFixed(2)}</Text>
{/* 已支付订单且商品可退款时显示申请退款按钮 */}
{pageType === 'paid' && goods.canRefund && (
<View className="refund_btn" onClick={() => handleRefund(goods.id)}>
<Text className="refund_text">退</Text>
<GoodsInfoCard
image={goods.image}
name={goods.name}
price={goods.price}
priceColor="#FF3A24"
showPriceSeparator={true}
rightButton={
pageType === 'paid' && getRefundBtnStatus(goods) !== 'hidden' ? (
<View
className={`refund_btn ${getRefundBtnStatus(goods)}`}
onClick={() => handleRefund(goods.id)}
>
<Text className="refund_text">{getRefundBtnText(getRefundBtnStatus(goods))}</Text>
</View>
)}
</View>
</View>
) : undefined
}
/>
</View>
))}
</View>

5
src/pagesUser/goodsRefundResult/index.config.ts

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
export default {
navigationBarTitleText: '商品消费',
navigationBarBackgroundColor: '#FFFFFF',
navigationBarTextStyle: 'black',
}

112
src/pagesUser/goodsRefundResult/index.scss

@ -0,0 +1,112 @@ @@ -0,0 +1,112 @@
.refund_result_page {
min-height: 100vh;
background-color: #F2F3F5;
padding: 30px;
// 退款结果状态卡片
.result_status_card {
background-color: #FFFFFF;
border-radius: 12px;
padding: 50px 30px;
margin-bottom: 30px;
text-align: center;
.status_title {
display: block;
font-size: 40px;
font-weight: 600;
margin-bottom: 30px;
&.success {
color: #000000;
}
&.failed {
color: #EE0A24;
}
}
// 成功退回账户信息
.refund_account {
display: block;
font-size: 28px;
color: #333333;
line-height: 1.5;
}
// 失败原因行
.fail_reason_row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
.reason_label {
font-size: 28px;
color: #333333;
}
.reason_value {
font-size: 28px;
color: #333333;
}
}
}
// 商品信息卡片
.goods_info_card {
background-color: #FFFFFF;
border-radius: 12px;
padding: 30px;
margin-bottom: 30px;
}
// 退款详细信息卡片
.refund_detail_card {
background-color: #FFFFFF;
border-radius: 12px;
padding: 30px;
.info_list {
display: flex;
flex-direction: column;
gap: 20px;
.info_row {
display: flex;
align-items: flex-start;
.info_label {
width: 180px;
font-size: 28px;
color: #333333;
flex-shrink: 0;
}
.info_value {
flex: 1;
margin-left: 30px;
font-size: 28px;
color: #595959;
text-align: left;
word-break: break-all;
}
}
}
// 虚线分隔
.divider {
height: 1PX;
border-bottom: 1PX dashed #D3D8DE;
margin: 30px 0;
}
// 联系商家仅失败时显示
.merchant_contact {
.contact_text {
font-size: 28px;
color: #333333;
}
}
}
}

181
src/pagesUser/goodsRefundResult/index.tsx

@ -0,0 +1,181 @@ @@ -0,0 +1,181 @@
/**
* 退
*/
import { View, Text } from '@tarojs/components'
import { useState } from 'react'
import Taro, { useLoad } from '@tarojs/taro'
import GoodsInfoCard from '@/components/GoodsInfoCard'
import './index.scss'
// 退款结果类型
type RefundResultType = 'success' | 'failed'
// 商品信息接口
interface GoodsInfo {
id: string
image: string
name: string
refundAmount: number
}
// 退款结果信息接口
interface RefundResultInfo {
resultType: RefundResultType // 退款结果类型
goods: GoodsInfo // 商品信息
// 成功时的字段
refundAccount?: string // 退回账户(微信/支付宝/银行卡)
// 失败时的字段
failReason?: string // 失败原因
// 通用字段
refundReason: string // 退款原因
applyAmount: string // 申请金额
applyTime: string // 申请时间
// 成功时额外字段
refundCompleteTime?: string // 退款完结时间
refundNo?: string // 退款编号
// 门店信息
storeName: string // 消费门店
address: string // 地址
merchantPhone?: string // 商家电话(失败时显示)
}
const GoodsRefundResult = () => {
const [pageType, setPageType] = useState<RefundResultType>('success')
// 模拟退款结果数据
const [refundInfo] = useState<RefundResultInfo>({
resultType: 'success',
goods: {
id: '1',
image: 'https://img01.yzcdn.cn/vant/cat.jpeg',
name: '美汁源Minute Maid果粒橙 橙汁饮料 1.25l/瓶',
refundAmount: 12.50
},
refundAccount: '微信/支付宝/银行卡(中国银行6666)',
refundReason: '计算错误',
applyAmount: '12.50元',
applyTime: '2025-01-01 12:00:00',
refundCompleteTime: '2025-01-01 12:06:00',
refundNo: 'a202501011206060000',
storeName: 'XXXXXXXXX店',
address: 'XXX市XXXXXXX区XXXXXXXXXX街道XXX大厦101',
merchantPhone: '16688889999',
failReason: '已成功取货'
})
useLoad(() => {
console.log('退款结果页面加载')
// 从 URL 获取页面类型
const router = Taro.getCurrentInstance().router
if (router?.params) {
const { type } = router.params
if (type === 'success' || type === 'failed') {
setPageType(type as RefundResultType)
}
}
})
// 联系商家
const handleCallMerchant = () => {
if (refundInfo.merchantPhone) {
Taro.makePhoneCall({
phoneNumber: refundInfo.merchantPhone
})
}
}
return (
<View className="refund_result_page">
{/* 退款结果状态卡片 */}
<View className="result_status_card">
<Text className={`status_title ${pageType === 'success' ? 'success' : 'failed'}`}>
{pageType === 'success' ? '退款成功' : '退款失败'}
</Text>
{pageType === 'success' ? (
// 成功:显示退回账户信息
<Text className="refund_account">
退{refundInfo.refundAccount} ¥ {refundInfo.goods.refundAmount.toFixed(2)}
</Text>
) : (
// 失败:显示失败原因
<View className="fail_reason_row">
<Text className="reason_label"></Text>
<Text className="reason_value">{refundInfo.failReason}</Text>
</View>
)}
</View>
{/* 商品信息卡片 */}
<View className="goods_info_card">
<GoodsInfoCard
image={refundInfo.goods.image}
name={refundInfo.goods.name}
priceLabel="退款:"
price={refundInfo.goods.refundAmount}
priceColor="#000000"
showPriceSeparator={false}
/>
</View>
{/* 退款详细信息卡片 */}
<View className="refund_detail_card">
<View className="info_list">
<View className="info_row">
<Text className="info_label">退</Text>
<Text className="info_value">{refundInfo.refundReason}</Text>
</View>
<View className="info_row">
<Text className="info_label"></Text>
<Text className="info_value">{refundInfo.applyAmount}</Text>
</View>
<View className="info_row">
<Text className="info_label"></Text>
<Text className="info_value">{refundInfo.applyTime}</Text>
</View>
{/* 成功时显示退款完结和退款编号 */}
{pageType === 'success' && (
<>
<View className="info_row">
<Text className="info_label">退</Text>
<Text className="info_value">{refundInfo.refundCompleteTime}</Text>
</View>
<View className="info_row">
<Text className="info_label">退</Text>
<Text className="info_value">{refundInfo.refundNo}</Text>
</View>
</>
)}
<View className="info_row">
<Text className="info_label"></Text>
<Text className="info_value">{refundInfo.storeName}</Text>
</View>
<View className="info_row">
<Text className="info_label"></Text>
<Text className="info_value">{refundInfo.address}</Text>
</View>
</View>
{/* 失败时显示联系商家 */}
{pageType === 'failed' && refundInfo.merchantPhone && (
<>
<View className="divider" />
<View className="merchant_contact" onClick={handleCallMerchant}>
<Text className="contact_text">{refundInfo.merchantPhone}</Text>
</View>
</>
)}
</View>
</View>
)
}
export default GoodsRefundResult
Loading…
Cancel
Save