From ac5d9292ca7ba2a88e9260462f812586d69b99c0 Mon Sep 17 00:00:00 2001 From: dzq <dzq@ys.com> Date: Wed, 9 Apr 2025 16:41:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AE=A1=E6=89=B9):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=AE=A1=E6=89=B9=E5=A4=84=E7=90=86=E9=A1=B5=E9=9D=A2=E5=8F=8A?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增审批处理页面,支持审批状态的更新、退款金额的填写、审核说明的输入以及审核凭证的上传。同时,优化了审批列表页面的交互,点击列表项可跳转至审批处理页面。引入了Pinia状态管理,用于存储当前审批单的详细信息。 --- src/common/apis/approval/index.ts | 12 +- src/common/apis/approval/type.ts | 10 ++ src/pages/approval/handle.vue | 261 ++++++++++++++++++++++++++++++ src/pages/approval/list.vue | 236 ++++++++++++--------------- src/pinia/stores/approval.ts | 25 +++ src/router/index.ts | 10 ++ 6 files changed, 422 insertions(+), 132 deletions(-) create mode 100644 src/pages/approval/handle.vue create mode 100644 src/pinia/stores/approval.ts diff --git a/src/common/apis/approval/index.ts b/src/common/apis/approval/index.ts index 617fd99..98e814c 100644 --- a/src/common/apis/approval/index.ts +++ b/src/common/apis/approval/index.ts @@ -1,5 +1,5 @@ import { request } from '@/http/axios' -import { SubmitApprovalRequestData, SubmitApprovalResponseData, SearchApiReturnApprovalQuery, ApiResponsePageData, ReturnApprovalEntity } from './type' +import { SubmitApprovalRequestData, SubmitApprovalResponseData, SearchApiReturnApprovalQuery, ApiResponsePageData, ReturnApprovalEntity, HandleApprovalRequestData } from './type' export const getApprovalListApi = (params: SearchApiReturnApprovalQuery) => { return request<ApiResponsePageData<ReturnApprovalEntity>>({ @@ -9,6 +9,8 @@ export const getApprovalListApi = (params: SearchApiReturnApprovalQuery) => { }) } + + export const submitApprovalApi = (data: SubmitApprovalRequestData) => { return request<SubmitApprovalResponseData>({ url: 'approval/submit', @@ -16,3 +18,11 @@ export const submitApprovalApi = (data: SubmitApprovalRequestData) => { data }) } + +export const handleApprovalApi = (data: HandleApprovalRequestData) => { + return request<ApiResponseMsgData<string>>({ + url: 'approval/handle', + method: 'post', + data + }) +} diff --git a/src/common/apis/approval/type.ts b/src/common/apis/approval/type.ts index bcc20c0..42c19fe 100644 --- a/src/common/apis/approval/type.ts +++ b/src/common/apis/approval/type.ts @@ -5,6 +5,16 @@ export interface SubmitApprovalRequestData { returnRemark: string } +export interface HandleApprovalRequestData { + /** 审批ID */ + approvalId: number + /** 审批状态 */ + status: number + returnAmount: number + auditImages: string + auditRemark: string +} + export interface SearchApiReturnApprovalQuery { pageNum: number pageSize: number diff --git a/src/pages/approval/handle.vue b/src/pages/approval/handle.vue new file mode 100644 index 0000000..c6f6b88 --- /dev/null +++ b/src/pages/approval/handle.vue @@ -0,0 +1,261 @@ +<script setup lang="ts"> +import { ref } from 'vue' +import { showConfirmDialog, showSuccessToast, showToast, UploaderFileListItem, Popup, Picker } from 'vant' + +const showStatusPicker = ref(false) +const showPreview = ref(false) +const currentPreviewImage = ref('') +const statusMap: { [key: number]: string } = { + 1: '待审核', + 2: '已通过', + 3: '已驳回' +} + +const statusOptions = [ + { text: '已通过', value: 1 }, + { text: '待审核', value: 2 }, + { text: '已驳回', value: 3 } +] +import axios from "axios" +import { handleApprovalApi } from '@/common/apis/approval' +import type { HandleApprovalRequestData } from '@/common/apis/approval/type' +import { useRoute, useRouter } from 'vue-router' +import { useApprovalStore } from '@/pinia/stores/approval' + +const { VITE_APP_BASE_API } = import.meta.env; +const router = useRouter() +const route = useRoute() +const approvalStore = useApprovalStore() + +const formData = ref<HandleApprovalRequestData>({ + approvalId: approvalStore.currentApproval?.approvalId || 0, + status: 1, + returnAmount: 0, + auditImages: '', + auditRemark: '' +}) + +const submitting = ref(false) +const fileList = ref<UploaderFileListItem[]>([]) +const uploading = ref(false) + +const validateForm = () => { + if (!formData.value.approvalId || isNaN(formData.value.approvalId)) { + showConfirmDialog({ title: '错误', message: '审批单ID参数错误' }) + return false + } + if (formData.value.returnAmount < 0) { + showConfirmDialog({ title: '提示', message: '退款金额不能为负数' }) + return false + } + if (formData.value.status === 3 && !formData.value.auditRemark) { + showConfirmDialog({ title: '提示', message: '驳回时必须填写审核说明' }) + return false + } + 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 + }) + formData.value.auditImages = fileList.value.map(item => item.url).join(',') + } catch (error) { + showConfirmDialog({ + title: '上传失败', + message: error instanceof Error ? error.message : '未知错误' + }) + } finally { + uploading.value = false + } +} + +const onStatusConfirm = ({ selectedOptions }: { selectedOptions: { value: number }[] }) => { + submitting.value = true + try { + if (!selectedOptions?.[0]?.value) { + throw new Error('请选择有效的审批状态') + } + formData.value.status = selectedOptions[0].value + showStatusPicker.value = false + } catch (error) { + showConfirmDialog({ + title: '操作失败', + message: error instanceof Error ? error.message : '状态选择异常' + }) + } finally { + submitting.value = false + } +} + +const previewImage = (url: string) => { + currentPreviewImage.value = url + showPreview.value = true +} +onMounted(() => { + if (!approvalStore.currentApproval) { + router.push('/approval/list') + } +}) + +const handleSubmit = async () => { + if (!validateForm()) return + + submitting.value = true + try { + const { code } = await handleApprovalApi(formData.value) + + if (code === 0) { + showSuccessToast('操作成功') + await showConfirmDialog({ + title: "操作成功", + message: `审批处理已完成` + }) + router.push('/approval/list') + } + } catch (error) { + console.error('提交失败:', error) + showConfirmDialog({ + title: '提交失败', + message: error instanceof Error ? error.message : '网络请求异常' + }) + } finally { + submitting.value = false + } +} +</script> + +<template> + <div class="approval-container"> + <van-nav-bar title="审批处理" left-text="返回" left-arrow fixed @click-left="() => router.go(-1)" /> + + <div class="content-wrapper"> + <van-cell-group> + <van-cell title="商品名称" :value="approvalStore.currentApproval?.goodsName" /> + <van-cell title="退还数量" :value="approvalStore.currentApproval?.returnQuantity" /> + <van-cell title="商品单价" :value="`¥${approvalStore.currentApproval?.goodsPrice}`" /> + <van-cell title="当前状态" :value="statusMap[approvalStore.currentApproval?.status || 1]" /> + </van-cell-group> + + <van-cell-group class="image-section"> + <van-cell title="退还凭证"> + <van-grid :column-num="3" gutter="10"> + <van-grid-item v-for="(img, index) in approvalStore.currentApproval?.returnImages.split(',')" + :key="index"> + <van-image :src="img" fit="cover" @click="previewImage(img)" /> + </van-grid-item> + </van-grid> + </van-cell> + </van-cell-group> + + <van-cell-group> + <van-cell title="退还说明" :value="approvalStore.currentApproval?.returnRemark" /> + + <!-- 原有表单字段保持不变 --> + <van-field v-model="formData.returnAmount" label="退款金额" type="number" :min="0" /> + <van-field :model-value="statusOptions.find(opt => opt.value === formData.status)?.text || ''" + label="审批结果" readonly clickable placeholder="请选择审批结果" @click="showStatusPicker = true" /> + <van-field v-model="formData.auditRemark" label="审核说明" type="textarea" rows="2" autosize /> + </van-cell-group> + + <van-popup v-model:show="showStatusPicker" position="bottom"> + <van-picker :columns="statusOptions" @confirm="onStatusConfirm" @cancel="showStatusPicker = false" /> + </van-popup> + + <van-popup v-model:show="showPreview" position="center" :style="{ width: '90%', height: '80%' }" round> + <div class="preview-wrapper"> + <van-icon name="cross" class="close-icon" @click="showPreview = false" /> + <van-image :src="currentPreviewImage" fit="contain" class="preview-image" /> + </div> + </van-popup> + + <van-cell-group class="upload-section"> + <van-cell title="审核凭证"> + <template #extra> + <van-uploader v-model="fileList" multiple max-count="3" :after-read="handleFileUpload" /> + </template> + </van-cell> + </van-cell-group> + + <div class="submit-bar"> + <van-button type="primary" block :loading="submitting" loading-text="提交中..." @click="handleSubmit"> + 提交审批 + </van-button> + </div> + </div> + </div> +</template> + +<style scoped> +.approval-container { + padding: 12px 16px; +} + +.content-wrapper { + padding-top: 46px; +} + +.upload-section { + margin: 20px 0; +} + +.preview-wrapper { + position: relative; + height: 100%; + padding: 20px; + + .close-icon { + position: absolute; + top: 10px; + right: 10px; + z-index: 1; + font-size: 24px; + color: #fff; + background: rgba(0, 0, 0, 0.3); + border-radius: 50%; + padding: 4px; + } + + .preview-image { + width: 100%; + height: 100%; + } +} + +.submit-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 16px; + background: #fff; + box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.1); +} +</style> \ No newline at end of file diff --git a/src/pages/approval/list.vue b/src/pages/approval/list.vue index e46fd22..87b2bed 100644 --- a/src/pages/approval/list.vue +++ b/src/pages/approval/list.vue @@ -1,46 +1,20 @@ <template> - <div class="approval-list-container"> - <!-- 搜索表单 --> - <van-form @submit="handleSearch"> - <van-field - v-model="searchParams.approvalId" - label="审批单号" - type="number" - placeholder="请输入审批单号" - /> - - <van-field - v-model="searchParams.orderId" - label="订单编号" - type="number" - placeholder="请输入订单编号" - /> + <div class="approval-list-container"> + <!-- 搜索表单 --> + <van-form @submit="handleSearch"> + <van-field v-model="searchParams.approvalId" label="审批单号" type="number" placeholder="请输入审批单号" /> - <van-field - v-model="searchParams.goodsId" - label="商品ID" - type="number" - placeholder="请输入商品ID" - /> + <van-field v-model="searchParams.orderId" label="订单编号" type="number" placeholder="请输入订单编号" /> - <van-field - name="status" - label="审批状态" - readonly - clickable - :value="statusText" - placeholder="请选择状态" - @click="showStatusPicker = true" - /> - <van-popup v-model:show="showStatusPicker" position="bottom"> - <van-picker - :columns="statusOptions" - @confirm="onStatusConfirm" - @cancel="showStatusPicker = false" - /> - </van-popup> + <van-field v-model="searchParams.goodsId" label="商品ID" type="number" placeholder="请输入商品ID" /> - <!-- <van-field + <van-field name="status" label="审批状态" readonly clickable v-model="statusText" placeholder="请选择状态" + @click="showStatusPicker = true" /> + <van-popup v-model:show="showStatusPicker" position="bottom"> + <van-picker :columns="statusOptions" @confirm="onStatusConfirm" @cancel="showStatusPicker = false" /> + </van-popup> + + <!-- <van-field readonly clickable name="date" @@ -57,40 +31,26 @@ /> </van-popup> --> - <div style="margin: 16px;"> - <van-button block type="primary" native-type="submit">搜索</van-button> - <van-button - block - plain - type="primary" - style="margin-top: 10px;" - @click="handleReset" - >重置</van-button> - </div> - </van-form> + <div style="margin: 16px;"> + <van-button block type="primary" native-type="submit">搜索</van-button> + <van-button block plain type="primary" style="margin-top: 10px;" @click="handleReset">重置</van-button> + </div> + </van-form> - <!-- 审批列表 --> - <van-list - v-model:loading="loading" - :finished="finished" - finished-text="没有更多了" - @load="onLoad" - > - <van-cell - v-for="item in list" - :key="item.approvalId" - :title="`审批单号:${item.approvalId}`" - > - <template #label> - <div>商品名称:{{ item.goodsName }}</div> - <div>申请时间:{{ item.createTime }}</div> - <van-tag :type="statusTagType(item.status)"> - {{ statusMap[item.status] }} - </van-tag> - </template> - </van-cell> - </van-list> - </div> + <!-- 审批列表 --> + <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> + <van-cell v-for="item in list" :key="item.approvalId" :title="`审批单号:${item.approvalId}`" clickable + @click="handleCellClick(item.approvalId)"> + <template #label> + <div>商品名称:{{ item.goodsName }}</div> + <div>申请时间:{{ item.createTime }}</div> + <van-tag :type="statusTagType(item.status)"> + {{ statusMap[item.status] }} + </van-tag> + </template> + </van-cell> + </van-list> + </div> </template> <script lang="ts" setup> @@ -98,26 +58,29 @@ import { ref, reactive } from 'vue' import { getApprovalListApi } from '@/common/apis/approval' import type { SearchApiReturnApprovalQuery, ReturnApprovalEntity } from '@/common/apis/approval/type' import type { PickerConfirmEventParams } from 'vant/es'; +import { useApprovalStore } from '@/pinia/stores/approval'; + +const router = useRouter() // 搜索参数 const searchParams = reactive<SearchApiReturnApprovalQuery>({ - pageNum: 1, - pageSize: 10, + pageNum: 1, + pageSize: 10, }) // 状态选择相关 const showStatusPicker = ref(false) const statusOptions = [ - { text: '全部', value: undefined }, - { text: '待审核', value: 0 }, - { text: '已通过', value: 1 }, - { text: '已拒绝', value: 2 }, + { text: '全部', value: undefined }, + { text: '待审核', value: 1 }, + { text: '已通过', value: 2 }, + { text: '已驳回', value: 3 }, ] const statusMap: { [key: number]: string } = { - 0: '待审核', - 1: '已通过', - 2: '已拒绝' + 1: '待审核', + 2: '已通过', + 3: '已驳回' } const statusText = ref('') @@ -131,88 +94,99 @@ const list = ref<ReturnApprovalEntity[]>([]) const loading = ref(false) const finished = ref(false) +const handleCellClick = (approvalId: number) => { + const approvalStore = useApprovalStore() + const currentItem = list.value.find(item => item.approvalId === approvalId) + if (currentItem) { + approvalStore.setCurrentApproval(currentItem) + } + router.push(`/approval/handle/${approvalId}`) +} + // 状态标签类型 const statusTagType = (status: number) => { - switch (status) { - case 0: return 'warning' - case 1: return 'success' - case 2: return 'danger' - default: return 'default' - } + switch (status) { + case 1: return 'warning' + case 2: return 'success' + case 3: return 'danger' + default: return 'default' + } } // 处理状态选择 const onStatusConfirm = (e: PickerConfirmEventParams) => { - const { selectedOptions } = e; - searchParams.status = Number(selectedOptions[0]?.value); - showStatusPicker.value = false; - // 确保赋值给 statusText 的是字符串类型 - statusText.value = String(selectedOptions[0]?.text || ''); + const { selectedOptions } = e; + searchParams.status = selectedOptions[0]?.value ? Number(selectedOptions[0]?.value) : undefined; + showStatusPicker.value = false; + // 确保赋值给 statusText 的是字符串类型 + statusText.value = String(selectedOptions[0]?.text || ''); } // 处理时间选择 const onDateConfirm = (values: Date[]) => { - const [start, end] = values - searchParams.startTime = start.toISOString().split('T')[0] - searchParams.endTime = end.toISOString().split('T')[0] - dateRangeText.value = `${searchParams.startTime} 至 ${searchParams.endTime}` - showDatePicker.value = false + const [start, end] = values + searchParams.startTime = start.toISOString().split('T')[0] + searchParams.endTime = end.toISOString().split('T')[0] + dateRangeText.value = `${searchParams.startTime} 至 ${searchParams.endTime}` + showDatePicker.value = false } // 搜索处理 const handleSearch = () => { - list.value = [] - searchParams.pageNum = 1 - onLoad() + list.value = [] + searchParams.pageNum = 1 + onLoad() } // 重置表单 const handleReset = () => { - Object.assign(searchParams, { - pageNum: 1, - pageSize: 10, - approvalId: undefined, - orderId: undefined, - goodsId: undefined, - status: undefined, - startTime: undefined, - endTime: undefined - }) - statusText.value = '' - dateRangeText.value = '' - handleSearch() + Object.assign(searchParams, { + pageNum: 1, + pageSize: 10, + approvalId: undefined, + orderId: undefined, + goodsId: undefined, + status: undefined, + startTime: undefined, + endTime: undefined + }) + statusText.value = '' + dateRangeText.value = '' + handleSearch() } // 加载数据 const onLoad = async () => { - try { - const { data } = await getApprovalListApi(searchParams) - list.value.push(...data.rows) - loading.value = false + try { + const { data } = await getApprovalListApi(searchParams) + list.value.push(...data.rows) + loading.value = false - if (list.value.length >= data.total) { - finished.value = true - } else { - searchParams.pageNum++ + if (list.value.length >= data.total) { + finished.value = true + } else { + searchParams.pageNum++ + } + } catch (error) { + console.error('获取审批列表失败', error) + finished.value = true } - } catch (error) { - console.error('获取审批列表失败', error) - finished.value = true - } } </script> <style scoped> .approval-list-container { - padding: 12px; + padding: 12px; } + .van-cell__title { - font-size: 14px; - color: #333; + font-size: 14px; + color: #333; } + .van-cell__label { - margin-top: 8px; - color: #666; - font-size: 12px; + margin-top: 8px; + color: #666; + font-size: 12px; } </style> \ No newline at end of file diff --git a/src/pinia/stores/approval.ts b/src/pinia/stores/approval.ts new file mode 100644 index 0000000..164652b --- /dev/null +++ b/src/pinia/stores/approval.ts @@ -0,0 +1,25 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { ReturnApprovalEntity } from '@/common/apis/approval/type' + +export interface ApprovalDetail extends ReturnApprovalEntity { + goodsName: string + coverImg: string +} + +export const useApprovalStore = defineStore('approval', () => { + const currentApproval = ref<ApprovalDetail | null>(null) + + const setCurrentApproval = (approval: ApprovalDetail) => { + currentApproval.value = approval + } + + return { + currentApproval, + setCurrentApproval + } +}) + +export function useApprovalStoreOutside() { + return useApprovalStore() +} \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index 1572027..62b9a4e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -34,6 +34,11 @@ export const routes: RouteRecordRaw[] = [ component: () => import('@/pages/approval/submit.vue'), meta: { requiresAuth: true } }, + { + path: '/approval/handle/:approvalId', + component: () => import('@/pages/approval/handle.vue'), + meta: { requiresAuth: true } + }, { path: '/order-success', name: 'OrderSuccess', @@ -151,6 +156,11 @@ export const routes: RouteRecordRaw[] = [ component: () => import('@/pages/approval/submit.vue'), meta: { requiresAuth: true } }, + { + path: '/approval/handle/:approvalId', + component: () => import('@/pages/approval/handle.vue'), + meta: { requiresAuth: true } + }, { path: '/order-success', name: 'OrderSuccess',