feat: 新增订单详情退款弹窗

This commit is contained in:
duanshuwen
2025-08-07 20:03:03 +08:00
parent a8a5a90d5e
commit 72c761428b
10 changed files with 1345 additions and 97 deletions

View File

@@ -1,6 +1,5 @@
<template>
<view class="login-wrapper">
<image class="bg" src="./images/bg.png"></image>
<view class="login-wrapper" :style="{ backgroundImage: `url(${loginBg})` }">
<!-- 头部内容 -->
<view class="login-header">
<!-- 卡通形象 -->
@@ -66,6 +65,7 @@ import CheckBox from "@/components/CheckBox/index.vue";
import AgreePopup from "./components/AgreePopup/index.vue";
import { loginAuth, bindPhone, checkPhone } from "@/manager/LoginManager";
import { goHome } from "@/hooks/useGoHome";
import loginBg from "./images/bg.png";
const isAgree = ref(false);
const visible = ref(false);

View File

@@ -1,70 +1,62 @@
.login-wrapper {
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
font-family: PingFang SC, PingFang SC;
height: 100vh;
padding-top: 168px;
position: relative;
.bg {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
width: 100%;
height: 100%;
}
.login-header {
text-align: center;
.login-avatar {
width: 150px;
display: block;
}
.login-title {
width: 137px;
margin: 6px auto;
}
.login-desc {
font-size: 12px;
color: #1E4C69;
line-height: 24px;
}
}
.login-btn-area {
margin-top: 46px;
width: 297px;
.login-btn {
background: linear-gradient( 246deg, #22A7FF 0%, #2567FF 100%);
width: 100%;
border-radius: 50px;
}
}
.login-agreement {
margin-top: 20px;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
font-family: PingFang SC, PingFang SC;
height: 100vh;
padding-top: 168px;
position: relative;
background-position: 0 0;
background-size: 100% 100%;
background-repeat: no-repeat;
.login-agreement-text {
font-size: 14px;
color: #666;
.login-header {
text-align: center;
max-height: 223px;
.login-avatar {
width: 150px;
display: block;
}
.login-title {
width: 137px;
margin: 6px auto;
}
.login-desc {
font-size: 12px;
color: #1e4c69;
line-height: 24px;
}
}
.login-agreement-link {
font-size: 14px;
color: #007aff;
margin: 0 5px;
.login-btn-area {
margin-top: 46px;
width: 297px;
.login-btn {
background: linear-gradient(246deg, #22a7ff 0%, #2567ff 100%);
width: 100%;
border-radius: 50px;
}
}
}
}
.login-agreement {
margin-top: 20px;
display: flex;
align-items: center;
.login-agreement-text {
font-size: 14px;
color: #666;
}
.login-agreement-link {
font-size: 14px;
color: #007aff;
margin: 0 5px;
}
}
}

View File

@@ -38,7 +38,7 @@
</template>
<script setup>
import { defineProps, computed, ref } from "vue";
import { defineProps, computed, ref, defineEmits } from "vue";
import {
preOrder,
orderPayNow,
@@ -56,6 +56,9 @@ const PAY_WAY_MAP = {
// 加载状态
const isLoading = ref(false);
// 定义事件发射器
const emit = defineEmits(['show-refund-popup']);
const props = defineProps({
orderData: {
type: Object,
@@ -111,39 +114,35 @@ const handleButtonClick = async () => {
// 支付渠道
const paySource = "1";
if (status === "2") {
// 情况2待使用状态显示退款弹窗
emit('show-refund-popup');
return; // 直接返回,不执行后续代码
}
try {
isLoading.value = true;
if (status === "2") {
// 情况2待使用状态直接申请退款
await orderRefund({ orderId });
// 情况1待支付状态或其他状态先预下单再支付
// 第一步:预下单
const res = await orderPayNow({ orderId, payWay, paySource });
console.log(res);
uni.showToast({
title: "退款申请已提交",
icon: "success",
});
} else {
// 情况1待支付状态或其他状态先预下单再支付
// 第一步:预下单
const res = await orderPayNow({ orderId, payWay, paySource });
console.log(res);
// 仅作为示例,非真实参数信息。
uni.requestPayment({
provider: "wxpay",
timeStamp: String(Date.now()),
nonceStr: "A1B2C3D4E5",
package: "prepay_id=wx20180101abcdefg",
signType: "MD5",
paySign: "",
success: (res) => {
console.log("success:" + JSON.stringify(res));
},
fail: (err) => {
console.log("fail:" + JSON.stringify(err));
},
});
}
// 仅作为示例,非真实参数信息。
uni.requestPayment({
provider: "wxpay",
timeStamp: String(Date.now()),
nonceStr: "A1B2C3D4E5",
package: "prepay_id=wx20180101abcdefg",
signType: "MD5",
paySign: "",
success: (res) => {
console.log("success:" + JSON.stringify(res));
},
fail: (err) => {
console.log("fail:" + JSON.stringify(err));
},
});
} catch (error) {
console.error("操作失败:", error);
uni.showToast({

View File

@@ -0,0 +1,158 @@
# RefundPopup 退款弹窗组件
## 组件概述
`RefundPopup` 是一个用于处理订单退款相关操作的弹窗组件,支持多种退款场景和状态展示。
## 功能需求分析
### 界面设计规范
1. **弹窗容器**
- 使用圆角矩形容器,背景色为白色
- 弹窗宽度适中,居中显示
- 支持遮罩层,点击遮罩可关闭弹窗
2. **头部区域**
- 显示可爱的花朵卡通形象作为视觉元素
- 卡通形象位于弹窗顶部中央位置
3. **内容区域**
- 主标题:根据不同场景显示相应提示文字
- 副标题:显示详细的退款规则和说明
- 金额显示:突出显示可退金额(橙色字体)
- 退款政策:详细列出各种退款条件和规则
4. **按钮区域**
- 双按钮布局:左侧为次要操作,右侧为主要操作
- 按钮样式:圆角矩形,左侧蓝色,右侧橙色
- 按钮文字:根据场景显示不同的操作文字
### 交互功能
1. **弹窗显示/隐藏**
- 支持通过方法调用显示弹窗
- 支持点击遮罩层关闭弹窗
- 支持点击按钮后关闭弹窗
2. **退款场景处理**
- **不予退款场景**:显示"不予退款12小时以内取消或未入住不予退款"
- **免费取消场景**:显示"免费取消提前48小时以上全额退还房费"
- **部分退款场景**:显示退款政策详情和可退金额
- **商品限制场景**:显示"该商品未使用随时可退"
3. **按钮交互**
- 左侧按钮:"退订政策"或"退订政策"(查看详情)
- 右侧按钮:"我知道了"或"点击退款"(确认操作)
- 支持按钮点击事件回调
4. **数据展示**
- 动态显示可退金额
- 展示详细的退款规则列表
- 支持不同退款类型的文案切换
### 技术要求
1. **组件架构**
- 使用 Vue 3 Composition API
- 支持 TypeScript可选
- 使用 uni-popup 作为弹窗基础组件
2. **样式处理**
- 使用 SASS 预处理器
- 支持响应式设计
- 遵循设计规范的颜色和字体
3. **性能优化**
- 懒加载弹窗内容
- 合理使用计算属性
- 避免不必要的重渲染
4. **代码规范**
- 清晰的组件结构
- 详细的注释说明
- 统一的命名规范
### 样式规范
1. **颜色规范**
- 主色调:蓝色 #007AFF(左侧按钮)
- 强调色:橙色 #FF9500(右侧按钮、金额)
- 文字色:黑色 #000000(主要文字)
- 辅助色:灰色 #666666(辅助文字)
- 背景色:白色 #FFFFFF
2. **字体规范**
- 主标题16px加粗
- 副标题14px常规
- 金额18px加粗橙色
- 按钮文字16px加粗
- 说明文字12px常规
3. **间距规范**
- 弹窗内边距20px
- 元素间距12px
- 按钮间距12px
- 按钮高度44px
### 组件接口
#### Props
```typescript
interface RefundPopupProps {
// 弹窗显示状态
visible: boolean
// 退款类型:'no_refund' | 'free_cancel' | 'partial_refund' | 'anytime_refund'
refundType: string
// 可退金额
refundAmount?: number
// 退款规则列表
refundRules?: string[]
// 自定义标题
title?: string
// 自定义描述
description?: string
}
```
#### Events
```typescript
interface RefundPopupEvents {
// 弹窗关闭事件
'update:visible': (visible: boolean) => void
// 查看政策按钮点击
'policy-click': () => void
// 确认按钮点击
'confirm-click': () => void
// 弹窗关闭事件
'close': () => void
}
```
#### Methods
```typescript
interface RefundPopupMethods {
// 显示弹窗
show(): void
// 隐藏弹窗
hide(): void
}
```
### 使用场景
1. **订单详情页面**:用户查看退款政策
2. **退款申请流程**:确认退款操作
3. **客服咨询场景**:展示退款规则
4. **订单管理后台**:处理退款申请
### 文件结构
```
RefundPopup/
├── index.vue # 主组件文件
├── styles/
│ └── index.scss # 样式文件
├── demo.vue # 演示页面
└── README.md # 组件文档
```
### 开发注意事项
1. 确保弹窗在不同屏幕尺寸下的适配
2. 处理长文本的换行和显示
3. 考虑无障碍访问支持
4. 添加适当的动画效果
5. 确保组件的可复用性和可扩展性

View File

@@ -0,0 +1,389 @@
<template>
<view class="demo-container">
<view class="demo-header">
<text class="demo-title">RefundPopup 退款弹窗组件演示</text>
</view>
<view class="demo-content">
<!-- 场景选择 -->
<view class="demo-section">
<text class="section-title">退款场景</text>
<view class="scenario-buttons">
<button
class="scenario-btn"
:class="{ active: currentScenario === 'no_refund' }"
@click="setScenario('no_refund')"
>
不予退款
</button>
<button
class="scenario-btn"
:class="{ active: currentScenario === 'free_cancel' }"
@click="setScenario('free_cancel')"
>
免费取消
</button>
<button
class="scenario-btn"
:class="{ active: currentScenario === 'partial_refund' }"
@click="setScenario('partial_refund')"
>
部分退款
</button>
<button
class="scenario-btn"
:class="{ active: currentScenario === 'anytime_refund' }"
@click="setScenario('anytime_refund')"
>
随时可退
</button>
</view>
</view>
<!-- 金额设置 -->
<view class="demo-section">
<text class="section-title">退款金额</text>
<view class="amount-input">
<input
class="amount-field"
type="number"
v-model="refundAmount"
placeholder="请输入退款金额"
/>
<text class="amount-unit"></text>
</view>
</view>
<!-- 操作按钮 -->
<view class="demo-section">
<button class="demo-btn primary" @click="showPopup">
显示退款弹窗
</button>
</view>
<!-- 事件日志 -->
<view class="demo-section">
<text class="section-title">事件日志</text>
<view class="event-log">
<view
class="log-item"
v-for="(log, index) in eventLogs"
:key="index"
>
<text class="log-time">{{ log.time }}</text>
<text class="log-event">{{ log.event }}</text>
</view>
<view class="log-empty" v-if="eventLogs.length === 0">
暂无事件日志
</view>
</view>
<button class="demo-btn secondary" @click="clearLogs">
清空日志
</button>
</view>
</view>
<!-- RefundPopup 组件 -->
<RefundPopup
v-model="popupVisible"
:refund-type="currentScenario"
:refund-amount="refundAmount"
:refund-rules="customRules"
@policy-click="handlePolicyClick"
@confirm-click="handleConfirmClick"
@close="handleClose"
/>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import RefundPopup from './index.vue'
// 响应式数据
const popupVisible = ref(false)
const currentScenario = ref('no_refund')
const refundAmount = ref(399)
const eventLogs = ref([])
// 自定义退款规则(可选)
const customRules = ref([])
// 方法定义
const setScenario = (scenario) => {
currentScenario.value = scenario
addLog(`切换到场景: ${getScenarioName(scenario)}`)
// 根据场景设置默认金额
switch (scenario) {
case 'no_refund':
refundAmount.value = 0
break
case 'free_cancel':
refundAmount.value = 399
break
case 'partial_refund':
refundAmount.value = 199
break
case 'anytime_refund':
refundAmount.value = 399
break
}
}
const getScenarioName = (scenario) => {
const names = {
'no_refund': '不予退款',
'free_cancel': '免费取消',
'partial_refund': '部分退款',
'anytime_refund': '随时可退'
}
return names[scenario] || scenario
}
const showPopup = () => {
popupVisible.value = true
addLog('显示退款弹窗')
}
const handlePolicyClick = () => {
addLog('点击了退订政策按钮')
// 这里可以跳转到政策详情页面
uni.showToast({
title: '查看退订政策',
icon: 'none'
})
}
const handleConfirmClick = () => {
addLog(`确认操作 - 场景: ${getScenarioName(currentScenario.value)}, 金额: ¥${refundAmount.value}`)
// 根据不同场景执行不同操作
if (currentScenario.value === 'no_refund') {
uni.showToast({
title: '已知晓退款政策',
icon: 'success'
})
} else {
uni.showToast({
title: '退款申请已提交',
icon: 'success'
})
}
}
const handleClose = () => {
addLog('关闭退款弹窗')
}
const addLog = (event) => {
const now = new Date()
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
eventLogs.value.unshift({
time,
event
})
// 限制日志数量
if (eventLogs.value.length > 10) {
eventLogs.value = eventLogs.value.slice(0, 10)
}
}
const clearLogs = () => {
eventLogs.value = []
addLog('清空事件日志')
}
// 初始化
addLog('演示页面已加载')
</script>
<style lang="scss" scoped>
.demo-container {
padding: 20px;
background: #f5f5f5;
min-height: 100vh;
}
.demo-header {
text-align: center;
margin-bottom: 30px;
.demo-title {
font-size: 20px;
font-weight: 600;
color: #333333;
}
}
.demo-content {
.demo-section {
background: #ffffff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.section-title {
font-size: 16px;
font-weight: 600;
color: #333333;
margin-bottom: 16px;
display: block;
}
}
}
.scenario-buttons {
display: flex;
flex-wrap: wrap;
gap: 12px;
.scenario-btn {
flex: 1;
min-width: 80px;
height: 40px;
border: 2px solid #e0e0e0;
border-radius: 20px;
background: #ffffff;
color: #666666;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
&.active {
border-color: #007aff;
background: #007aff;
color: #ffffff;
}
&:active {
transform: scale(0.95);
}
}
}
.amount-input {
display: flex;
align-items: center;
gap: 8px;
.amount-field {
flex: 1;
height: 44px;
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 0 16px;
font-size: 16px;
background: #ffffff;
&:focus {
border-color: #007aff;
outline: none;
}
}
.amount-unit {
font-size: 16px;
color: #666666;
font-weight: 500;
}
}
.demo-btn {
width: 100%;
height: 48px;
border: none;
border-radius: 24px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
&.primary {
background: #007aff;
color: #ffffff;
&:active {
background: #0056cc;
transform: scale(0.98);
}
}
&.secondary {
background: #f0f0f0;
color: #666666;
margin-top: 12px;
&:active {
background: #e0e0e0;
transform: scale(0.98);
}
}
}
.event-log {
max-height: 200px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 12px;
background: #f9f9f9;
.log-item {
display: flex;
align-items: center;
gap: 12px;
padding: 4px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.log-time {
font-size: 12px;
color: #999999;
font-family: monospace;
min-width: 60px;
}
.log-event {
font-size: 14px;
color: #333333;
flex: 1;
}
}
.log-empty {
text-align: center;
color: #999999;
font-size: 14px;
padding: 20px 0;
}
}
// 响应式适配
@media screen and (max-width: 375px) {
.demo-container {
padding: 16px;
}
.demo-content {
.demo-section {
padding: 16px;
margin-bottom: 16px;
}
}
.scenario-buttons {
.scenario-btn {
min-width: 70px;
height: 36px;
font-size: 13px;
}
}
}
</style>

View File

@@ -0,0 +1,274 @@
<template>
<view class="example-page">
<view class="page-header">
<text class="page-title">订单详情</text>
</view>
<view class="order-info">
<view class="order-item">
<text class="item-name">鲜牛肉红烧牛肉面二两+例汤1份</text>
<text class="item-price">¥128</text>
</view>
<view class="order-item">
<text class="item-name">红油干拌鲜肉云吞+例汤1份</text>
<text class="item-price">¥50</text>
</view>
<view class="order-total">
<text class="total-label">总计</text>
<text class="total-price">¥178</text>
</view>
</view>
<view class="order-actions">
<button class="action-button refund-btn" @click="showRefundPopup">
申请退款
</button>
<button class="action-button contact-btn" @click="contactService">
联系客服
</button>
</view>
<!-- 退款弹窗组件 -->
<RefundPopup
v-model="refundVisible"
:refund-type="refundType"
:refund-amount="refundAmount"
@policy-click="viewRefundPolicy"
@confirm-click="handleRefundConfirm"
@close="handleRefundClose"
/>
</view>
</template>
<script setup>
import { ref } from 'vue'
import RefundPopup from './index.vue'
// 响应式数据
const refundVisible = ref(false)
const refundType = ref('partial_refund')
const refundAmount = ref(89) // 50% 退款
// 方法定义
const showRefundPopup = () => {
// 根据订单状态和时间判断退款类型
const orderTime = new Date('2024-01-15 10:00:00')
const currentTime = new Date()
const hoursDiff = (currentTime - orderTime) / (1000 * 60 * 60)
if (hoursDiff < 12) {
refundType.value = 'no_refund'
refundAmount.value = 0
} else if (hoursDiff < 24) {
refundType.value = 'partial_refund'
refundAmount.value = 89 // 50% 退款
} else if (hoursDiff < 48) {
refundType.value = 'free_cancel'
refundAmount.value = 178 // 全额退款
} else {
refundType.value = 'anytime_refund'
refundAmount.value = 178
}
refundVisible.value = true
}
const viewRefundPolicy = () => {
// 跳转到退款政策页面
uni.navigateTo({
url: '/pages/policy/refund'
})
}
const handleRefundConfirm = () => {
if (refundType.value === 'no_refund') {
uni.showToast({
title: '已了解退款政策',
icon: 'success'
})
return
}
// 提交退款申请
uni.showLoading({
title: '提交中...'
})
// 模拟API调用
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '退款申请已提交',
icon: 'success'
})
// 可以跳转到退款状态页面
// uni.navigateTo({
// url: '/pages/refund/status'
// })
}, 2000)
}
const handleRefundClose = () => {
console.log('退款弹窗已关闭')
}
const contactService = () => {
uni.showActionSheet({
itemList: ['在线客服', '电话客服', '意见反馈'],
success: (res) => {
switch (res.tapIndex) {
case 0:
// 打开在线客服
uni.navigateTo({
url: '/pages/service/chat'
})
break
case 1:
// 拨打客服电话
uni.makePhoneCall({
phoneNumber: '400-123-4567'
})
break
case 2:
// 打开意见反馈
uni.navigateTo({
url: '/pages/feedback/index'
})
break
}
}
})
}
</script>
<style lang="scss" scoped>
.example-page {
padding: 20px;
background: #f5f5f5;
min-height: 100vh;
}
.page-header {
text-align: center;
margin-bottom: 20px;
.page-title {
font-size: 18px;
font-weight: 600;
color: #333333;
}
}
.order-info {
background: #ffffff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.order-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-of-type {
border-bottom: none;
}
.item-name {
flex: 1;
font-size: 14px;
color: #333333;
line-height: 1.4;
}
.item-price {
font-size: 16px;
color: #ff6b35;
font-weight: 600;
}
}
.order-total {
display: flex;
justify-content: flex-end;
align-items: center;
padding-top: 12px;
margin-top: 12px;
border-top: 2px solid #f0f0f0;
.total-label {
font-size: 16px;
color: #333333;
font-weight: 600;
}
.total-price {
font-size: 20px;
color: #ff6b35;
font-weight: 700;
margin-left: 8px;
}
}
}
.order-actions {
display: flex;
gap: 16px;
.action-button {
flex: 1;
height: 48px;
border: none;
border-radius: 24px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
&.refund-btn {
background: #ff6b35;
color: #ffffff;
&:active {
background: #e55a2b;
transform: scale(0.98);
}
}
&.contact-btn {
background: #007aff;
color: #ffffff;
&:active {
background: #0056cc;
transform: scale(0.98);
}
}
}
}
// 响应式适配
@media screen and (max-width: 375px) {
.example-page {
padding: 16px;
}
.order-info {
padding: 16px;
margin-bottom: 16px;
}
.order-actions {
gap: 12px;
.action-button {
height: 44px;
font-size: 15px;
}
}
}
</style>

View File

@@ -0,0 +1,217 @@
<template>
<uni-popup ref="popupRef" type="center" @maskClick="handleClose">
<view class="refund-popup">
<!-- 头部卡通形象 -->
<image
src="@/static/dh.png"
class="refund-popup__avatar"
mode="widthFix"
/>
<!-- 内容区域 -->
<view class="refund-popup__content">
<!-- 主标题 -->
<view class="refund-popup__title">{{ currentTitle }}</view>
<!-- 金额显示 -->
<view class="refund-popup__amount" v-if="showAmount">
<text class="amount-symbol">¥</text>
<text class="amount-value">{{ refundAmount }}</text>
<text class="amount-unit"></text>
</view>
<view class="refund-popup__amount-label" v-if="showAmount"
>可退金额</view
>
<!-- 描述信息 -->
<view class="refund-popup__description" v-if="currentDescription">
{{ currentDescription }}
</view>
<!-- 退款政策详情 -->
<view class="refund-popup__policy" v-if="showPolicy">
<view class="policy-title">退订政策</view>
<view class="policy-list">
<view
class="policy-item"
v-for="(rule, index) in currentRules"
:key="index"
>
{{ rule }}
</view>
</view>
</view>
</view>
<!-- 按钮区域 -->
<view class="refund-popup__actions">
<view class="action-btn secondary-btn" @click="handlePolicyClick">
{{ leftButtonText }}
</view>
<view class="action-btn primary-btn" @click="handleConfirmClick">
{{ rightButtonText }}
</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, computed, watch, shallowRef } from "vue";
// Props定义
const props = defineProps({
// 弹窗显示状态
modelValue: {
type: Boolean,
default: false,
},
// 退款类型
refundType: {
type: String,
default: "no_refund",
validator: (value) =>
["no_refund", "all_refund", "anytime_refund"].includes(value),
},
// 可退金额
refundAmount: {
type: Number,
default: 0,
},
// 退款规则列表
refundRules: {
type: Array,
default: () => [],
},
// 自定义标题
title: {
type: String,
default: "",
},
// 自定义描述
description: {
type: String,
default: "",
},
});
// Events定义
const emit = defineEmits([
"update:modelValue",
"policy-click",
"confirm-click",
"close",
]);
// 弹窗引用
const popupRef = ref(null);
// 退款场景配置使用shallowRef优化性能
const refundScenarios = shallowRef({
no_refund: {
title: "您在入住日期12小时以内申请退款不可退款,如有疑问请咨询客服",
description: "",
showAmount: false,
showPolicy: false,
leftButton: "退订政策",
rightButton: "我知道了",
rules: [],
},
all_refund: {
title: "您在入住日期24小时内申请退款可退还100%金额",
description: "",
showAmount: true,
showPolicy: true,
leftButton: "退订政策",
rightButton: "点击退款",
rules: [],
},
anytime_refund: {
title: "该商品未使用随时可退",
description: "",
showAmount: true,
showPolicy: false,
leftButton: "退订政策",
rightButton: "点击退款",
rules: [],
},
});
// 计算属性
const currentScenario = computed(
() =>
refundScenarios.value[props.refundType] || refundScenarios.value.no_refund
);
const currentTitle = computed(() => props.title || currentScenario.value.title);
const currentDescription = computed(
() => props.description || currentScenario.value.description
);
const currentRules = computed(() => {
if (props.refundRules.length) {
return props.refundRules;
}
return currentScenario.value.rules;
});
const showAmount = computed(
() => currentScenario.value.showAmount && props.refundAmount > 0
);
const showPolicy = computed(
() => currentScenario.value.showPolicy && currentRules.value.length > 0
);
const leftButtonText = computed(() => currentScenario.value.leftButton);
const rightButtonText = computed(() => currentScenario.value.rightButton);
// 方法定义
const show = () => popupRef.value.open();
const hide = () => popupRef.value.close();
// 监听modelValue变化
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
show();
} else {
hide();
}
},
{ immediate: true }
);
// 监听退款金额变化,进行数据验证
watch(
() => props.refundAmount,
(newVal) => {
if (newVal < 0) {
console.warn("RefundPopup: 退款金额不能为负数");
}
},
{ immediate: true }
);
const handleClose = () => {
emit("update:modelValue", false);
emit("close");
};
const handlePolicyClick = () => {
emit("policy-click");
};
const handleConfirmClick = () => {
emit("confirm-click");
handleClose();
};
</script>
<style lang="scss" scoped>
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,168 @@
// RefundPopup 退款弹窗样式
.refund-popup {
width: 320px;
background: linear-gradient(173deg, #cbf6ff 3%, #ffffff 32%);
border-radius: 12px;
box-sizing: border-box;
padding-top: 64px;
position: relative;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
// 头部区域
&__avatar {
width: 132px;
height: 132px;
position: absolute;
top: -45px;
left: 50%;
transform: translateX(-50%);
}
// 内容区域
&__content {
padding: 12px 20px 20px;
text-align: center;
}
&__title {
font-size: 16px;
font-weight: 500;
color: #333;
line-height: 22px;
margin-bottom: 12px;
text-align: center;
}
&__amount {
display: flex;
justify-content: center;
align-items: baseline;
margin-bottom: 4px;
.amount-symbol {
font-size: 12px;
color: #ff6a00;
}
.amount-value {
font-size: 24px;
color: #ff6a00;
margin: 0 2px;
}
.amount-unit {
font-size: 12px;
color: #ff6a00;
}
}
&__amount-label {
font-size: 12px;
color: #333;
margin-bottom: 16px;
}
&__description {
font-size: 14px;
color: #333333;
line-height: 1.5;
margin-bottom: 16px;
text-align: left;
}
&__policy {
text-align: left;
margin-bottom: 16px;
.policy-title {
font-size: 14px;
color: #007aff;
font-weight: 600;
margin-bottom: 8px;
}
.policy-list {
.policy-item {
font-size: 12px;
color: #333333;
line-height: 22px;
text-align: justify;
&:last-child {
margin-bottom: 0;
}
}
}
}
// 按钮区域
&__actions {
display: flex;
gap: 12px;
padding: 0 20px 20px;
.action-btn {
flex: 1;
height: 44px;
border-radius: 22px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.3s ease;
outline: none;
&.secondary-btn {
background: #007aff;
color: #ffffff;
&:active {
background: #0056cc;
transform: scale(0.98);
}
}
&.primary-btn {
background: #ff9500;
color: #ffffff;
&:active {
background: #e6850e;
transform: scale(0.98);
}
}
}
}
}
// 动画效果
@keyframes popupFadeIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes flowerBounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-4px);
}
60% {
transform: translateY(-2px);
}
}
.refund-popup {
animation: popupFadeIn 0.3s ease-out;
}

