Просмотр исходного кода

feat: 新增申请售后页面并优化品相条件显示逻辑

新增申请售后页面,支持退货退款和仅退款两种类型
将品相条件映射提取为公共常量,减少重复代码
优化投诉页面,使用公共图片上传组件并支持多商品展示
调整退款详情页的客服按钮位置和样式
为订单详情和售后相关接口添加API支持
ylong 1 день назад
Родитель
Сommit
8abc715017

+ 6 - 0
api/modules/mall.js

@@ -41,6 +41,9 @@ export const useMallApi = (Vue, vm) => {
         // 图书商品详情页面
         getBookDetailAjax: (params) => vm.$u.get('/token/shop/bookDetail', params),
 
+        // 订单详情
+        getShopOrderDetailAjax: (params) => vm.$u.get('/token/shop/order/getOrderDetail', params),
+
         // 订单支付
         payShopOrderAjax: (data) => vm.$u.post('/token/shop/order/orderPay', data),
 
@@ -52,5 +55,8 @@ export const useMallApi = (Vue, vm) => {
 
 		// 根据专题id获取专题下的图书 (带分页)
 		getBookListByCateIdAjax: (params) => vm.$u.get('/token/shop/showIndex/getBookListByCateId', params),
+		
+		// 申请退款
+		applyRefundAjax: (data) => vm.$u.post('/token/shop/order/applyRefund', data),
 	}
 }

+ 198 - 0
components/image-upload.vue

