Files
lilishop-uniapp/pages/order/afterSales/afterSalesDetail.vue
pikachu1995@126.com 2107e3f03a feat: 补齐预约下单售后链路,并接入礼品卡结算
立即购买先选时段再结算,免物流地址,售后按数量退款;同步接入礼品卡入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 17:07:27 +08:00

657 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page-wrap">
<scroll-view scroll-y class="page-scroll">
<up-form
:model="form"
ref="uForm"
label-position="left"
label-width="180rpx"
>
<view class="after-sales-goods-detail-view">
<view class="header">
<view>
本次售后服务将由
<text class="seller-name">{{ sku.storeName }}</text>
为您提供
</view>
</view>
<view>
<template v-for="(item,index) in sku.orderItems" :key="index">
<view class="goods-item-view" v-if="item.sn == sn"
@click="gotoGoodsDetail(sku.goodsId || sku.goods_id)">
<view class="goods-img">
<u-image
border-radius="6"
width="131rpx"
height="131rpx"
mode="aspectFill"
:src="getGoodsImage(item)"
></u-image>
</view>
<view class="goods-info">
<view class="goods-title">{{ getGoodsName(item) }}</view>
<view class="goods-price">
<text>{{ unitPrice(displayRefundPrice) }}</text>
<text class="num">购买数量: {{ item.num }}</text>
</view>
</view>
</view>
</template>
</view>
<view class="after-num">
<text class="after-num-label">申请数量</text>
<u-number-box
:value="parseInt(form.num)"
disabled-input
:min="1"
:max="parseInt(sku.num)"
bg-color="#fff"
@change="valChange"
></u-number-box>
</view>
</view>
<view class="body-view">
<!-- 退款原因 -->
<view class="opt-view form-block">
<view class="form-row" @click="openReasonPicker">
<text class="form-row-label">申请原因</text>
<view class="form-row-value">
<text class="form-value" :class="{ placeholder: !form.reason }">
{{ form.reason || '请选择申请原因' }}
</text>
<u-icon name="arrow-right" color="#ccc" size="16"></u-icon>
</view>
</view>
<up-form-item label="申请说明" label-width="180rpx" :border-bottom="false" class="desc-item">
<u-input
v-model="form.problemDesc"
type="textarea"
border="none"
input-align="right"
height="120"
placeholder="请描述申请售后的说明"
/>
</up-form-item>
</view>
<!-- 退款方式 / 银行信息 -->
<view class="opt-view form-block">
<up-form-item label="退款方式" label-width="180rpx" :border-bottom="true">
<view class="form-value">{{
applyInfo.refundWay == 'ORIGINAL' ? '原路退回' : '账号退款'
}}</view>
</up-form-item>
<template v-if="
applyInfo.accountType === 'BANK_TRANSFER' &&
applyInfo.applyRefundPrice != 0
">
<up-form-item label="银行开户行" label-width="180rpx" :border-bottom="true">
<u-input
v-model="form.bankDepositName"
border="none"
input-align="right"
placeholder="请输入银行开户行"
/>
</up-form-item>
<up-form-item label="银行开户名" label-width="180rpx" :border-bottom="true">
<u-input
v-model="form.bankAccountName"
border="none"
input-align="right"
placeholder="请输入银行开户名"
/>
</up-form-item>
<up-form-item label="银行账号" label-width="180rpx" :border-bottom="true" class="bank-account-item">
<u-input
v-model="form.bankAccountNumber"
border="none"
input-align="right"
placeholder="请输入银行账号"
/>
</up-form-item>
</template>
<up-form-item
v-if="form.serviceType !== 'RETURN_MONEY'"
label="返回方式"
label-width="180rpx"
:border-bottom="false"
>
<view class="form-value">快递至第三方卖家</view>
</up-form-item>
</view>
<!-- 上传凭证 -->
<view class="opt-view upload-block">
<view class="img-title">上传凭证最多5张</view>
<view class="images-view">
<u-upload
:file-list="fileList"
:auto-upload="false"
width="150"
@afterRead="onUploadAfterRead"
:max-count="5"
></u-upload>
</view>
</view>
<view class="opt-tip">提交服务单后,售后专员可能与您电话沟通,请保持手机畅通</view>
</view>
</up-form>
</scroll-view>
<view class="submit-view">
<u-button
type="primary"
ripple
shape="circle"
v-if="applyInfo.refundWay"
:custom-style="{ backgroundColor: lightColor, width: '100%' }"
@click="onSubmit"
>提交申请</u-button>
</view>
<u-picker
v-model:show="reasonSelectShow"
:columns="[reasonList]"
keyName="label"
valueName="value"
title="申请原因"
@confirm="reasonSelectConfirm"
></u-picker>
<u-toast ref="uToast" />
</view>
</template>
<script setup lang="ts">
import { ref, reactive, computed, getCurrentInstance, watch } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useStore } from '@/store'
import { unitPrice, parseGoodsImageUrl } from '@/utils/filters.js'
import { getAfterSaleReason, applyReturn, getAfterSaleInfo } from '@/api/after-sale'
import { isAppointmentOrder } from '@/utils/goodsType.js'
import { computePartialRefundPrice } from '@/utils/appointmentGoods.js'
import { handleUploadAfterRead } from '@/utils/uploadHelper.js'
import storage from '@/utils/storage.js'
const store = useStore()
const { proxy } = getCurrentInstance()!
const $u = proxy!.$u
const lightColor = computed(() => store.getters.lightColor)
const fileList = ref<any[]>([])
const sn = ref('')
const sku = ref<any>({})
const reasonSelectShow = ref(false)
const reasonList = ref<any[]>([])
const applyInfo = ref<any>({})
const uToast = ref<any>(null)
const isAppointmentAfterSale = ref(false)
const displayRefundPrice = computed(() => {
if (!isAppointmentAfterSale.value) return applyInfo.value?.applyRefundPrice
return computePartialRefundPrice(applyInfo.value, form.num)
})
watch(
() => form.num,
() => {
if (!isAppointmentAfterSale.value) return
applyInfo.value = {
...applyInfo.value,
displayRefundPrice: displayRefundPrice.value,
}
}
)
const form = reactive({
orderItemSn: '',
skuId: '',
reason: '',
problemDesc: '',
images: [] as string[],
num: 1,
goodsId: '',
accountType: '',
applyRefundPrice: '',
refundWay: '',
serviceType: 'RETURN_GOODS',
bankDepositName: '',
bankAccountName: '',
bankAccountNumber: '',
})
onLoad((options) => {
let navTitle = '申请售后'
form.serviceType = 'RETURN_GOODS'
if (options?.value == '1') {
navTitle = '申请退货'
form.serviceType = 'RETURN_GOODS'
}
if (options?.value == '2') {
navTitle = '申请换货'
form.serviceType = 'EXCHANGE_GOODS'
}
if (options?.value == '3') {
navTitle = '申请退款'
form.serviceType = 'RETURN_MONEY'
}
uni.setNavigationBarTitle({ title: navTitle })
sn.value = options?.sn || ''
sku.value = storage.getAfterSaleData()
isAppointmentAfterSale.value = isAppointmentOrder(sku.value?.orderType)
form.orderItemSn = options?.sn || ''
form.skuId = sku.value.skuId
form.num = sku.value.num
form.goodsId = sku.value.goodsId
fetchReasonActions(form.serviceType)
init(options?.sn || '')
})
function hideLoadingIfNeeded() {
if (store.state.isShowToast) uni.hideLoading()
}
function getGoodsName(item: any) {
return item.goodsName || item.name || ''
}
function getGoodsImage(item: any) {
const image = item.image || item.goodsImage || item.thumbnail
return parseGoodsImageUrl(image)
}
function gotoGoodsDetail(goodsId: string) {
if (!goodsId) return
uni.navigateTo({
url: `/pages/product/goods?id=${form.skuId}&goodsId=${goodsId}`,
})
}
async function fetchReasonActions(serviceType: string) {
uni.showLoading({ title: '加载中' })
await getAfterSaleReason(serviceType).then((res) => {
if (res.data.success) {
reasonList.value = res.data.result.map((item: any) => ({
value: item.id,
label: item.reason,
}))
}
})
hideLoadingIfNeeded()
}
function init(orderItemSn: string) {
getAfterSaleInfo(orderItemSn).then((response) => {
if (response.data.code == 400) {
uni.showToast({
title: response.data.message,
duration: 2000,
icon: 'none',
})
} else {
const result = response.data.result
applyInfo.value = {
...result,
totalNum: result.orderItemNum || sku.value?.num,
}
form.accountType = result.accountType
}
})
}
function openReasonPicker() {
if (!reasonList.value.length) {
uni.showToast({ title: '暂无可选原因', icon: 'none' })
return
}
reasonSelectShow.value = true
}
function reasonSelectConfirm(val: any) {
const selected = val?.value?.[0] || val?.[0]
if (selected) {
form.reason = selected.label || selected.text || ''
}
}
function valChange(e: { value: number }) {
form.num = e.value
if (isAppointmentAfterSale.value) {
applyInfo.value = {
...applyInfo.value,
applyRefundPrice: computePartialRefundPrice(applyInfo.value, form.num),
}
}
}
function onUploadAfterRead(event: any) {
handleUploadAfterRead(event, fileList.value, (urls) => {
form.images = urls
})
}
function showToast(message: string, type = 'error') {
const text = message || (type === 'success' ? '操作成功' : '操作失败')
if (uToast.value) {
uToast.value.show({ message: text, type })
return
}
uni.showToast({
title: text,
icon: type === 'success' ? 'success' : 'none',
})
}
function onSubmit() {
if (!validateFormParams()) return
uni.showLoading({ title: '加载中' })
form.accountType = applyInfo.value.accountType
form.refundWay = applyInfo.value.refundWay
form.applyRefundPrice = isAppointmentAfterSale.value
? computePartialRefundPrice(applyInfo.value, form.num)
: applyInfo.value.applyRefundPrice
applyReturn(sn.value, form).then((resp) => {
hideLoadingIfNeeded()
if (resp.data.success) {
showToast('提交成功', 'success')
uni.redirectTo({ url: '/pages/order/afterSales/applySuccess' })
} else {
showToast(resp.data.message || '提交失败', 'error')
}
})
}
function validateFormParams() {
if ($u.test.isEmpty(form.reason)) {
showToast('请选择退款原因')
return false
}
if ($u.test.isEmpty(form.problemDesc)) {
showToast('请输入退款说明')
return false
}
if (
applyInfo.value.accountType === 'BANK_TRANSFER' &&
applyInfo.value.applyRefundPrice != 0
) {
if ($u.test.isEmpty(form.bankDepositName)) {
showToast('请输入银行开户行')
return false
}
if ($u.test.isEmpty(form.bankAccountName)) {
showToast('请输入银行开户名')
return false
}
if ($u.test.isEmpty(form.bankAccountNumber)) {
showToast('请输入银行账号')
return false
}
if ($u.test.chinese(form.bankAccountName) === false) {
showToast('银行开户名需为中文')
return false
}
if ($u.test.chinese(form.bankDepositName) === false) {
showToast('银行开户行需为中文')
return false
}
}
return true
}
</script>
<style lang="scss" scoped>
page {
background: $page-color-base;
height: 100%;
}
.page-wrap {
display: flex;
flex-direction: column;
height: 100vh;
background: $page-color-base;
}
.page-scroll {
flex: 1;
height: 0;
box-sizing: border-box;
}
.body-view {
padding-bottom: 24rpx;
}
.after-sales-goods-detail-view {
background-color: #fff;
.header {
background-color: #f7f7f7;
color: #999999;
font-size: 22rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
line-height: 70rpx;
.seller-name {
color: $main-color;
}
}
.goods-item-view {
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 24rpx 30rpx;
background-color: #eef1f2;
.goods-img {
flex-shrink: 0;
width: 131rpx;
height: 131rpx;
border-radius: 6rpx;
overflow: hidden;
}
.goods-info {
flex: 1;
min-width: 0;
padding-left: 24rpx;
.goods-title {
margin-bottom: 12rpx;
color: $font-color-dark;
font-size: 26rpx;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
word-break: break-all;
}
.goods-price {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
font-size: 28rpx;
color: $light-color;
.num {
flex-shrink: 0;
margin-left: 16rpx;
font-size: 24rpx;
color: #999999;
}
}
}
}
}
.after-num {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 0 30rpx;
padding: 24rpx 0;
border-top: 1px solid #f0f0f0;
.after-num-label {
font-size: 30rpx;
color: #333;
}
}
.opt-tip {
margin-top: 20rpx;
padding: 10rpx 30rpx 30rpx;
font-size: 22rpx;
color: #999;
line-height: 1.6;
}
.opt-view {
background-color: #fff;
margin-top: 20rpx;
}
.form-block {
padding: 0;
.form-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
min-height: 100rpx;
padding: 0 30rpx;
border-bottom: 1px solid #f0f0f0;
.form-row-label {
flex-shrink: 0;
width: 150rpx;
font-size: 30rpx;
color: #333;
}
.form-row-value {
flex: 1;
min-width: 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 8rpx;
}
}
:deep(.u-form-item) {
padding: 0 30rpx;
min-height: 100rpx;
}
:deep(.u-form-item__body) {
min-height: 100rpx;
align-items: center;
}
:deep(.u-form-item__body__left__content__label) {
font-size: 30rpx;
color: #333;
}
:deep(.u-form-item__body__right__content__slot) {
flex: 1;
display: flex;
justify-content: flex-end;
align-items: center;
}
:deep(.u-input) {
padding: 0 !important;
}
:deep(.u-input__content) {
background: transparent !important;
}
:deep(.u-input__content__field-wrapper__field) {
font-size: 28rpx !important;
color: #666 !important;
}
}
.upload-block {
padding: 24rpx 30rpx 30rpx;
.img-title {
font-size: 30rpx;
color: #333;
margin-bottom: 20rpx;
}
.images-view {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
}
}
.form-value {
width: 100%;
text-align: right;
font-size: 28rpx;
color: #666;
&.placeholder {
color: #c0c4cc;
}
}
.desc-item {
:deep(.u-form-item__body) {
align-items: flex-start;
padding-top: 24rpx;
padding-bottom: 24rpx;
min-height: auto;
}
:deep(.u-form-item__body__left) {
padding-top: 8rpx;
}
:deep(.u-form-item__body__right__content__slot) {
justify-content: flex-end;
align-items: flex-start;
}
:deep(.u-input__content__field-wrapper__field) {
text-align: right !important;
}
:deep(.u-input__content__field-wrapper__field--placeholder) {
text-align: right !important;
}
}
.submit-view {
flex-shrink: 0;
z-index: 999;
box-sizing: border-box;
padding: 16rpx 32rpx calc(16rpx + env(safe-area-inset-bottom));
background-color: #ffffff;
border-top: 1px solid #f2f2f2;
}
</style>