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',