@@ -0,0 +1,198 @@
+<template>
+	<view class="image-upload-container">
+		<view class="upload-list">
+			<view class="upload-item" v-for="(item, index) in value" :key="index">
+				<u-image :src="item" width="160rpx" height="160rpx" border-radius="8"
+					@click="onPreview(index)"></u-image>
+				<view class="delete-btn" @click.stop="onDelete(index)">
+					<u-icon name="close" color="#ffffff" size="20"></u-icon>
+				</view>
+			</view>
+			<view class="upload-btn" v-if="value.length < maxCount" @click="onChoose"
+				:style="{ backgroundColor: bgColor }">
+				<u-icon name="camera" size="48" color="#909399"></u-icon>
+				<text class="btn-text">图片/视频</text>
+			</view>
+		</view>
+		<view class="upload-tip" v-if="tip">
+			<u-icon name="info-circle" size="24" color="#909399"></u-icon>
+			<text class="tip-text">{{ tip }}</text>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'CommonImageUpload',
+		props: {
+			value: {
+				type: Array,
+				default: () => []
+			},
+			maxCount: {
+				type: Number,
+				default: 9
+			},
+			bgColor: {
+				type: String,
+				default: '#f4f5f6'
+			},
+			uploadUrl: {
+				type: String,
+				required: true
+			},
+			header: {
+				type: Object,
+				default: () => ({})
+			},
+			name: {
+				type: String,
+				default: 'file'
+			},
+			tip: {
+				type: String,
+				default: ''
+			}
+		},
+		methods: {
+			onChoose() {
+				uni.chooseImage({
+					count: this.maxCount - this.value.length,
+					success: async (res) => {
+						uni.showLoading({
+							title: '上传中...'
+						});
+						const tempFilePaths = res.tempFilePaths;
+						const uploadPromises = tempFilePaths.map(path => {
+							return this.uploadFileAjax(path);
+						});
+
+						try {
+							const results = await Promise.all(uploadPromises);
+							const newList = [...this.value, ...results];
+							this.$emit('input', newList);
+							uni.hideLoading();
+						} catch (e) {
+							uni.hideLoading();
+							uni.showToast({
+								title: e.msg || '上传失败',
+								icon: 'none'
+							});
+							console.error(e);
+						}
+					}
+				});
+			},
+			uploadFileAjax(filePath) {
+				return new Promise((resolve, reject) => {
+					// 合并 header,确保 token 存在
+					const header = {
+						'Authorization': uni.getStorageSync('token') || '',
+						...this.header
+					};
+
+					// 处理 URL,如果传入的是完整 URL 则直接使用,否则拼接 baseUrl
+					let url = this.uploadUrl;
+					if (!url.startsWith('http')) {
+						url = uni.$u.http.config.baseUrl + url;
+					}
+
+					uni.uploadFile({
+						url: url,
+						filePath: filePath,
+						name: this.name,
+						header: header,
+						success: (res) => {
+							try {
+								const data = JSON.parse(res.data);
+								if (data.code == 200) {
+									resolve(data.data);
+								} else {
+									reject(data);
+								}
+							} catch (e) {
+								reject({ msg: '解析失败' });
+							}
+						},
+						fail: (err) => {
+							reject(err);
+						}
+					});
+				});
+			},
+
+			onDelete(index) {
+				const newList = [...this.value];
+				newList.splice(index, 1);
+				this.$emit('input', newList);
+			},
+			onPreview(index) {
+				uni.previewImage({
+					urls: this.value,
+					current: index
+				});
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.image-upload-container {
+		width: 100%;
+	}
+
+	.upload-list {
+		display: flex;
+		flex-wrap: wrap;
+		margin: 0 -10rpx;
+	}
+
+	.upload-item,
+	.upload-btn {
+		width: 160rpx;
+		height: 160rpx;
+		margin: 10rpx;
+		border-radius: 8rpx;
+		position: relative;
+	}
+
+	.upload-btn {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+
+		.btn-text {
+			font-size: 24rpx;
+			color: #909399;
+			margin-top: 10rpx;
+		}
+	}
+
+	.delete-btn {
+		position: absolute;
+		top: 0;
+		right: 0;
+		width: 40rpx;
+		height: 40rpx;
+		background-color: rgba(0, 0, 0, 0.5);
+		border-bottom-left-radius: 8rpx;
+		border-top-right-radius: 8rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		z-index: 1;
+	}
+
+	.upload-tip {
+		margin-top: 20rpx;
+		display: flex;
+		align-items: center;
+
+		.tip-text {
+			font-size: 24rpx;
+			color: #909399;
+			margin-left: 10rpx;
+		}
+	}
+</style>

+ 7 - 0
main.js

@@ -29,6 +29,13 @@ import {
 	replaceSale
 } from '@/utils/replace.js'
 Vue.prototype.$replaceSale = replaceSale
+import {
+	CONDITION_MAP,
+	getConditionText
+} from '@/utils/constants.js'
+Vue.filter('conditionText', getConditionText)
+Vue.prototype.$conditionMap = CONDITION_MAP
+
 import {
 	copyByUniappApi
 } from '@/utils/uniapp-api.js';

+ 4 - 9
pages-car/components/buy-order-item.vue

@@ -27,7 +27,7 @@
                         </view>
                     </view>
                     <view class="book-sku" v-if="order.bookConditionType">
-                        品相:{{ qualityNames[order.bookConditionType] || '未知' }}
+                        品相:{{ order.bookConditionType | conditionText }}
                     </view>
                 </view>
             </view>
@@ -74,7 +74,9 @@
                 :custom-style="btnStyle" @click.stop="handleAction('remind')">催发货</u-button>
 
             <!-- 更多操作 (仅在已完成状态下显示) -->
-            <u-button v-if="order.status == '4'" size="mini" shape="circle" plain :custom-style="btnStyle"
+            <u-button
+                v-if="order.status == '4' && (order.showAfterSales == 1 || order.showCheckExpress == 1 || order.showApplyInvoice == 1)"
+                size="mini" shape="circle" plain :custom-style="btnStyle"
                 @click.stop="handleMoreAction">更多</u-button>
 
             <!-- 申请售后 -->
@@ -122,13 +124,6 @@
         },
         data() {
             return {
-                //1-良好 2-中等 3-次品 4-全新
-                qualityNames: {
-                    '1': '良好',
-                    '2': '中等',
-                    '3': '次品',
-                    '4': '全新',
-                },
                 defaultCover: 'https://uviewui.com/album/1.jpg',
                 btnStyle: {
                     marginLeft: '20rpx',

+ 1 - 7
pages-car/components/cart-item.vue

@@ -71,18 +71,12 @@
         },
         data() {
             return {
-                conditionMap: {
-                    1: '良好',
-                    2: '中等',
-                    3: '次品',
-                    4: '全新'
-                }
             };
         },
         computed: {
             conditionName() {
                 // 如果是数字,映射;如果是字符串,直接显示
-                return this.conditionMap[this.item.conditionType] || this.item.conditionType || '中等';
+                return this.$conditionMap[this.item.conditionType] || this.item.conditionType || '-';
             },
             isValid() {
                 // stockStatus: 1-有库存 2-库存紧张 3-暂无库存

+ 2 - 8
pages-car/components/condition-popup.vue

@@ -46,13 +46,7 @@
 			return {
 				visible: false,
 				currentCondition: null,
-				skuList: [],
-				conditionMap: {
-					1: '良好',
-					2: '中等',
-					3: '次品',
-					4: '全新'
-				}
+				skuList: []
 			};
 		},
 		methods: {
@@ -66,7 +60,7 @@
 				this.visible = false;
 			},
 			getConditionName(type) {
-				return this.conditionMap[type] || '未知';
+				return this.$conditionMap[type] || '-';
 			},
 			isStockEmpty(sku) {
 				return !sku.stockNum || sku.stockNum <= 0;

+ 831 - 0
pages-car/pages/apply-refund.vue

@@ -0,0 +1,831 @@
+<template>
+	<view class="apply-refund-page">
+		<!-- 退款商品 -->
+		<view class="card">
+			<view class="type-section">
+				<view class="section-title">请选择售后类型</view>
+				<view class="type-btn-group">
+					<view class="type-btn" :class="{ active: refundType === '1' }"
+						@click="confirmType([{ value: '1', label: '退货退款' }])">
+						<text>退货退款</text>
+					</view>
+					<view class="type-btn" :class="{ active: refundType === '2' }"
+						@click="confirmType([{ value: '2', label: '仅退款' }])">
+						<text>仅退款</text>
+					</view>
+				</view>
+			</view>
+
+			<view class="card-header">
+				<text class="card-title">退款商品</text>
+			</view>
+			<view class="goods-list">
+				<view v-for="(item, index) in orderInfo.detailVoList" :key="index" class="goods-item">
+					<view class="checkbox-box" @click.stop="toggleCheck(item)">
+						<u-icon v-if="item.checked" name="checkmark-circle-fill" color="#38C148" size="44"></u-icon>
+						<u-icon v-else name="checkmark-circle" color="#ccc" size="44"></u-icon>
+					</view>
+					<image :src="item.cover" mode="aspectFill" class="goods-cover"></image>
+					<view class="goods-info">
+						<view class="goods-name u-line-2">{{ item.bookName }}</view>
+						<view class="goods-sku" v-if="item.isbn">ISBN: {{ item.isbn }}</view>
+						<view class="price-box">
+							<text class="price">¥{{ item.payPrice }}</text>
+							<u-number-box v-model="item.refundNum" :min="1" :max="item.num"
+								@change="calculateRefundMoney"></u-number-box>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+
+		<!-- 退款原因配置 -->
+		<view class="card no-padding">
+			<u-cell-group :border="false">
+				<!-- 退款原因 -->
+				<u-cell-item title="退款原因" :value="refundReasonText || '请选择'" @click="showReasonPicker = true" required
+					:border-bottom="refundType === '2' ? false : true"></u-cell-item>
+
+				<!-- 货物状态 (仅退款显示) -->
+				<u-cell-item v-if="refundType === '1'" title="货物状态" :value="shopStatusText"
+					@click="showStatusPicker = true" required :border-bottom="false"></u-cell-item>
+			</u-cell-group>
+		</view>
+
+		<!-- 退款金额与凭证 -->
+		<view class="card no-padding">
+			<view class="card-title" style="padding:30rpx 0 0 20rpx">退款金额:</view>
+			<u-cell-group :border="false">
+				<!-- 退回支付渠道 -->
+				<u-cell-item :title="refundChannelText" :arrow="false">
+					<view slot="right-icon" class="refund-money-box" @click="showRefundAmountPopup = true">
+						<text class="money">¥{{ refundMoney }}</text>
+						<view class="edit-tag">
+							<u-icon name="edit-pen" size="24"></u-icon>
+							<text>修改</text>
+						</view>
+					</view>
+				</u-cell-item>
+				<!-- 上传描述和凭证 -->
+				<u-cell-item title="上传描述和凭证" :value="uploadStatusText" @click="showUploadPopup = true" is-link
+					:border-bottom="false"></u-cell-item>
+			</u-cell-group>
+		</view>
+
+		<!-- 退货方式 (仅退货退款显示) - 单独分区 -->
+		<view class="card no-padding" v-if="refundType === '1'">
+			<u-cell-group :border="false">
+				<u-cell-item title="退货方式" :value="returnMethodText || '请选择'" @click="showReturnMethodPicker = true"
+					required></u-cell-item>
+
+				<!-- 上门取件地址 (仅上门取件显示) -->
+				<view class="address-section" @click="chooseAddress">
+					<view class="flex-a flex-j-b mb-20" v-if="address.name">
+						<view class="address-label">我的地址</view>
+						<image src="/pages-mine/static/adderss.png" style="width: 40rpx; height: 40rpx"></image>
+						<view class="flex-d flex-1 ml-24" style="margin-right: 20rpx">
+							<view class="flex-a flex-j-b mb-10">
+								<view class="address-text">{{ address.name }}</view>
+								<view class="address-text">{{ address.mobile }}</view>
+							</view>
+							<view class="address-text">{{ address.province || '' }}{{ address.city || '' }}{{
+								address.area || '' }}{{ address.fullAddress }}</view>
+						</view>
+						<u-icon name="arrow-right" :size="28" color="#666" top="4"></u-icon>
+					</view>
+
+					<view class="flex-a flex-j-b" v-else>
+						<view class="flex-a">
+							<u-icon name="plus-circle-fill" :size="48" color="#38C148" top="2"></u-icon>
+							<view class="ml-10 font-30 address-text">我的地址</view>
+							<text class="u-required">*</text>
+						</view>
+						<view class="flex-a">
+							<view class="ml-10 address-text">请添加</view>
+							<u-icon name="arrow-right" :size="28" color="#666" top="4"></u-icon>
+						</view>
+					</view>
+				</view>
+			</u-cell-group>
+		</view>
+
+
+		<!-- 我的服务 - 单独分区 -->
+		<view class="card no-padding">
+			<view class="card-title" style="padding:30rpx 0 0 20rpx">我的服务</view>
+			<u-cell-group :border="false">
+				<u-cell-item title="退换无忧" value="服务生效中" :arrow="true" :border-bottom="false">
+					<u-icon slot="icon" name="checkmark-circle-fill" color="#38C148" size="32"
+						style="margin-right: 10rpx;"></u-icon>
+				</u-cell-item>
+			</u-cell-group>
+		</view>
+
+		<!-- 底部占位 -->
+		<view style="height: 60rpx;"></view>
+
+		<!-- 底部固定栏 -->
+		<view class="footer-bar">
+			<view class="footer-left" @click="showRefundAmountPopup = true">
+				<text class="label">{{ refundChannelText }}:</text>
+				<text class="amount">¥{{ refundMoney }}</text>
+				<text class="detail-link">明细</text>
+			</view>
+			<view class="footer-right">
+				<u-button type="primary" shape="circle" :custom-style="submitBtnStyle" @click="submit">提交申请</u-button>
+			</view>
+		</view>
+
+		<!-- 弹窗: 退款原因 -->
+		<u-select v-model="showReasonPicker" :list="reasonList" @confirm="confirmReason"></u-select>
+		<!-- 弹窗: 货物状态 -->
+		<u-select v-model="showStatusPicker" :list="statusList" @confirm="confirmStatus"></u-select>
+		<!-- 弹窗: 退货方式 -->
+		<u-select v-model="showReturnMethodPicker" :list="returnMethodList" :default-value="returnMethodIndex"
+			@confirm="confirmReturnMethod"></u-select>
+
+		<!-- 弹窗: 上传描述和凭证 -->
+		<u-popup v-model="showUploadPopup" mode="bottom" border-radius="24" height="800">
+			<view class="popup-container full-height">
+				<view class="popup-header">
+					<text class="title">上传描述和凭证</text>
+					<u-icon name="close" size="32" color="#999" @click="showUploadPopup = false"></u-icon>
+				</view>
+				<view class="popup-content">
+					<view class="upload-textarea-box">
+						<u-input v-model="description" type="textarea" placeholder="补充描述,有助于平台更好的处理售后问题" :height="100"
+							maxlength="200" />
+					</view>
+					<view class="upload-area">
+						<u-upload ref="uUpload" :action="uploadAction" :max-count="5" :header="uploadHeader" name="file"
+							:file-list="fileList" @on-list-change="handleUploadChange" width="160" height="160">
+						</u-upload>
+					</view>
+				</view>
+				<view class="popup-footer safe-area-bottom">
+					<u-button type="primary" shape="circle" :custom-style="submitBtnStyle"
+						@click="showUploadPopup = false">完成</u-button>
+				</view>
+			</view>
+		</u-popup>
+
+		<!-- 弹窗: 退款明细 -->
+		<u-popup v-model="showRefundAmountPopup" mode="bottom" border-radius="24">
+			<view class="popup-container">
+				<view class="popup-header">
+					<text class="title">退款金额明细</text>
+					<u-icon name="close" size="32" color="#999" @click="showRefundAmountPopup = false"></u-icon>
+				</view>
+				<view class="popup-content">
+					<view class="detail-item">
+						<view class="item-row">
+							<u-icon name="rmb-circle" size="36" color="#666"></u-icon>
+							<text class="item-label">{{ refundChannelText }}</text>
+							<text class="item-value">¥{{ refundMoney }}</text>
+						</view>
+					</view>
+					<!-- 如果有红包/优惠,展示在这里 -->
+					<view class="detail-item" v-if="otherRefundAmount > 0">
+						<view class="item-row">
+							<u-icon name="red-packet" size="36" color="#666"></u-icon>
+							<text class="item-label">退回其他</text>
+						</view>
+						<view class="sub-list">
+							<view class="sub-item" v-if="orderInfo.discountMoney > 0">
+								<text class="sub-label">优惠金额</text>
+								<text class="sub-value">¥{{ orderInfo.discountMoney }}</text>
+							</view>
+							<!-- 预留红包字段 -->
+							<view class="sub-item" v-if="orderInfo.redPacketMoney > 0">
+								<text class="sub-label">红包</text>
+								<text class="sub-value">¥{{ orderInfo.redPacketMoney }}</text>
+							</view>
+						</view>
+					</view>
+				</view>
+				<view class="popup-footer safe-area-bottom">
+					<u-button type="primary" shape="circle" :custom-style="submitBtnStyle"
+						@click="showRefundAmountPopup = false">我知道了</u-button>
+				</view>
+			</view>
+		</u-popup>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				orderId: '',
+				orderInfo: {
+					detailVoList: [],
+					discountMoney: 0,
+					redPacketMoney: 0 // 假设有
+				},
+
+				// 表单数据
+				refundType: '1', // 1: 退货退款, 2: 仅退款
+				refundTypeText: '退货退款',
+
+				refundReason: '',
+				refundReasonText: '',
+
+				shopStatus: '2', // 1: 未收到货, 2: 已收到货
+				shopStatusText: '已收到货',
+
+				returnMethod: '3', // 1: 上门取件, 2: 寄件点自寄, 3: 自行寄回
+				returnMethodText: '自行寄回',
+
+				description: '',
+				fileList: [], // 用于回显和同步状态
+
+				address: {}, // 上门取件地址
+
+				// 辅助数据
+				showReasonPicker: false,
+				showStatusPicker: false,
+				showReturnMethodPicker: false,
+				showRefundAmountPopup: false,
+				showUploadPopup: false, // 上传弹窗
+
+				reasonList: [],
+				statusList: [
+					{ value: '1', label: '未收到货' },
+					{ value: '2', label: '已收到货' }
+				],
+				returnMethodList: [
+					{ value: '1', label: '上门取件' },
+					{ value: '2', label: '寄件点自寄' },
+					{ value: '3', label: '自行寄回' }
+				],
+
+				uploadAction: uni.$u.http.config.baseUrl + '/token/shop/feedback/fileUpload',
+				uploadHeader: {
+					'Authorization': uni.getStorageSync('token') || ''
+				},
+				submitBtnStyle: {
+					width: '100%',
+					height: '80rpx',
+					fontSize: '30rpx',
+					backgroundColor: '#38C148',
+					color: '#ffffff'
+				}
+			};
+		},
+		computed: {
+			returnMethodIndex() {
+				const index = this.returnMethodList.findIndex(item => item.value === this.returnMethod);
+				return index > -1 ? [index] : [2];
+			},
+			selectedGoods() {
+				return this.orderInfo.detailVoList.filter(item => item.checked);
+			},
+			maxRefundMoney() {
+				if (!this.orderInfo.detailVoList || this.orderInfo.detailVoList.length === 0) return '0.00';
+
+				let totalOriginal = 0;
+				this.orderInfo.detailVoList.forEach(item => {
+					totalOriginal += Number(item.payPrice) * item.num;
+				});
+
+				if (totalOriginal === 0) return '0.00';
+
+				let selectedOriginal = 0;
+				this.selectedGoods.forEach(item => {
+					selectedOriginal += Number(item.payPrice) * item.refundNum;
+				});
+
+				let payMoney = Number(this.orderInfo.payMoney) || 0;
+				// 按比例计算: (选中商品原价 / 订单总原价) * 实付金额
+				let refund = (selectedOriginal / totalOriginal) * payMoney;
+				return refund.toFixed(2);
+			},
+			refundMoney() {
+				return this.maxRefundMoney;
+			},
+			refundChannelText() {
+				const type = String(this.orderInfo.payType);
+				if (type === '1') return '退回余额';
+				if (type === '2') return '退回微信';
+				return '退回支付渠道';
+			},
+			otherRefundAmount() {
+				return (Number(this.orderInfo.discountMoney) || 0) + (Number(this.orderInfo.redPacketMoney) || 0);
+			},
+			uploadStatusText() {
+				if ((this.description && this.description.trim().length > 0) || this.fileList.length > 0) {
+					return '已补充';
+				}
+				return '上传有助于处理退款';
+			}
+		},
+		onLoad(options) {
+			if (options.orderId) {
+				this.orderId = options.orderId;
+				this.loadOrderDetail();
+				this.getRefundReasons();
+			}
+		},
+		onShow() {
+			let selectAddr = uni.getStorageSync("selectAddr");
+			if (selectAddr) {
+				this.address = selectAddr;
+				uni.removeStorageSync("selectAddr");
+			}
+		},
+		onUnload() {
+
+		},
+		methods: {
+			loadOrderDetail() {
+				this.$u.api.getShopOrderDetailAjax({ orderId: this.orderId }).then(res => {
+					if (res.code === 200) {
+						this.orderInfo = res.data;
+						// 初始化商品选中状态和退款数量
+						this.orderInfo.detailVoList.forEach(item => {
+							this.$set(item, 'checked', true);
+							this.$set(item, 'refundNum', item.num);
+						});
+
+						// 根据状态默认选中类型
+						if (this.orderInfo.status == '3') { // 待收货
+							// 默认退货退款
+							this.refundType = '1';
+							this.refundTypeText = '退货退款';
+							this.shopStatus = '1';
+							this.shopStatusText = '未收到货';
+						} else {
+							this.refundType = '1';
+							this.refundTypeText = '退货退款';
+							this.shopStatus = '2';
+							this.shopStatusText = '已收到货';
+						}
+
+						// 默认使用订单收货地址作为取件地址
+						this.address = {
+							id: this.orderInfo.receiverAddressId,
+							name: this.orderInfo.receiverName,
+							mobile: this.orderInfo.receiverMobile,
+							fullAddress: this.orderInfo.receiverAddress,
+						};
+					}
+				});
+			},
+			getRefundReasons() {
+				uni.$u.http.get("/token/common/getDictOptions?type=shop_order_complaints_options").then(res => {
+					if (res.code === 200) {
+						this.reasonList = res.data.map(item => ({
+							value: item.dictValue || item.dictLabel,
+							label: item.dictLabel
+						}));
+					}
+				});
+			},
+			toggleCheck(item) {
+				item.checked = !item.checked;
+			},
+			calculateRefundMoney() {
+				// Computed auto updates
+			},
+			confirmType(e) {
+				this.refundType = e[0].value;
+				this.refundTypeText = e[0].label;
+
+				// 切换类型时重置一些状态
+				if (this.refundType === '2') { // 仅退款
+					this.returnMethod = '';
+					this.returnMethodText = '';
+				} else {
+					if (!this.returnMethod) {
+						this.returnMethod = '3';
+						this.returnMethodText = '自行寄回';
+					}
+				}
+			},
+			confirmReason(e) {
+				this.refundReason = e[0].value;
+				this.refundReasonText = e[0].label;
+			},
+			confirmStatus(e) {
+				this.shopStatus = e[0].value;
+				this.shopStatusText = e[0].label;
+			},
+			confirmReturnMethod(e) {
+				this.returnMethod = e[0].value;
+				this.returnMethodText = e[0].label;
+			},
+			chooseAddress() {
+				uni.navigateTo({
+					url: `/pages-mine/pages/address/list?id=${this.address.id || ''}&isSelect=1`
+				});
+			},
+			handleUploadChange(lists) {
+				this.fileList = lists;
+			},
+			submit() {
+				if (this.selectedGoods.length === 0) {
+					this.$u.toast('请选择退款商品');
+					return;
+				}
+				if (!this.refundReason) {
+					this.$u.toast('请选择退款原因');
+					return;
+				}
+				if (this.refundType === '1' && !this.returnMethod) {
+					this.$u.toast('请选择退货方式');
+					return;
+				}
+				if (this.refundType === '1' && this.returnMethod === '1' && !this.address.name) {
+					this.$u.toast('请选择取件地址');
+					return;
+				}
+
+				// 处理图片
+				let files = [];
+				// 优先使用 fileList (因为 handleUploadChange 已同步)
+				// 或者直接从 ref 获取,双重保险
+				let uploadFiles = this.fileList;
+				if (this.$refs.uUpload && this.$refs.uUpload.lists) {
+					uploadFiles = this.$refs.uUpload.lists;
+				}
+
+				uploadFiles.forEach(item => {
+					if (item.response && item.response.code === 200) {
+						files.push(item.response.data);
+					} else if (item.url) {
+						files.push(item.url);
+					}
+				});
+
+				const params = {
+					orderId: this.orderId,
+					refundDetailList: this.selectedGoods.map(item => ({
+						detailOrderId: item.detailOrderId || item.id,
+						num: item.refundNum
+					})),
+					refundType: this.refundType,
+					shopStatus: this.shopStatus,
+					refundReason: this.refundReason,
+					refundMoney: this.refundMoney,
+					sendType: this.returnMethod,
+					addressId: this.address.id || '',
+					description: this.description,
+					fileUrlList: files
+				};
+
+				// 附加地址信息(如果是上门取件)
+				if (this.refundType === '1' && this.returnMethod === '1') {
+					// 这里的参数结构需根据后端实际要求调整,这里假设后端接收 address 对象
+					params.address = this.address;
+				}
+
+				this.$u.api.applyRefundAjax(params).then(res => {
+					if (res.code === 200) {
+						this.$u.toast('提交成功');
+						setTimeout(() => {
+							uni.navigateBack();
+						}, 1500);
+					} else {
+						this.$u.toast(res.msg || '提交失败');
+					}
+				});
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.apply-refund-page {
+		min-height: 100vh;
+		background-color: #f5f5f5;
+		padding: 20rpx;
+		padding-bottom: 140rpx; // Space for fixed footer
+	}
+
+	.card {
+		background-color: #fff;
+		border-radius: 16rpx;
+		margin-bottom: 20rpx;
+		padding: 30rpx;
+		overflow: hidden;
+
+		&.no-padding {
+			padding: 0;
+		}
+
+		.type-section {
+			margin-bottom: 30rpx;
+
+			.section-title {
+				font-size: 30rpx;
+				font-weight: bold;
+				color: #333;
+				margin-bottom: 20rpx;
+			}
+
+			.type-btn-group {
+				display: flex;
+
+				.type-btn {
+					flex: 1;
+					height: 64rpx;
+					display: flex;
+					align-items: center;
+					justify-content: center;
+					border: 2rpx solid #e5e5e5;
+					border-radius: 8rpx;
+					margin-right: 20rpx;
+					font-size: 28rpx;
+					color: #333;
+					transition: all 0.3s;
+
+					&:last-child {
+						margin-right: 0;
+					}
+
+					&.active {
+						border-color: #38C148;
+						color: #38C148;
+						background-color: rgba(56, 193, 72, 0.05);
+						font-weight: bold;
+					}
+				}
+			}
+		}
+
+		.card-header {
+			padding: 10rpx 0 20rpx;
+			border-bottom: 1rpx solid #f5f5f5;
+			margin-bottom: 20rpx;
+
+			.card-title {
+				font-size: 30rpx;
+				font-weight: bold;
+				color: #333;
+			}
+		}
+
+		.card-title {
+			// For simple title without header line
+			font-size: 30rpx;
+			font-weight: bold;
+			color: #333;
+			margin-bottom: 10rpx;
+		}
+	}
+
+	.goods-list {
+		.goods-item {
+			display: flex;
+			align-items: center;
+			margin-bottom: 30rpx;
+
+			&:last-child {
+				margin-bottom: 0;
+			}
+
+			.checkbox-box {
+				margin-right: 20rpx;
+			}
+
+			.goods-cover {
+				width: 140rpx;
+				height: 140rpx;
+				border-radius: 8rpx;
+				margin-right: 20rpx;
+			}
+
+			.goods-info {
+				flex: 1;
+				height: 140rpx;
+				display: flex;
+				flex-direction: column;
+				justify-content: space-between;
+
+				.goods-name {
+					font-size: 28rpx;
+					color: #333;
+					line-height: 1.4;
+				}
+
+				.goods-sku {
+					font-size: 24rpx;
+					color: #999;
+				}
+
+				.price-box {
+					display: flex;
+					justify-content: space-between;
+					align-items: center;
+
+					.price {
+						font-size: 30rpx;
+						color: #333;
+						font-weight: 500;
+					}
+				}
+			}
+		}
+	}
+
+	.refund-money-box {
+		display: flex;
+		align-items: center;
+
+		.money {
+			font-size: 32rpx;
+			color: #38C148; // Theme color
+			font-weight: bold;
+		}
+
+		.edit-tag {
+			display: flex;
+			align-items: center;
+			background-color: #f0f0f0;
+			padding: 4rpx 12rpx;
+			border-radius: 20rpx;
+			margin-left: 16rpx;
+
+			text {
+				font-size: 22rpx;
+				color: #666;
+				margin-left: 4rpx;
+			}
+		}
+	}
+
+	.address-box {
+		flex: 1;
+		display: flex;
+		align-items: center;
+		justify-content: flex-end;
+		text-align: right;
+		color: #333;
+		font-size: 28rpx;
+
+		.addr-detail {
+			font-size: 24rpx;
+			color: #999;
+			max-width: 300rpx;
+		}
+	}
+
+	.footer-bar {
+		position: fixed;
+		bottom: 0;
+		left: 0;
+		right: 0;
+		height: 200rpx;
+		background-color: #fff;
+		display: flex;
+		align-items: center;
+		padding: 0 30rpx;
+		box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
+		z-index: 100;
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+
+		.footer-left {
+			flex: 1;
+			display: flex;
+			align-items: center;
+
+			.label {
+				font-size: 26rpx;
+				color: #333;
+			}
+
+			.amount {
+				font-size: 36rpx;
+				color: #38C148;
+				font-weight: bold;
+				margin: 0 10rpx;
+			}
+
+			.detail-link {
+				font-size: 24rpx;
+				color: #999;
+				text-decoration: underline;
+			}
+		}
+
+		.footer-right {
+			width: 240rpx;
+		}
+	}
+
+	.popup-container {
+		padding: 30rpx;
+		background-color: #fff;
+
+		&.full-height {
+			height: 100%;
+			display: flex;
+			flex-direction: column;
+		}
+
+		.popup-header {
+			display: flex;
+			justify-content: space-between;
+			align-items: center;
+			margin-bottom: 40rpx;
+			flex-shrink: 0;
+
+			.title {
+				font-size: 32rpx;
+				font-weight: bold;
+				color: #333;
+			}
+		}
+
+		.popup-content {
+			margin-bottom: 40rpx;
+			flex: 1;
+			overflow-y: auto;
+
+			.detail-item {
+				margin-bottom: 30rpx;
+
+				.item-row {
+					display: flex;
+					align-items: center;
+					margin-bottom: 10rpx;
+
+					.item-label {
+						flex: 1;
+						margin-left: 20rpx;
+						font-size: 30rpx;
+						color: #333;
+					}
+
+					.item-value {
+						font-size: 30rpx;
+						color: #333;
+						font-weight: 500;
+					}
+				}
+
+				.sub-list {
+					padding-left: 60rpx;
+
+					.sub-item {
+						display: flex;
+						justify-content: space-between;
+						margin-top: 10rpx;
+						font-size: 26rpx;
+						color: #999;
+					}
+				}
+			}
+
+			.upload-textarea-box {
+				background-color: #f9f9f9;
+				padding: 20rpx;
+				border-radius: 12rpx;
+
+				.counter {
+					text-align: right;
+					font-size: 24rpx;
+					color: #999;
+					margin: 10rpx 0 0;
+				}
+			}
+
+			.upload-area {
+				margin-top: 20rpx;
+			}
+		}
+
+		.popup-footer {
+			margin-top: 20rpx;
+			flex-shrink: 0;
+
+			&.safe-area-bottom {
+				padding-bottom: constant(safe-area-inset-bottom);
+				padding-bottom: env(safe-area-inset-bottom);
+			}
+		}
+	}
+
+	.address-section {
+		padding: 26rpx 32rpx;
+	}
+
+	.address-text {
+		color: #333333;
+		font-family: PingFang SC;
+		font-weight: 400;
+	}
+
+	.address-label {
+		font-size: 28rpx;
+		color: #909399;
+		margin-right: 20rpx;
+		width: 140rpx;
+	}
+
+	.u-required {
+		color: #ff0000;
+		margin-left: 8rpx;
+	}
+</style>

+ 38 - 101
pages-car/pages/complaint.vue

@@ -75,35 +75,47 @@
                 </view>
             </view>
 
-            <!-- 相关订单 (新增部分,匹配原型) -->
-            <view class="form-block order-block" v-if="orderInfo">
+            <!-- 相关订单 -->
+            <view class="form-block order-block"
+                v-if="complaintInfo.orderDetailList && complaintInfo.orderDetailList.length > 0">
                 <view class="common-text-2 mb-20">相关订单</view>
-                <view class="order-item flex">
-                    <image class="goods-img" :src="orderInfo.goodsImg" mode="aspectFill"></image>
-                    <view class="goods-info flex-1">
-                        <view class="goods-name u-line-2">{{ orderInfo.goodsName }}</view>
-                        <view class="goods-price">¥{{ orderInfo.goodsPrice }}</view>
+                <view class="flex mb-30" v-for="(item, index) in complaintInfo.orderDetailList"
+                    :key="item.detailOrderId">
+                    <image :src="item.cover" mode="aspectFill"
+                        style="width: 120rpx; height: 120rpx; border-radius: 8rpx; flex-shrink: 0;"></image>
+                    <view class="flex-1 ml-20"
+                        style="display: flex; flex-direction: column; justify-content: space-between; min-height: 120rpx;">
+                        <view style="display: flex; justify-content: space-between;">
+                            <view class="u-line-2"
+                                style="font-size: 28rpx; color: #333; line-height: 40rpx; flex: 1; margin-right: 20rpx;">
+                                {{ item.bookName }}
+                            </view>
+                            <view style="font-size: 28rpx; color: #333;">¥{{ item.price }}</view>
+                        </view>
+                        <view style="font-size: 24rpx; color: #999; margin-top: 8rpx;" v-if="item.conditionType">
+                            品相:{{ item.conditionType | conditionText }}
+                        </view>
+                        <view style="display: flex; justify-content: flex-end; margin-top: 10rpx;">
+                            <u-number-box v-model="item.num" disabled :input-width="60"
+                                :input-height="50"></u-number-box>
+                        </view>
                     </view>
-                    <!-- 简单的步进器展示或者数量 -->
-                    <!-- 原型右下角有步进器,这里只展示数量即可,因为是投诉整个订单或其中商品 -->
-                    <view class="goods-num">x{{ orderInfo.goodsNum }}</view>
                 </view>
             </view>
 
 
-            <view class="common-text-2 required mb-20">上传凭证(最多3张)</view>
-
-            <u-upload class="upload-image" :fileList="fileList" @on-choose-complete="afterRead" @delete="deletePic"
-                :maxCount="3" :auto-upload="false" :previewFullImage="true" uploadText="点击上传"
-                @on-uploaded="onUploaded"></u-upload>
-
+            <!-- 上传凭证 -->
+            <view class="common-text-2 required mb-20 mt-20">上传凭证(最多3张)</view>
+            <view class="form-block" style="padding: 20rpx">
+                <common-image-upload v-model="uploadSuccessList" :maxCount="3" :uploadUrl="uploadUrl"
+                    tip="上传“有效截图”可以让问题优先被发现哦!"></common-image-upload>
+            </view>
 
+            <!-- 投诉说明 -->
             <view class="common-text-2 required mb-20 mt-20">投诉说明</view>
             <view class="form-block" style="padding: 20rpx">
-                <!-- 投诉说明 -->
                 <u-input v-model="description" type="textarea" placeholder="描述具体情况,有助于客服更快处理" :height="200"
                     :border="false" maxlength="300"></u-input>
-                <view class="text-right" style="color: #999; font-size: 24rpx;">{{ description.length }}/300</view>
             </view>
 
             <view class="form-block mt-20">
@@ -129,26 +141,27 @@
 </template>
 
 <script>
-    import ENV_CONFIG from "@/.env.js";
+    import CommonImageUpload from "@/components/image-upload.vue";
     // api前缀
-    const env = ENV_CONFIG[process.env.ENV_TYPE || "dev"];
     export default {
+        components: {
+            CommonImageUpload
+        },
         data() {
             return {
                 showComplaintList: false,
                 complaintReason: "",
                 phone: "",
                 description: "",
-                fileList: [],
                 showPicker: false,
                 reasonList: [],
                 orderId: "",
                 complaintInfo: {
                     status: 1,
                     platformReply: "",
+                    orderDetailList: [],
                     disposeLogList: [],
                 },
-                orderInfo: null, // 用于展示相关订单信息
                 uploadSuccessList: [],
                 submitBtnStyle: {
                     backgroundColor: '#38C148',
@@ -170,31 +183,15 @@
                 };
                 return statusMap[status] || "未知状态";
             },
+            uploadUrl() {
+                return `/api/token/shop/order/complaintUpload/${this.orderId}`;
+            },
         },
         onLoad(ops) {
             if (ops.orderId) {
                 this.orderId = ops.orderId;
                 this.getComplaintInfo();
             }
-            // 获取本地存储的订单信息用于展示
-            const tempOrder = uni.getStorageSync('tempComplaintOrder');
-            if (tempOrder && tempOrder.orderId == this.orderId) {
-                // 提取第一个商品展示,或者根据业务逻辑展示
-                // 假设展示第一个商品
-                if (tempOrder.orderItemList && tempOrder.orderItemList.length > 0) {
-                    const item = tempOrder.orderItemList[0];
-                    this.orderInfo = {
-                        goodsImg: item.goodsCover || item.bookCover, // 适配不同字段
-                        goodsName: item.goodsName || item.bookName,
-                        goodsPrice: item.goodsPrice || item.price,
-                        goodsNum: item.goodsCount || item.count || 1
-                    }
-                }
-                // 预填联系方式
-                if (tempOrder.address && tempOrder.address.phone) {
-                    this.phone = tempOrder.address.phone;
-                }
-            }
 
             this.getComplaintsOptions();
         },
@@ -232,70 +229,10 @@
                 this.complaintReason = this.reasonList[e[0]];
                 this.showPicker = false;
             },
-            onUploaded(lists, name) {
-                console.log(lists, name, "xx111x");
-            },
-            afterRead(lists) {
-                // 先检查token是否存在
-                const token = uni.getStorageSync("token");
-
-                const uploadTasks = lists.map((item) => {
-                    return new Promise((resolve, reject) => {
-                        // 修改为 shop/order 上传接口 (猜测路径,如果失败需确认)
-                        // 参考 getComplaintsInfo 是在 shop/order 下
-                        const uploadUrl = env.apiUrl + `/api/token/shop/order/complaintUpload/${this.orderId}`;
-                        console.log(item, uploadUrl, "xx111x");
-                        uni.uploadFile({
-                            url: uploadUrl,
-                            filePath: item.url,
-                            name: "file",
-                            header: {
-                                Authorization: "Bearer " + token,
-                            },
-                            success: (res) => {
-                                try {
-                                    const result = JSON.parse(res.data);
-                                    if (result.code === 200 && result.data) {
-                                        resolve(result.data);
-                                    } else {
-                                        uni.$u.toast(result.msg || "上传失败");
-                                        reject(new Error(result.msg || "上传失败"));
-                                    }
-                                } catch (e) {
-                                    reject(e);
-                                }
-                            },
-                            fail: (err) => {
-                                uni.$u.toast("上传失败");
-                                reject(err);
-                            },
-                        });
-                    });
-                });
-
-                Promise.all(uploadTasks)
-                    .then((results) => {
-                        this.uploadSuccessList = results.flat();
-                        this.fileList = lists;
-                        console.log(this.fileList, "xx111x", results);
-                    })
-                    .catch((err) => {
-                        console.error("Upload failed:", err);
-                    });
-            },
-            deletePic(event) {
-                this.fileList.splice(event.index, 1);
-                // 同时也需要从 uploadSuccessList 中移除
-                if (this.uploadSuccessList && this.uploadSuccessList.length > event.index) {
-                    this.uploadSuccessList.splice(event.index, 1);
-                }
-            },
             submitComplaint() {
                 if (!this.complaintReason) {
                     return uni.$u.toast("请选择投诉原因");
                 }
-                // 移除图片必填校验,或者根据需求决定。原型上写着“上传凭证”,一般非必填,但这里保持逻辑一致
-                // if (this.fileList.length === 0) { ... } 
 
                 if (!this.description) {
                     return uni.$u.toast("请输入投诉说明");

+ 5 - 0
pages-car/pages/my-order.vue

@@ -133,6 +133,11 @@
                 } else if (type === 'refund') {
                     if (order.status == '2') {
                         this.$refs.refundDialog.open(order);
+                    } else {
+                        // 跳转到申请售后页面
+                        uni.navigateTo({
+                            url: `/pages-car/pages/apply-refund?orderId=${order.orderId}`
+                        });
                     }
                 } else if (type === 'confirm') {
                     uni.showModal({

+ 6 - 11
pages-car/pages/refund-detail.vue

@@ -29,7 +29,7 @@
 				<image :src="goods.cover" mode="aspectFill" class="goods-cover"></image>
 				<view class="goods-info">
 					<view class="goods-title u-line-2">{{ goods.bookName }}</view>
-					<view class="goods-sku" v-if="goods.isbn">品相:{{ conditionTypeMap[goods.conditionType] || '默认' }}
+					<view class="goods-sku" v-if="goods.isbn">品相:{{ goods.conditionType | conditionText }}
 					</view>
 					<view class="price-box">
 						<text class="price">¥{{ goods.price }}</text>
@@ -99,7 +99,7 @@
 		</view>
 
 		<!-- 占位符 -->
-		<view style="height: 120rpx;" v-if="showBottomBar"></view>
+		<view style="height: 150rpx;" v-if="showBottomBar"></view>
 
 		<!-- 客服按钮 -->
 		<FloatingDrag :width="126" :height="140" :initial-position="servicePosition"
@@ -128,15 +128,9 @@
 			return {
 				// 客服按钮位置
 				servicePosition: {
-					left: "auto",
-					right: 0,
-					bottom: "20%",
-				},
-				conditionTypeMap: {
-					'1': '良好',
-					'2': '中等',
-					'3': '次品',
-					'4': '全新'
+					left: 0,
+					right: 'auto',
+					bottom: "10%",
 				},
 				refundOrderId: '',
 				orderInfo: {
@@ -375,6 +369,7 @@
 							display: flex;
 							justify-content: space-between;
 							align-items: center;
+							margin-top: 20rpx;
 
 							.price {
 								font-size: 28rpx;

+ 2 - 4
pages-sell/components/select-good-popup/index.vue

@@ -45,7 +45,7 @@
 					<image v-if="currentQuality === opt.conditionType" src="/pages-sell/static/select-good/selected.png"
 						class="bg-image"></image>
 					<view class="left">
-						<text class="opt-name">{{ qualityNames[opt.conditionType - 1] || '未知' }}</text>
+						<text class="opt-name">{{ $conditionMap[opt.conditionType] || '-' }}</text>
 						<view class="opt-discount" :class="{ active: currentQuality === opt.conditionType }">
 							<text>{{ opt.discount }}折</text>
 						</view>
@@ -87,13 +87,11 @@
 				currentQuality: 1,
 				currentProduct: {},
 				qualityOptions: [],
-				//1-良好 2-中等 3-次品 4-全新
-				qualityNames: ['良好', '中等', '次品', '全新'],
 			};
 		},
 		computed: {
 			currentQualityName() {
-				const opt = this.qualityNames[this.currentQuality - 1];
+				const opt = this.$conditionMap[this.currentQuality];
 				return opt || '未知';
 			},
 			selectedOption() {

+ 6 - 0
pages.json

@@ -333,6 +333,12 @@
                         "navigationBarTitleText": "订单详情"
                     }
                 },
+                {
+                    "path": "pages/apply-refund",
+                    "style": {
+                        "navigationBarTitleText": "申请售后"
+                    }
+                },
                 {
                     "path": "pages/confirm-order",
                     "style": {

+ 10 - 0
utils/constants.js

@@ -0,0 +1,10 @@
+export const CONDITION_MAP = {
+    1: '良好',
+    2: '中等',
+    3: '次品',
+    4: '全新'
+}
+
+export const getConditionText = (type) => {
+    return CONDITION_MAP[type] || '-';
+}