feat(approval): 优化退货审批流程并添加文件上传功能

- 在 `.env` 配置文件中启用调试工具
- 在 `.env.development`、`.env.staging` 和 `.env.production` 中添加后端地址配置
- 修改 `SubmitApprovalRequestData` 接口,合并 `orderId` 和 `goodsId` 为 `orderGoodsId`
- 在 `approval/submit.vue` 中添加文件上传功能,并优化表单提交逻辑
- 在 `order/index.vue` 中添加订单商品状态显示和退款按钮条件判断
This commit is contained in:
dzq 2025-04-08 09:22:21 +08:00
parent bc46f40285
commit 42eae0b1cd
8 changed files with 159 additions and 100 deletions

2
.env
View File

@ -7,4 +7,4 @@ VITE_APP_TITLE = 借还柜
VITE_ROUTER_HISTORY = hash VITE_ROUTER_HISTORY = hash
## 是否开启 console 调试工具 ## 是否开启 console 调试工具
VITE_CONSOLE = false VITE_CONSOLE = true

View File

@ -5,3 +5,6 @@ VITE_BASE_URL = http://localhost:8090/api
## 开发环境域名和静态资源公共路径(一般 / 或 ./ 都可以) ## 开发环境域名和静态资源公共路径(一般 / 或 ./ 都可以)
VITE_PUBLIC_PATH = / VITE_PUBLIC_PATH = /
# 后端地址
VITE_APP_BASE_API = '/dev-api'

View File

@ -4,3 +4,6 @@
VITE_BASE_URL = '/shop-api/api' VITE_BASE_URL = '/shop-api/api'
## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/mobvue/ 域名下就需要填写 /mobvue/ ## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/mobvue/ 域名下就需要填写 /mobvue/
VITE_PUBLIC_PATH = /shop/ VITE_PUBLIC_PATH = /shop/
# 后端地址
VITE_APP_BASE_API = '/shop-back-end'

View File

@ -5,3 +5,6 @@ VITE_BASE_URL = '/shop-api/api'
## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/ 域名下就需要填写 / ## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/ 域名下就需要填写 /
VITE_PUBLIC_PATH = /shop/ VITE_PUBLIC_PATH = /shop/
# 后端地址
VITE_APP_BASE_API = '/shop-back-end'

View File

@ -1,6 +1,5 @@
export interface SubmitApprovalRequestData { export interface SubmitApprovalRequestData {
orderId: number orderGoodsId: number
goodsId: number
returnQuantity: number returnQuantity: number
returnImages: string returnImages: string
} }

View File