View File

@@ -12,26 +12,41 @@
<GoodsInfo :orderData="orderData" />
<UserInfo :orderData="orderData" />
<NoticeInfo :orderData="orderData" />
<OrderInfo :orderData="orderData" />
<OrderInfo :orderData="orderData" @show-refund-popup="showRefundPopup" />
<!-- 退款状态显示 -->
<RefundPopup
v-model="refundVisible"
:refund-type="refundType"
:refund-amount="refundAmount"
@policy-click="viewRefundPolicy"
@confirm-click="handleRefundConfirm"
/>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { userOrderDetail } from "@/request/api/OrderApi";
import { userOrderDetail, orderRefund } from "@/request/api/OrderApi";
import OrderQrcode from "./components/OrderQrcode/index.vue";
import OrderStatusInfo from "./components/OrderStatusInfo/index.vue";
import GoodsInfo from "./components/GoodsInfo/index.vue";
import UserInfo from "./components/UserInfo/index.vue";
import NoticeInfo from "./components/NoticeInfo/index.vue";
import OrderInfo from "./components/OrderInfo/index.vue";
import RefundPopup from "./components/RefundPopup/index.vue";
const refundVisible = ref(false);
const refundType = ref("free_cancel"); // 默认退款类型
const refundAmount = ref(0); // 退款金额
const orderData = ref({});
onLoad(async ({ orderId }) => {
const res = await userOrderDetail({ orderId });
orderData.value = res.data;
// 设置退款金额为订单支付金额
refundAmount.value = parseFloat(res.data.payAmt || 0);
console.log(res);
});
@@ -41,6 +56,42 @@ const goBack = () => {
delta: 1,
});
};
// 显示退款弹窗
const showRefundPopup = () => {
refundVisible.value = true;
};
// 查看退款政策
const viewRefundPolicy = () => {
console.log("查看退款政策");
// 这里可以跳转到退款政策页面或显示详细政策
};
// 确认退款
const handleRefundConfirm = async () => {
try {
// 调用退款API
await orderRefund({ orderId: orderData.value.orderId });
uni.showToast({
title: "退款申请已提交",
icon: "success",
});
// 刷新订单状态
const res = await userOrderDetail({ orderId: orderData.value.orderId });
orderData.value = res.data;
// 更新退款金额
refundAmount.value = parseFloat(res.data.payAmt || 0);
} catch (error) {
console.error("退款失败:", error);
uni.showToast({
title: "退款申请失败,请重试",
icon: "none",
});
}
};
</script>
<style lang="scss" scoped>

BIN
static/dh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB