feat: 商品申请评分+逆向退款申请

This commit is contained in:
zc 2025-12-07 20:34:14 +08:00
parent c9de557000
commit f7f14e59cb
5 changed files with 305 additions and 237 deletions

View File

@ -124,4 +124,81 @@ export const apiGetOrderCount = (data) => {
);
}
// 申请退款
export const apiApplyRefund = (data) => {
return httpRequest.post(
`${baseUrl}/reverse/add`,
data
);
}
// 申请评价
export const apiApplyComment = (data) => {
return httpRequest.post(
`${baseUrl}/comment/add`,
data
);
}
// 上传图片(支持批量上传)
export const apiUploadFile = (fileList) => {
// uni.request不支持FormData和文件上传需要使用uni.uploadFile
// fileList应该是文件对象数组格式[{path:'xxx'}] 或 [{tempFilePath:'xxx'}]
return new Promise((resolve, reject) => {
// 获取用户信息用于添加token
let user_info = uni.getStorageSync("user_info") || {};
if (typeof user_info === "string") {
try {
user_info = JSON.parse(user_info) || {};
} catch (e) {
user_info = {};
}
}
// 批量上传所有文件
const uploadPromises = fileList.map((file, index) => {
return new Promise((fileResolve, fileReject) => {
const filePath = file.path || file.tempFilePath || file.path || file;
if (!filePath) {
fileReject({ msg: `${index + 1}个文件路径无效` });
return;
}
uni.uploadFile({
url: `${baseUrl}/upload/file`,
filePath: filePath,
name: 'files',
header: {
token: user_info.token || '',
buyerId: user_info.buyerId || '',
flag: user_info.flag || ''
},
success: (res) => {
try {
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
if (res.statusCode === 200) {
fileResolve(data);
} else {
fileReject({ code: res.statusCode, msg: data.msg || '上传失败', data });
}
} catch (e) {
fileReject({ code: res.statusCode, msg: '解析响应数据失败', data: res.data });
}
},
fail: (err) => {
fileReject({ msg: err.errMsg || '上传失败', err });
}
});
});
});
// 等待所有文件上传完成
Promise.all(uploadPromises)
.then(results => {
resolve(results);
})
.catch(err => {
reject(err);
});
});
}

View File

