| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900 |
- <template>
- <view class="container">
- <!-- 退款商品选择 -->
- <view class="section-block product-section">
- <text class="section-title">选择退款商品</text>
- <view class="product-list">
- <view
- v-for="(product, index) in orderInfo.products"
- :key="index"
- :class="['product-item', { 'product-item-selected': selectedProducts.includes(index) }]"
- >
- <!-- 商品主体信息 -->
- <view class="product-main" @click="toggleProductSelect(index)">
- <view class="product-checkbox">
- <view :class="['checkbox-icon', { checked: selectedProducts.includes(index) }]">
- <text v-if="selectedProducts.includes(index)">✓</text>
- </view>
- </view>
- <view class="product-image">
- <image v-if="product.image" :src="product.image" mode="aspectFill"></image>
- <view v-else class="image-placeholder"></view>
- </view>
- <view class="product-detail">
- <view class="name-container" @click.stop="showProductName(product.name)">
- <text class="product-name">{{ product.name }}</text>
- <text v-if="product.name.length > 12" class="expand-tip">...</text>
- </view>
- <view class="product-meta">
- <text class="product-price">¥{{ (product.price || 0).toFixed(2) }}</text>
- <text class="product-quantity-label">购买数量:x{{ product.quantity }}</text>
- </view>
- </view>
- </view>
-
- <!-- 退款数量选择器(仅当商品被选中时显示,独立一行) -->
- <view v-if="selectedProducts.includes(index)" class="refund-quantity-selector">
- <view class="selector-row">
- <text class="selector-label">退款数量</text>
- <view class="quantity-control">
- <view
- :class="['control-btn', (product.refundQuantity || 1) > 1 ? '' : 'disabled']"
- @click.stop="decreaseQuantity(index)"
- >
- <text>-</text>
- </view>
- <view class="quantity-display">
- <text class="quantity-text">{{ product.refundQuantity || 1 }}</text>
- </view>
- <view
- :class="['control-btn', (product.refundQuantity || 1) < product.quantity ? '' : 'disabled']"
- @click.stop="increaseQuantity(index)"
- >
- <text>+</text>
- </view>
- </view>
- </view>
- <view class="refund-amount-preview">
- <text class="preview-label">退款金额</text>
- <text class="preview-value">¥{{ ((product.price || 0) * (product.refundQuantity || 1)).toFixed(2) }}</text>
- </view>
- </view>
- </view>
- </view>
- <view class="selected-tip">
- 已选择 {{ selectedProducts.length }} 件商品,共 {{ refundQuantity }} 件
- </view>
- </view>
-
- <!-- 退款原因 -->
- <view class="section-block reason-section">
- <text class="section-title">退款原因</text>
- <radio-group @change="handleReasonChange">
- <label class="radio-item" v-for="(reason, index) in refundReasons" :key="index">
- <view class="radio-wrapper">
- <radio :value="reason.value" :checked="selectedReason === reason.value" color="#999999" />
- </view>
- <text class="radio-label">{{ reason.label }}</text>
- </label>
- </radio-group>
-
- <!-- 其他原因的文本输入框 -->
- <view v-if="selectedReason === '4'" class="remark-input-wrapper">
- <textarea
- v-model="refundRemark"
- class="remark-textarea"
- placeholder="请详细描述您的问题或投诉内容"
- maxlength="100"
- :show-confirm-bar="false"
- />
- <text class="remark-count">{{ refundRemark.length }}/100</text>
- </view>
- </view>
-
- <!-- 退款信息 -->
- <view class="section-block info-section">
- <text class="section-title">退款信息</text>
- <view class="info-row">
- <text class="info-label">订单编号:</text>
- <text class="info-value">{{ orderInfo.orderNo || '-' }}</text>
- </view>
- <view class="info-row">
- <text class="info-label">订单总金额:</text>
- <text class="info-value">¥{{ (orderInfo.totalAmount || 0).toFixed(2) }}</text>
- </view>
- <view class="info-row">
- <text class="info-label">退款数量:</text>
- <text class="info-value">{{ refundQuantity }}</text>
- </view>
- <view class="info-row">
- <text class="info-label">退款总金额:</text>
- <text class="info-value refund-amount">¥{{ refundAmount.toFixed(2) }}</text>
- </view>
- <view class="info-row" v-if="orderInfo.payTime">
- <text class="info-label">退款期限:</text>
- <text :class="['info-value', isRefundExpired ? 'expired' : '']">{{ refundExpireText }}</text>
- </view>
- </view>
-
- <!-- 提示信息 -->
- <view class="section-block tip-section">
- <text class="tip-text">• 请选择需要退款的商品</text>
- <text class="tip-text">• 退款申请提交后,工作人员将尽快处理</text>
- <text class="tip-text">• 退款金额将原路返回至您的支付账户</text>
- </view>
-
- <!-- 确认提交按钮 -->
- <button class="submit-btn" @click="submitRefund">确认提交</button>
- </view>
- </template>
- <script setup lang="ts">
- import { ref, computed, onMounted } from 'vue';
- import { onLoad } from '@dcloudio/uni-app';
- import { checkAuth } from '../../utils/auth';
- import { getOrderDetail } from '../../api/order';
- import type { OrderInfo } from '../../api/order';
- import { post } from '../../utils/request';
- // 路由参数
- const orderId = ref<string>('');
- // 订单信息
- const orderInfo = ref<OrderInfo>({
- id: '',
- orderNo: '',
- outTradeNo: '',
- hahaOrderNo: '',
- deviceId: '',
- totalAmount: 0,
- payStatus: '',
- status: 0,
- createTime: '',
- products: []
- });
- // 选中的商品索引列表
- const selectedProducts = ref<number[]>([]);
- // 退款数量(根据选中商品的退款数量计算)
- const refundQuantity = computed(() => {
- return selectedProducts.value.reduce((sum, index) => {
- const product = orderInfo.value.products[index];
- return sum + (product.refundQuantity || 1);
- }, 0);
- });
- // 退款金额(根据选中商品的退款数量计算)
- const refundAmount = computed(() => {
- return selectedProducts.value.reduce((sum, index) => {
- const product = orderInfo.value.products[index];
- if (product) {
- return sum + (product.price || 0) * (product.refundQuantity || 1);
- }
- return sum;
- }, 0);
- });
- // 判断是否超过退款期限
- const isRefundExpired = computed(() => {
- if (!orderInfo.value.payTime) return false;
-
- const payTime = new Date(orderInfo.value.payTime);
- const now = new Date();
- const diffTime = now.getTime() - payTime.getTime();
- const diffDays = diffTime / (1000 * 60 * 60 * 24);
-
- return diffDays > 7;
- });
- // 退款期限提示文本
- const refundExpireText = computed(() => {
- if (!orderInfo.value.payTime) return '';
-
- const payTime = new Date(orderInfo.value.payTime);
- const now = new Date();
- const diffTime = now.getTime() - payTime.getTime();
- const diffDays = diffTime / (1000 * 60 * 60 * 24);
-
- if (diffDays > 7) {
- return '已超过退款期限';
- } else {
- const remainingDays = Math.ceil(7 - diffDays);
- return `剩余 ${remainingDays} 天`;
- }
- });
- const selectedReason = ref('');
- const refundRemark = ref('');
- const refundReasons = [
- { label: '订单扣款错误', value: '1' },
- { label: '扣款金额与实际金额不符', value: '2' },
- { label: '商品过期或变质', value: '3' },
- { label: '其他(请填写投诉内容)', value: '4' }
- ];
- const handleReasonChange = (e: any) => {
- selectedReason.value = e.detail.value;
- // 如果切换到非"其他"原因,清空备注内容
- if (selectedReason.value !== '4') {
- refundRemark.value = '';
- }
- };
- /**
- * 切换商品选中状态
- */
- const toggleProductSelect = (index: number) => {
- const selectedIndex = selectedProducts.value.indexOf(index);
- if (selectedIndex > -1) {
- // 取消选中:清除退款数量
- orderInfo.value.products[index].refundQuantity = undefined;
- selectedProducts.value.splice(selectedIndex, 1);
- } else {
- // 选中:初始化退款数量为 1
- if (!orderInfo.value.products[index].refundQuantity) {
- orderInfo.value.products[index].refundQuantity = 1;
- }
- selectedProducts.value.push(index);
- }
- };
- /**
- * 显示商品全称
- */
- const showProductName = (productName: string) => {
- uni.showModal({
- title: '商品名称',
- content: productName,
- showCancel: false,
- confirmText: '知道了'
- });
- };
- /**
- * 减少退款数量
- */
- const decreaseQuantity = (index: number) => {
- const product = orderInfo.value.products[index];
- if (product.refundQuantity && product.refundQuantity > 1) {
- product.refundQuantity--;
- }
- };
- /**
- * 增加退款数量
- */
- const increaseQuantity = (index: number) => {
- const product = orderInfo.value.products[index];
- if ((product.refundQuantity || 1) < product.quantity) {
- if (!product.refundQuantity) {
- product.refundQuantity = 2;
- } else {
- product.refundQuantity++;
- }
- }
- };
- /**
- * 显示商品详情
- */
- const showProductDetail = () => {
- // TODO: 跳转到商品详情页或弹出商品详情弹窗
- uni.showToast({
- title: '商品详情功能开发中',
- icon: 'none'
- });
- };
- /**
- * 加载订单详情
- */
- const loadOrderDetail = async () => {
- if (!orderId.value) {
- uni.showToast({
- title: '订单 ID 缺失',
- icon: 'none'
- });
- setTimeout(() => {
- uni.navigateBack();
- }, 1500);
- return;
- }
- try {
- const detail = await getOrderDetail({ orderId: orderId.value });
- orderInfo.value = detail;
-
- // 检查订单是否超过退款期限(7天)
- if (detail.payTime) {
- const payTime = new Date(detail.payTime);
- const now = new Date();
- const diffTime = now.getTime() - payTime.getTime();
- const diffDays = diffTime / (1000 * 60 * 60 * 24);
-
- if (diffDays > 7) {
- uni.showModal({
- title: '退款期限已过',
- content: '该订单已完成超过7天,不支持申请退款',
- showCancel: false,
- confirmText: '知道了',
- success: () => {
- uni.navigateBack();
- }
- });
- return;
- }
- }
-
- // 默认不选中任何商品(移除之前的默认全选逻辑)
- selectedProducts.value = [];
- } catch (error) {
- console.error('加载订单详情失败:', error);
- uni.showToast({
- title: '加载订单详情失败',
- icon: 'none'
- });
- setTimeout(() => {
- uni.navigateBack();
- }, 1500);
- }
- };
- /**
- * 页面加载时检查登录状态并加载订单详情
- */
- onMounted(() => {
- // 检查登录状态,未登录会自动跳转到登录页
- checkAuth();
- });
- /**
- * 监听页面加载参数
- */
- onLoad((options) => {
- if (options && options.orderId) {
- // 直接使用字符串类型的订单ID,避免精度丢失
- orderId.value = String(options.orderId);
- loadOrderDetail();
- } else {
- uni.showToast({
- title: '订单 ID 缺失',
- icon: 'none'
- });
- setTimeout(() => {
- uni.navigateBack();
- }, 1500);
- }
- });
- /**
- * 提交退款申请
- */
- const submitRefund = async () => {
- // 校验是否选择了商品
- if (selectedProducts.value.length === 0) {
- uni.showToast({
- title: '请选择要退款的商品',
- icon: 'none'
- });
- return;
- }
- // 校验退款原因
- if (!selectedReason.value) {
- uni.showToast({
- title: '请选择退款原因',
- icon: 'none'
- });
- return;
- }
- // 如果选择"其他"原因,校验备注内容
- if (selectedReason.value === '4' && !refundRemark.value.trim()) {
- uni.showToast({
- title: '请填写投诉内容',
- icon: 'none'
- });
- return;
- }
- // 映射前端退款原因到后端描述
- const reasonMap: Record<string, string> = {
- '1': '订单扣款错误',
- '2': '扣款金额与实际金额不符',
- '3': '商品过期或变质',
- '4': '其他'
- };
- let reasonText = reasonMap[selectedReason.value] || '用户申请退款';
-
- // 如果是"其他"原因,将备注内容附加到原因后面
- if (selectedReason.value === '4' && refundRemark.value.trim()) {
- reasonText = `其他:${refundRemark.value.trim()}`;
- }
- // 构建选中的商品信息(包含退款数量)
- const selectedProductIds = selectedProducts.value.map(index => {
- const product = orderInfo.value.products[index];
- return {
- productId: product.id,
- productName: product.name,
- quantity: product.refundQuantity || 1, // 使用用户选择的退款数量
- price: product.price
- };
- });
- try {
- // 调用后端退款 API
- const result = await post('/payment/refund', {
- orderId: orderId.value,
- reason: reasonText,
- products: selectedProductIds // 传递选中的商品信息
- });
- uni.showToast({
- title: '退款申请成功',
- icon: 'success'
- });
- setTimeout(() => {
- uni.navigateBack();
- }, 1500);
- } catch (error: any) {
- console.error('退款申请失败:', error);
- // 错误提示已在 request 拦截器中处理
- }
- };
- </script>
- <style>
- .container {
- min-height: 100vh;
- background-color: #f5f5f5;
- padding-top: 20rpx;
- padding-bottom: 40rpx;
- }
- /* 商品选择区域 */
- .product-section {
- margin-bottom: 20rpx;
- padding-bottom: 20rpx;
- }
- .product-list {
- display: flex;
- flex-direction: column;
- gap: 20rpx;
- }
- .product-item {
- display: flex;
- flex-direction: column;
- background-color: #ffffff;
- border-radius: 12rpx;
- padding: 24rpx;
- border: 2rpx solid transparent;
- transition: all 0.3s ease;
- }
- .product-item-selected {
- border-color: #07c160;
- background-color: #f6ffed;
- }
- .product-item:active {
- background-color: #fafafa;
- }
- .product-main {
- display: flex;
- align-items: center;
- width: 100%;
- }
- .product-checkbox {
- margin-right: 20rpx;
- flex-shrink: 0;
- }
- .checkbox-icon {
- width: 44rpx;
- height: 44rpx;
- border-radius: 50%;
- border: 3rpx solid #dddddd;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s ease;
- }
- .checkbox-icon.checked {
- background-color: #07c160;
- border-color: #07c160;
- box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
- }
- .checkbox-icon text {
- color: #ffffff;
- font-size: 26rpx;
- font-weight: bold;
- }
- .product-image {
- width: 140rpx;
- height: 140rpx;
- border-radius: 12rpx;
- overflow: hidden;
- flex-shrink: 0;
- margin-right: 20rpx;
- background-color: #f5f5f5;
- box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
- }
- .product-image image {
- width: 100%;
- height: 100%;
- }
- .image-placeholder {
- width: 100%;
- height: 100%;
- background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%);
- display: flex;
- align-items: center;
- justify-content: center;
- color: #999999;
- font-size: 40rpx;
- }
- .product-detail {
- flex: 1;
- display: flex;
- flex-direction: column;
- justify-content: center;
- min-width: 0;
- gap: 8rpx;
- }
- .name-container {
- position: relative;
- display: flex;
- align-items: center;
- cursor: pointer;
- max-width: 100%;
- }
- .product-name {
- font-size: 28rpx;
- color: #333333;
- line-height: 1.4;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- flex: 1;
- min-width: 0;
- }
- .expand-tip {
- font-size: 26rpx;
- color: #1890ff;
- margin-left: 8rpx;
- flex-shrink: 0;
- padding: 4rpx 12rpx;
- background-color: #e6f7ff;
- border-radius: 6rpx;
- }
- .product-meta {
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 8rpx;
- }
- .product-price {
- font-size: 32rpx;
- color: #ff4400;
- font-weight: 600;
- }
- .product-quantity {
- font-size: 24rpx;
- color: #999999;
- }
- .product-quantity-label {
- font-size: 24rpx;
- color: #999999;
- background-color: #f5f5f5;
- padding: 4rpx 12rpx;
- border-radius: 6rpx;
- }
- /* 退款数量选择器 */
- .refund-quantity-selector {
- margin-top: 20rpx;
- margin-left: 56rpx;
- padding: 20rpx 24rpx;
- background-color: #fafffe;
- border-radius: 12rpx;
- border: 1rpx solid #d9f7be;
- }
- .selector-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .selector-label {
- font-size: 28rpx;
- color: #52c41a;
- font-weight: 500;
- flex-shrink: 0;
- }
- .quantity-control {
- display: flex;
- align-items: center;
- gap: 16rpx;
- }
- .control-btn {
- width: 60rpx;
- height: 60rpx;
- border-radius: 12rpx;
- background-color: #ffffff;
- border: 2rpx solid #e0e0e0;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 36rpx;
- color: #333333;
- transition: all 0.2s;
- }
- .control-btn:active {
- background-color: #f0f0f0;
- transform: scale(0.95);
- }
- .control-btn.disabled {
- background-color: #fafafa;
- border-color: #f0f0f0;
- color: #cccccc;
- cursor: not-allowed;
- }
- .control-btn.disabled:active {
- background-color: #fafafa;
- transform: none;
- }
- .quantity-display {
- min-width: 80rpx;
- height: 60rpx;
- text-align: center;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: #ffffff;
- border: 2rpx solid #07c160;
- border-radius: 12rpx;
- padding: 0 16rpx;
- }
- .quantity-text {
- font-size: 32rpx;
- color: #07c160;
- font-weight: 600;
- }
- /* 退款金额预览 */
- .refund-amount-preview {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-top: 16rpx;
- padding-top: 16rpx;
- border-top: 1rpx dashed #d9f7be;
- }
- .preview-label {
- font-size: 26rpx;
- color: #999999;
- }
- .preview-value {
- font-size: 30rpx;
- color: #ff4400;
- font-weight: 600;
- }
- /* 退款商品选择器 */
- .section-item {
- background-color: #ffffff;
- padding: 28rpx 30rpx;
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 20rpx;
- }
- .section-label {
- font-size: 30rpx;
- color: #333333;
- }
- .arrow-right {
- font-size: 32rpx;
- color: #cccccc;
- }
- /* 区域块 */
- .section-block {
- background-color: #ffffff;
- padding: 30rpx;
- margin-bottom: 20rpx;
- }
- .section-title {
- font-size: 30rpx;
- color: #333333;
- font-weight: 500;
- margin-bottom: 30rpx;
- display: block;
- }
- /* 选项列表 */
- radio-group {
- display: block;
- }
- .radio-item {
- display: flex;
- align-items: center;
- margin-bottom: 32rpx;
- }
- .radio-item:last-child {
- margin-bottom: 0;
- }
- .radio-wrapper {
- margin-right: 16rpx;
- display: flex;
- align-items: center;
- }
- .radio-wrapper radio {
- transform: scale(1.1);
- }
- .radio-label {
- font-size: 28rpx;
- color: #333333;
- line-height: 1.5;
- }
- /* 备注输入框 */
- .remark-input-wrapper {
- margin-top: 24rpx;
- padding: 20rpx;
- background-color: #f9f9f9;
- border-radius: 12rpx;
- border: 2rpx solid #e8e8e8;
- }
- .remark-textarea {
- width: 100%;
- min-height: 160rpx;
- padding: 20rpx;
- background-color: #ffffff;
- border-radius: 8rpx;
- border: 2rpx solid #e0e0e0;
- font-size: 28rpx;
- color: #333333;
- line-height: 1.6;
- box-sizing: border-box;
- }
- .remark-textarea:focus {
- border-color: #1890ff;
- }
- .remark-count {
- display: block;
- text-align: right;
- font-size: 24rpx;
- color: #999999;
- margin-top: 12rpx;
- }
- /* 信息区域 */
- .info-section {
- padding-bottom: 20rpx;
- }
- .info-row {
- display: flex;
- align-items: center;
- padding: 18rpx 0;
- }
- .info-label {
- font-size: 28rpx;
- color: #999999;
- min-width: 200rpx;
- }
- .info-value {
- font-size: 28rpx;
- color: #333333;
- flex: 1;
- text-align: right;
- }
- .info-value.expired {
- color: #ff4444;
- font-weight: 600;
- }
- /* 提交按钮 */
- .submit-btn {
- margin: 40rpx 30rpx;
- background-color: #f8f8f8;
- border: 1rpx solid #e8e8e8;
- border-radius: 10rpx;
- font-size: 32rpx;
- color: #333333;
- padding: 24rpx 0;
- line-height: 1;
- font-weight: 500;
- }
- .submit-btn::after {
- border: none;
- }
- /* 商品信息 */
- .product-info {
- display: flex;
- align-items: center;
- gap: 16rpx;
- }
- .refund-amount {
- color: #ff4400;
- font-weight: 500;
- }
- /* 提示信息 */
- .tip-section {
- background-color: #fffbea;
- border: 1rpx solid #ffe58f;
- border-radius: 8rpx;
- padding: 24rpx 30rpx;
- margin: 30rpx;
- }
- .tip-text {
- display: block;
- font-size: 24rpx;
- color: #faad14;
- line-height: 1.8;
- }
- .selected-tip {
- text-align: right;
- font-size: 26rpx;
- color: #07c160;
- margin-top: 20rpx;
- padding: 16rpx 20rpx;
- background-color: #f6ffed;
- border-radius: 8rpx;
- font-weight: 500;
- }
- </style>
|