@ -1,32 +1,41 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, watch } from 'vue'
import { showConfirmDialog, showSuccessToast } from 'vant' import { showConfirmDialog, showSuccessToast, showToast, UploaderFileListItem } from 'vant'
import axios from "axios"
import { submitApprovalApi } from '@/common/apis/approval' import { submitApprovalApi } from '@/common/apis/approval'
import type { SubmitApprovalRequestData } from '@/common/apis/approval/type' import type { SubmitApprovalRequestData } from '@/common/apis/approval/type'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const { VITE_APP_BASE_API } = import.meta.env;
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const formData = ref<SubmitApprovalRequestData>({ const formData = ref<SubmitApprovalRequestData>({
orderId: Number(route.query.orderId), orderGoodsId: Number(route.query.orderGoodsId),
goodsId: Number(route.query.goodsId),
returnQuantity: 1, returnQuantity: 1,
returnImages: '' returnImages: ''
}) })
watch(() => route.query.orderGoodsId, (newVal) => {
formData.value.orderGoodsId = newVal ? Number(newVal) : NaN
})
const orderId = ref(Number(route.query.orderId))
console.log('orderId: ', orderId.value)
watch(() => route.query.orderId, (newVal) => {
orderId.value = newVal? Number(newVal) : NaN
console.log('orderId: ', orderId.value)
})
const submitting = ref(false) const submitting = ref(false)
const fileList = ref<Array<{ url: string }>>([]) const fileList = ref<UploaderFileListItem[]>([])
const uploading = ref(false)
const validateForm = () => { const validateForm = () => {
if (!formData.value.orderId || isNaN(formData.value.orderId)) { if (!formData.value.orderGoodsId || isNaN(formData.value.orderGoodsId)) {
showConfirmDialog({ title: '错误', message: '订单ID参数错误' }) showConfirmDialog({ title: '错误', message: '订单ID参数错误' })
return false return false
} }
if (!formData.value.goodsId || isNaN(formData.value.goodsId)) {
showConfirmDialog({ title: '错误', message: '商品ID参数错误' })
return false
}
if (formData.value.returnQuantity < 1) { if (formData.value.returnQuantity < 1) {
showConfirmDialog({ title: '提示', message: '退还数量至少为1' }) showConfirmDialog({ title: '提示', message: '退还数量至少为1' })
return false return false
@ -34,20 +43,63 @@ const validateForm = () => {
return true return true
} }
const handleFileUpload = async (items: UploaderFileListItem | UploaderFileListItem[]) => {
const files = Array.isArray(items) ? items : [items]
uploading.value = true
try {
const uploadPromises = files.map(async (item) => {
item.status = 'uploading'
item.message = '上传中...'
const file = item.file as File
const formData = new FormData()
formData.append('file', file)
const { data } = await axios.post<{
code: number
data: { url: string }
message?: string
}>(VITE_APP_BASE_API + '/file/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
if (data.code !== 0) {
throw new Error(data.message || '文件上传失败')
}
return { url: data.data.url }
})
const urls = await Promise.all(uploadPromises)
files.forEach((item, index) => {
item.status = 'done'
item.message = ''
item.url = urls[index].url
})
showToast('上传成功')
} catch (error) {
showConfirmDialog({
title: '上传失败',
message: error instanceof Error ? error.message : '未知错误'
})
} finally {
uploading.value = false
}
}
const handleSubmit = async () => { const handleSubmit = async () => {
if (!validateForm()) return if (!validateForm()) return
submitting.value = true submitting.value = true
try { try {
formData.value.returnImages = fileList.value.map(f => f.url).join(',') formData.value.returnImages = fileList.value.map(item => item.url).join(',')
const { code } = await submitApprovalApi(formData.value) const { code, data } = await submitApprovalApi(formData.value)
if (code === 0) { if (code === 0) {
showSuccessToast('提交成功') showSuccessToast('提交成功')
router.push({ name: 'approval-result', query: { id: formData.value.orderId } }) router.push('/order/' + orderId)
} }
} catch (error) { } catch (error) {
console.error('提交失败:', error)
showConfirmDialog({ showConfirmDialog({
title: '提交失败', title: '提交失败',
message: error instanceof Error ? error.message : '网络请求异常' message: error instanceof Error ? error.message : '网络请求异常'
@ -60,46 +112,24 @@ const handleSubmit = async () => {
<template> <template>
<div class="approval-container"> <div class="approval-container">
<van-nav-bar <van-nav-bar title="退货审批" left-text="返回" left-arrow fixed @click-left="() => router.go(-1)" />
title="退货审批"
left-text="返回"
left-arrow
fixed
@click-left="() => router.go(-1)"
/>
<div class="content-wrapper"> <div class="content-wrapper">
<van-cell-group> <van-cell-group>
<!-- 移除订单ID和商品ID的输入框 --> <!-- 移除订单ID和商品ID的输入框 -->
<van-field <van-field v-model="formData.returnQuantity" label="退还数量" type="number" :min="1" />
v-model="formData.returnQuantity"
label="退还数量"
type="number"
:min="1"
/>
</van-cell-group> </van-cell-group>
<van-cell-group class="upload-section"> <van-cell-group class="upload-section">
<van-cell title="凭证图片"> <van-cell title="凭证图片">
<template #extra> <template #extra>
<van-uploader <van-uploader v-model="fileList" multiple max-count="3" :after-read="handleFileUpload" />
v-model="fileList"
multiple
max-count="3"
:after-read="() => {}"
/>
</template> </template>
</van-cell> </van-cell>
</van-cell-group> </van-cell-group>
<div class="submit-bar"> <div class="submit-bar">
<van-button <van-button type="primary" block :loading="submitting" loading-text="提交中..." @click="handleSubmit">
type="primary"
block
:loading="submitting"
loading-text="提交中..."
@click="handleSubmit"
>
提交申请 提交申请
</van-button> </van-button>
</div> </div>

View File

@ -30,13 +30,22 @@ const order = computed(() => {
return orderStore.orders.find(o => o.orderId === orderId.value) return orderStore.orders.find(o => o.orderId === orderId.value)
}) })
const orderGoodsStatusMap: Record<number, string> = {
5: '审核中',
2: '已退货',
6: '退货未通过'
}
const getOrderGoodsStatusText = (status: number) => {
return orderGoodsStatusMap[status] || ''
}
const handleRefund = (item: any) => { const handleRefund = (item: any) => {
if (order.value?.status !== 4) return
router.push({ router.push({
path: '/approval/submit', path: '/approval/submit',
query: { query: {
goodsId: item.goodsInfo.goodsId, orderGoodsId: item.orderGoods.orderGoodsId,
orderId: order.value.orderId orderId: orderId.value,
} }
}) })
} }
@ -85,8 +94,8 @@ onMounted(() => {
<div class="action-row"> <div class="action-row">
<p>数量: {{ item.orderGoods.quantity }}</p> <p>数量: {{ item.orderGoods.quantity }}</p>
<p>小计: ¥{{ (item.orderGoods.price * item.orderGoods.quantity).toFixed(2) }}</p> <p>小计: ¥{{ (item.orderGoods.price * item.orderGoods.quantity).toFixed(2) }}</p>
<template v-if="item.orderGoods.status === 1">
<van-button <van-button
v-if="order.status === 4"
type="primary" type="primary"
size="mini" size="mini"
class="refund-btn" class="refund-btn"
@ -94,6 +103,14 @@ onMounted(() => {
> >
退还 退还
</van-button> </van-button>
</template>
<span
v-if="[2,5,6].includes(item.orderGoods.status)"
class="status-text"
:class="`status-${item.orderGoods.status}`"
>
{{ getOrderGoodsStatusText(item.orderGoods.status) }}
</span>
</div> </div>
</div> </div>
</van-cell> </van-cell>
@ -171,12 +188,15 @@ goods-info {
color: #fff; color: #fff;
background: #ee0a24; background: #ee0a24;
border-radius: 15px; border-radius: 15px;
padding: 0 12px; padding: 8px 20px;
font-size: 14px;
min-width: 80px;
} }
.action-row { .action-row {
display: flex; display: flex;
flex-direction: column;
gap: 8px; gap: 8px;
align-items: center; align-items: flex-end;
} }
</style> </style>

View File

@ -30,5 +30,6 @@ declare module 'vue' {
VanTabbar: typeof import('vant/es')['Tabbar'] VanTabbar: typeof import('vant/es')['Tabbar']
VanTabbarItem: typeof import('vant/es')['TabbarItem'] VanTabbarItem: typeof import('vant/es')['TabbarItem']
VanTag: typeof import('vant/es')['Tag'] VanTag: typeof import('vant/es')['Tag']
VanUploader: typeof import('vant/es')['Uploader']
} }
} }