@ -3,12 +3,10 @@
<view class="goods-list">
<view class="goods-item" v-for="(item, index) in goodsList" :key="index">
<!-- 商品详情 -->
<view class="goods-main">
<!-- 商品图片 -->
<!-- <view class="goods-main">
<view class="goods-image">
<image class="image" :src="item.goods_image" mode="scaleToFill"></image>
</view>
<!-- 商品信息 -->
<view class="goods-content">
<view class="goods-title"><text class="twoline-hide">{{ item.goods_name }}</text></view>
<view class="goods-props clearfix">
@ -17,7 +15,6 @@
</view>
</view>
</view>
<!-- 交易信息 -->
<view class="goods-trade">
<view class="goods-price">
<text class="unit"></text>
@ -27,22 +24,22 @@
<text>×{{ item.total_num }}</text>
</view>
</view>
</view>
</view> -->
<!-- 选择评价 -->
<view class="score-row">
<view class="score-item score-praise" :class="{ active: formData[index].score == 10 }" @click="setScore(index, 10)">
<view class="score-item score-praise" :class="{ active: formData[index].level == 'good' }" @click="setScore(index, 'good')">
<view class="score">
<text class="score-icon iconfont icon-haoping"></text>
<text class="score-text">好评</text>
</view>
</view>
<view class="score-item score-review" :class="{ active: formData[index].score == 20 }" @click="setScore(index, 20)">
<view class="score-item score-review" :class="{ active: formData[index].level == 'common' }" @click="setScore(index, 'common')">
<view class="score">
<text class="score-icon iconfont icon-zhongping"></text>
<text class="score-text">中评</text>
</view>
</view>
<view class="score-item score-negative" :class="{ active: formData[index].score == 30 }" @click="setScore(index, 30)">
<view class="score-item score-negative" :class="{ active: formData[index].level == 'notgood' }" @click="setScore(index, 'notgood')">
<view class="score">
<text class="score-icon iconfont icon-chaping"></text>
<text class="score-text">差评</text>
@ -50,9 +47,52 @@
</view>
</view>
<!-- 评分详情 -->
<view class="rating-detail">
<view class="rating-item">
<text class="rating-label">商品分:</text>
<view class="rating-stars">
<u-rate
:current="formData[index].descMatch || 5"
:count="5"
:size="28"
active-color="#FFD700"
inactive-color="#E0E0E0"
@change="(val) => setGoodsScore(index, val)"
></u-rate>
</view>
</view>
<view class="rating-item">
<text class="rating-label">卖家服务评分:</text>
<view class="rating-stars">
<u-rate
:current="formData[index].sellerService || 5"
:count="5"
:size="28"
active-color="#FFD700"
inactive-color="#E0E0E0"
@change="(val) => setServiceScore(index, val)"
></u-rate>
</view>
</view>
<view class="rating-item">
<text class="rating-label">物流评分:</text>
<view class="rating-stars">
<u-rate
:current="formData[index].logisticsService || 5"
:count="5"
:size="28"
active-color="#FFD700"
inactive-color="#E0E0E0"
@change="(val) => setLogisticsScore(index, val)"
></u-rate>
</view>
</view>
</view>
<!-- 评价内容 -->
<view class="form-content">
<textarea class="textarea" v-model="formData[index].content" maxlength="500" placeholder="请输入评价内容 (留空则不评价)"></textarea>
<textarea class="textarea" v-model="formData[index].serviceComment" maxlength="500" placeholder="请输入评价内容 (留空则不评价)"></textarea>
</view>
<!-- 图片列表 -->
@ -81,41 +121,33 @@
</template>
<script>
import * as UploadApi from '@/api/upload'
import * as OrderCommentApi from '@/api/order/comment'
import * as OrderApi from '@/api/order'
const maxImageLength = 6
export default {
data() {
return {
//
isLoading: true,
// ID
orderId: null,
//
goodsList: [],
//
goodsList: [{}],
formData: [],
//
maxImageLength,
//
disabled: false
disabled: false,
trackNumber: null
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad({ orderId }) {
this.orderId = orderId
//
this.getGoodsList()
onLoad({ tradeOrderId, trackNumber }) {
this.orderId = tradeOrderId
this.trackNumber = trackNumber
this.initFormData()
this.isLoading = false
// this.getGoodsList()
},
methods: {
//
/* // 获取待评价商品列表
getGoodsList() {
const app = this
app.isLoading = true
@ -125,29 +157,41 @@
app.initFormData()
app.isLoading = false
})
},
}, */
// form
initFormData() {
const { goodsList } = this
const formData = goodsList.map(item => {
return {
goods_id: item.goods_id,
order_goods_id: item.order_goods_id,
score: 10,
content: '',
trackNumber: this.trackNumber,
level: 'good',
descMatch: 0, // 5
sellerService: 0, // 2
logisticsService: 0, // 3
serviceComment: '',
commentDetailList: '',
imageList: [],
uploaded: []
}
})
this.formData = formData
},
//
setScore(index, score) {
this.formData[index].score = score
setScore(index, level) {
this.formData[index].level = level
},
//
setGoodsScore(index, score) {
this.formData[index].descMatch = score
},
//
setServiceScore(index, score) {
this.formData[index].sellerService = score
},
//
setLogisticsScore(index, score) {
this.formData[index].logisticsService = score
},
//
chooseImage(index) {
const app = this
@ -163,29 +207,26 @@
}
});
},
//
deleteImage(index, imageIndex) {
this.formData[index].imageList.splice(imageIndex, 1)
},
//
handleSubmit() {
const app = this
//
if (app.disabled === true) return false
//
app.disabled = true
//
const imagesLength = app.getImagesLength()
if (imagesLength > 0) {
app.uploadFile()
.then(() => {
console.log('then')
app.onSubmit()
const imageList = app.formData.map(item => item.imageList)
OrderApi.apiUploadFile(imageList.flat())
.then(results => {
app.formData[0].commentDetailList = results.map(result => {
return {"commentUrl":result.data[0].url,"type":"image"}
});
app.onSubmit();
})
.catch(err => {
console.log('catch')
app.disabled = false
if (err.statusCode !== 0) {
app.$toast(err.errMsg)
@ -196,61 +237,31 @@
app.onSubmit()
}
},
//
getImagesLength() {
const { formData } = this
let imagesLength = 0
formData.forEach(item => {
if (item.content.trim()) {
if (item.serviceComment.trim()) {
imagesLength += item.imageList.length
}
})
return imagesLength
},
//
onSubmit() {
const app = this
OrderCommentApi.submit(app.orderId, app.formData)
const params = { ...app.formData[0], imageList: undefined }
OrderApi.apiApplyComment(params)
.then(result => {
app.$toast(result.message)
app.$toast(result.msg)
setTimeout(() => {
app.disabled = false
uni.navigateBack()
}, 1500)
})
.catch(err => app.disabled = false)
},
//
uploadFile() {
const app = this
const { formData } = app
//
const files = []
formData.forEach((item, index) => {
if (item.content.trim() && item.imageList.length) {
const images = item.imageList.map(image => image)
files.push({ formDataIndex: index, images })
}
})
//
return new Promise((resolve, reject) => {
Promise.all(files.map((file, index) => {
return new Promise((resolve, reject) => {
UploadApi.image(file.images)
.then(fileIds => {
app.formData[index].uploaded = fileIds
resolve(fileIds)
})
.catch(reject)
})
}))
.then(resolve, reject)
})
}
}
}
</script>
@ -364,6 +375,35 @@
}
}
//
.rating-detail {
padding: 20rpx 20rpx;
margin-top: 10rpx;
.rating-item {
display: flex;
align-items: center;
margin-bottom: 20rpx;
font-size: 28rpx;
color: #333;
&:last-child {
margin-bottom: 0;
}
.rating-label {
width: 200rpx;
color: #666;
}
.rating-stars {
flex: 1;
display: flex;
align-items: center;
}
}
}
//
.form-content {
padding: 14rpx 10rpx;

View File

@ -82,7 +82,7 @@
<view class="goods-title"><text class="twoline-hide">{{ goods.productName }}</text></view>
<view class="goods-props clearfix">
<view class="goods-props-item" v-for="(props, idx) in goods.skuInfo" :key="idx">
<text>{{ props.value.name }}</text>
<text>{{ props.propertyName }}: {{ props.propertyValue }}</text>
</view>
</view>
</view>
@ -179,7 +179,7 @@
<view v-if="!['apply_cancel', 'close', 'cancel', 'refund'].includes(orderDetail.tradeOrderEntity.status)" class="footer-fixed">
<view class="btn-wrapper">
<block v-for="(action, idx) in orderDetail.orderActionList" :key="idx">
<view :class="['btn-item', { 'active': ['pay', 'delivered', 'shipped'].some(key => action.interfaceUri.includes(key)) }]" @click="onClickBtn(item.tradeOrderEntity.tradeOrderId, action)">{{ action.desc }}</view>
<view :class="['btn-item', { 'active': ['pay', 'delivered', 'shipped'].some(key => action.interfaceUri.includes(key)) }]" @click="onClickBtn(orderId, orderDetail.vvPackageEntity.trackNumber, action)">{{ action.desc }}</view>
</block>
</view>
</view>
@ -305,47 +305,29 @@
},
//
handleApplyRefund(orderGoodsId) {
this.$goPageByToken('pages/refund/apply', { orderGoodsId })
handleApplyRefund(orderGoodsId, trackNumber) {
this.$goPageByToken('pages/refund/apply', { orderGoodsId, trackNumber })
},
//
onCancel(orderId) {
onClick(orderId, url, tip) {
const app = this
uni.showModal({
title: '友情提示',
content: '确认要取消该订单吗?',
content: tip,
success(o) {
if (o.confirm) {
OrderApi.cancel(orderId)
OrderApi.apiActionOrder(url, { tradeOrderId: orderId })
.then(result => {
//
app.$toast(result.message)
//
setTimeout(() => app.getOrderDetail(true), 1500)
app.$toast(result.msg)
const timer = setTimeout(() => {
clearTimeout(timer);
uni.navigateBack();
}, 1500);
})
}
}
});
},
onReceipt(orderId, url) {
const app = this
uni.showModal({
title: '友情提示',
content: '确认要确认收货吗?',
success(o) {
if (o.confirm) {
OrderApi.apiActionOrder(url, { tradeOrderId: orderId })
.then(result => {
//
app.$toast(result.msg)
//
app.onRefreshList()
})
}
}
});
},
//
/* async onReceipt(orderId) {
const app = this
@ -440,22 +422,26 @@
this.$goPageByToken('pages/checkout/cashier/index', { orderId })
},
//
handleTargetComment(orderId) {
this.$goPageByToken('pages/order/comment/index', { orderId })
handleTargetComment(orderId, trackNumber) {
this.$goPageByToken('pages/order/comment/index', { tradeOrderId: orderId, trackNumber })
},
onClickBtn(orderId, trackNumber, action) {
console.warn('----- my data is orderId, action: ', orderId, trackNumber, action)
const tip = [{ name: '/applyCancel', tip: '确认要取消该订单吗?' }, { name: '/receipt', tip: '确认要收货吗?' }, { name: '/shipped', tip: '确认要收货吗?' }]
if (action.interfaceUri.includes('/pay')) {
this.onPay(orderId)
} else if (action.interfaceUri.includes('/comment')) {
this.handleTargetComment(orderId, trackNumber)
} else if (action.interfaceUri.includes('/refund')) {
this.handleApplyRefund(orderId, trackNumber)
} else if (action.interfaceUri.includes('/logistics')) {
this.handleTargetExpress(trackNumber)
} else {
const tipContent = tip.find(item => action.interfaceUri.includes(item.name))?.tip
this.onClick(orderId, action.interfaceUri, tipContent)
}
}
},
onClickBtn(orderId, action) {
console.warn('----- my data is orderId, action: ', orderId, action)
if (action.interfaceUri === '/app/order/applyCancel') {
this.onCancel(orderId, action.interfaceUri)
} else if (action.interfaceUri.includes('/pay')) {
this.onPay(orderId)
} else if (action.interfaceUri.includes('/receipt')) {
this.onReceipt(orderId, action.interfaceUri)
} else if (action.interfaceUri.includes('/comment')) {
this.handleTargetComment(orderId)
}
}
}
</script>

View File

@ -53,7 +53,7 @@
<view v-if="item.order_status != OrderStatusEnum.CANCELLED.value" class="order-handle">
<view class="btn-group clearfix">
<block v-for="(action, i) in item.orderActionList" :key="action.interfaceUri">
<view :class="['btn-item', i && 'active']" @click="onClickBtn(item.tradeOrderEntity.tradeOrderId, action)">{{ action.desc }}</view>
<view :class="['btn-item', i && 'active']" @click="onClickBtn(item.tradeOrderEntity.tradeOrderId, item.vvPackageEntity.trackNumber, action)">{{ action.desc }}</view>
</block>
<!-- 未支付取消订单 -->
<!-- <block v-if="item.interfaceUri == PayStatusEnum.PENDING.value">
@ -295,33 +295,42 @@
this.mescroll.resetUpScroll()
}, 120)
},
onClickBtn(orderId, action) {
console.warn('----- my data is orderId, action: ', orderId, action)
if (action.interfaceUri === '/app/order/applyCancel') {
this.onCancel(orderId, action.interfaceUri)
} else if (action.interfaceUri.includes('/pay')) {
this.onPay(orderId)
} else if (action.interfaceUri.includes('/receipt')) {
this.onReceipt(orderId, action.interfaceUri)
} else if (action.interfaceUri.includes('/comment')) {
this.handleTargetComment(orderId)
}
//
handleTargetExpress(trackNumber) {
this.$goPageByToken('pages/order/express/index', { trackNumber })
},
// 退/
handleApplyRefund(orderId, trackNumber) {
this.$goPageByToken('pages/refund/apply', { orderId, trackNumber })
},
onClickBtn(orderId, trackNumber, action) {
console.warn('----- my data is orderId, trackNumber, action: ', orderId, trackNumber, action)
const tip = [{ name: '/applyCancel', tip: '确认要取消该订单吗?' }, { name: '/receipt', tip: '确认要收货吗?' }, { name: '/shipped', tip: '确认要收货吗?' }]
if (action.interfaceUri.includes('/pay')) {
this.onPay(orderId)
} else if (action.interfaceUri.includes('/comment')) {
this.handleTargetComment(orderId, trackNumber)
} else if (action.interfaceUri.includes('/refund')) {
this.handleApplyRefund(orderId, trackNumber)
} else if (action.interfaceUri.includes('/logistics')) {
this.handleTargetExpress(trackNumber)
} else {
const tipContent = tip.find(item => action.interfaceUri.includes(item.name))?.tip
this.onClick(orderId, action.interfaceUri, tipContent)
}
},
//
onCancel(orderId, url) {
onClick(orderId, url, tip) {
const app = this
uni.showModal({
title: '友情提示',
content: '确认要取消该订单吗?',
content: tip,
success(o) {
if (o.confirm) {
OrderApi.apiActionOrder(url, { tradeOrderId: orderId })
.then(result => {
//
app.$toast(result.msg)
//
app.onRefreshList()
})
}
@ -339,27 +348,9 @@
},
//
handleTargetComment(orderId) {
this.$goPageByToken('pages/order/comment/index', { tradeOrderId: orderId })
handleTargetComment(orderId, trackNumber) {
this.$goPageByToken('pages/order/comment/index', { tradeOrderId: orderId, trackNumber })
},
onReceipt(orderId, url) {
const app = this
uni.showModal({
title: '友情提示',
content: '确认要确认收货吗?',
success(o) {
if (o.confirm) {
OrderApi.apiActionOrder(url, { tradeOrderId: orderId })
.then(result => {
//
app.$toast(result.msg)
//
app.onRefreshList()
})
}
}
});
}
/*
//
async onReceipt(orderIndex) {

View File

@ -2,7 +2,7 @@
<view v-if="!isLoading" class="container" :style="appThemeStyle">
<!-- 商品详情 -->
<view class="goods-detail b-f dis-flex flex-dir-row">
<!-- <view class="goods-detail b-f dis-flex flex-dir-row">
<view class="left">
<image class="goods-image" :src="goods.goods_image"></image>
</view>
@ -22,12 +22,12 @@
</view>
</view>
</view>
-->
<!-- 服务类型 -->
<view class="row-service b-f m-top20">
<view class="row-title">服务类型</view>
<view class="service-switch dis-flex">
<view class="switch-item" v-for="(item, index) in RefundTypeEnum.data" :key="index" :class="{ active: formData.type == item.value }"
<view class="switch-item" v-for="(item, index) in RefundTypeEnum" :key="index" :class="{ active: formData.refundType == item.value }"
@click="onSwitchService(item.value)">{{ item.name }}</view>
</view>
</view>
@ -36,15 +36,15 @@
<view class="row-textarea b-f m-top20">
<view class="row-title">申请原因</view>
<view class="content">
<textarea class="textarea" v-model="formData.content" maxlength="2000" placeholder="请详细填写申请原因,注意保持商品的完好,建议您先与卖家沟通"
<textarea class="textarea" v-model="formData.refundReason" maxlength="2000" placeholder="请详细填写申请原因,注意保持商品的完好,建议您先与卖家沟通"
placeholderStyle="color:#ccc"></textarea>
</view>
</view>
<!-- 退款金额 -->
<view v-if="formData.type == RefundTypeEnum.RETURN.value" class="row-money b-f m-top20 dis-flex">
<view v-if="formData.refundType == 2" class="row-money b-f m-top20 dis-flex">
<view class="row-title">退款金额</view>
<view class="money col-m">{{ goods.total_pay_price }}</view>
<view class="money col-m">{{ reFundPrice }}</view>
</view>
<!-- 上传凭证 -->
@ -75,68 +75,59 @@
</template>
<script>
import { RefundTypeEnum } from '@/common/enum/order/refund'
import * as UploadApi from '@/api/upload'
import * as RefundApi from '@/api/refund'
import * as OrderApi from '@/api/order'
const maxImageLength = 6
export default {
data() {
return {
//
RefundTypeEnum,
//
RefundTypeEnum: [
{ name: '仅退款', value: 1, show: true },
{ name: '退货退款', value: 2, show: false }
],
isLoading: true,
// id
orderGoodsId: null,
//
goods: {},
//
formData: {
// ID
images: [],
//
type: 10,
//
content: ''
resourceDTOList: [],
refundType: 1,
refundReason: ''
},
//
imageList: [],
//
maxImageLength,
//
disabled: false
disabled: false,
reFundPrice: 0,
trackNumber: null
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad({ orderGoodsId }) {
onLoad({ orderGoodsId, trackNumber }) {
this.orderGoodsId = orderGoodsId
if (trackNumber) {
this.RefundTypeEnum[1].show = true
this.trackNumber = trackNumber
}
//
this.getGoodsDetail()
OrderApi.apiGetOrderDetail({ tradeOrderLineIdList: [orderGoodsId] }).then(res => {
if (res.data.length) {
this.reFundPrice = res.data[0].payAmount
this.isLoading = false
}
})
},
methods: {
//
getGoodsDetail() {
const app = this
app.isLoading = true
RefundApi.goods(app.orderGoodsId)
.then(result => {
app.goods = result.data.goods
app.isLoading = false
})
},
//
onSwitchService(value) {
this.formData.type = value
this.formData.refundType = value
},
//
chooseImage() {
const app = this
@ -148,6 +139,7 @@
sizeType: ['original', 'compressed'], //
sourceType: ['album', 'camera'], //
success({ tempFiles }) {
console.warn('----- my data is 2222: ', tempFiles)
// tempFiles = [{path:'xxx', size:100}]
app.imageList = oldImageList.concat(tempFiles)
},
@ -172,12 +164,10 @@
})
// #endif
},
//
deleteImage(imageIndex) {
this.imageList.splice(imageIndex, 1)
},
//
handleSubmit() {
const app = this
@ -185,7 +175,7 @@
//
if (app.disabled === true) return false
//
if (!app.formData.content.trim().length) {
if (!app.formData.refundReason.trim().length) {
app.$toast('请填写申请原因')
return false
}
@ -193,53 +183,37 @@
app.disabled = true
//
if (imageList.length > 0) {
app.uploadFile()
.then(() => app.onSubmit())
.catch(err => {
app.disabled = false
if (err.statusCode !== 0) {
app.$toast(err.errMsg)
}
console.log('err', err)
OrderApi.apiUploadFile(imageList)
.then(results => {
app.formData.resourceDTOList = results.map(result => {
return result.data[0].url;
});
app.onSubmit();
})
.catch(err => {
app.disabled = false;
const errMsg = err.msg || err.errMsg || '上传失败';
app.$toast(errMsg);
console.log('上传失败', err);
});
} else {
app.onSubmit()
app.onSubmit();
}
},
//
onSubmit() {
const app = this
RefundApi.apply(app.orderGoodsId, app.formData)
const params = {...app.formData, trackNumber: app.trackNumber, tradeOrderId: app.trackNumber ? undefined : app.orderGoodsId }
OrderApi.apiApplyRefund(params)
.then(result => {
app.$toast(result.message)
app.$toast(result.msg)
setTimeout(() => {
app.disabled = false
uni.navigateBack()
}, 1500)
})
.catch(err => app.disabled = false)
},
//
uploadFile() {
const app = this
const { imageList } = app
//
return new Promise((resolve, reject) => {
if (imageList.length > 0) {
UploadApi.image(imageList)
.then(fileIds => {
app.formData.images = fileIds
resolve(fileIds)
})
.catch(reject)
} else {
resolve()
}
})
}
}
}
</script>