瀏覽代碼

fix:同步web端的样式与操作

LYK 3 周之前
父節點
當前提交
cde6bc387a

+ 16 - 0
src/api/platform/animal/index.ts

@@ -14,6 +14,10 @@ export function usePlatAnimalCageApplicationApi() {
     create(params?: Object) {
       return request.postRequest(basePath, 'PlatAnimalCageApplication', 'Create', params)
     },
+    // 重新提交
+    reSubmit(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalCageApplication', 'Submit', params)
+    },
     // 列表
     getList(params?: Object) {
       return request.postRequest(basePath, 'PlatAnimalCageApplication', 'GetApplicationList', params)
@@ -26,6 +30,10 @@ export function usePlatAnimalCageApplicationApi() {
     updateById(params?: Object) {
       return request.postRequest(basePath, 'PlatAnimalCageApplication', 'UpdateApplication', params)
     },
+    // 撤回
+    quashApprove(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalCageApplication', 'QuashApprove', params)
+    },
     // 动物类型列表
     getAnimalTypeList(params?: Object) {
       return request.postRequest(basePath, 'PlatAnimals', 'GetVendorAnimalCategoires', params)
@@ -82,10 +90,18 @@ export function usePlatAnimalCageApplicationApi() {
     createAnimalTakeawayApplications(params?: Object) {
       return request.postRequest(basePath, 'PlatAnimalTakeawayApplications', 'Create', params)
     },
+    // 提交/重新提交申请带离
+    submitAnimalTakeawayApplications(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalTakeawayApplications', 'Submit', params)
+    },
     // 更新申请带离
     updateAnimalTakeawayApplications(params?: Object) {
       return request.postRequest(basePath, 'PlatAnimalTakeawayApplications', 'Update', params)
     },
+    // 撤回申请带离
+    quashApproveAnimalTakeawayApplications(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalTakeawayApplications', 'QuashApprove', params)
+    },
     // 删除申请带离
     deleteAnimalTakeawayApplications(params?: Object) {
       return request.postRequest(basePath, 'PlatAnimalTakeawayApplications', 'Delete', params)

+ 4 - 0
src/api/platform/home/index.ts

@@ -8,6 +8,7 @@
  */
 import request from '/@/utils/micro_request.js';
 const basePath = import.meta.env.VITE_PLATFORM_API;
+const adminPath = import.meta.env.VITE_ADMIN;
 export function usePlatformApi() {
   return {
     // 平台管理
@@ -74,6 +75,9 @@ export function usePlatformApi() {
     // 入室获取用户是否有证书
     onCheckUserCertificate(params?: Object) {
       return request.postRequest(basePath, 'PlatPlatform', 'CheckUserCertificate', params)
+    },
+    getUserPlatformList(params?: Object) {
+      return request.postRequest(adminPath, 'Platform', 'GetListByUser', params)
     }
   }
 }

+ 2 - 1
src/view/Application.vue

@@ -669,6 +669,7 @@
 
       const params = {
         ...deepClone(state.form),
+        categoryId: state.form.categoryId != null ? String(state.form.categoryId) : null,
         categoryName: animalTypeList.value.find((item) => item.id == state.form.categoryId)?.name,
         projectGroupName: projects.value.find((item) => item.id == state.form.projectGroupId)?.projectName,
         startDate: dayjs(state.form.startDate).format(DateFormat),
@@ -732,4 +733,4 @@ ul {
 :deep(.el-checkbox) {
   white-space: pre-wrap;
 }
-</style>
+</style>

+ 84 - 8
src/view/animal/application/components/Application.vue

@@ -283,6 +283,7 @@ const animalTypeList = ref<any[]>([])
 const submitting = ref<boolean>(false) // 提交状态,用于控制按钮加载状态
 
 const defaultFormData = {
+  id: 0,
   projectGroupName: '',
   projectGroupId: null,
   projectName: '',
@@ -326,7 +327,7 @@ const state = reactive({
 })
 
 const getDicts = () => {
-  Promise.all([
+  return Promise.all([
     platAnimalCageApplicationApi.getAnimalTypeList({}),
     platAnimalCageApplicationApi.getProjectGroup({}),
   ]).then(([animalType, projectGroup]) => {
@@ -359,11 +360,84 @@ const resetForm = () => {
   safePromise.value = false
 }
 
+const isValidJsonArray = (value: any) => {
+  if (Array.isArray(value)) return true
+  if (typeof value !== 'string') return false
+  try {
+    const parsed = JSON.parse(value)
+    return Array.isArray(parsed)
+  } catch {
+    return false
+  }
+}
+
+const parseRangeField = (value: any) => {
+  if (!value) return { min: null, max: null }
+  if (typeof value === 'object' && value.min !== undefined && value.max !== undefined) {
+    return { min: value.min ?? null, max: value.max ?? null }
+  }
+  if (typeof value === 'string') {
+    try {
+      const cleanStr = value.replace(/\\"/g, '"').replace(/^"|"$/g, '')
+      const parsed = JSON.parse(cleanStr)
+      return { min: parsed?.min ?? null, max: parsed?.max ?? null }
+    } catch {
+      return { min: null, max: null }
+    }
+  }
+  return { min: null, max: null }
+}
+
 // 打开弹窗
-const openDialog = () => {
+const openDialog = async (type: 'add' | 'edit', row?: any) => {
   resetForm()
-  getDicts()
-  state.dialog.title = '新增实验动物笼位申请'
+  await getDicts()
+
+  state.dialog.type = type
+  state.dialog.title = type === 'edit' ? '重新提交实验动物笼位申请' : '新增实验动物笼位申请'
+
+  if (type === 'edit' && row) {
+    const categoryId = row.categoryId ?? row.category_id ?? null
+    const categoryName = row.category_name ?? row.categoryName ?? ''
+    const projectGroupName = row.projectGroupName ?? row.project_name ?? row.projectName ?? ''
+    const projectGroupId = row.projectGroupId ?? row.projectGroup_id ?? null
+    const level = row.level ?? row.level_id ?? row.levelId ?? null
+    const levelNameFromRow = row.levelName ?? row.level_name ?? ''
+    const normalizedLevel = level != null ? Number(level) : null
+    const normalizedLevelName =
+      levelNameFromRow || (normalizedLevel != null ? LeavelList.find((item) => item.id === normalizedLevel)?.name || '' : '')
+
+    state.form = {
+      ...deepClone(defaultFormData),
+      ...row,
+      id: Number(row.id) || 0,
+      categoryId: categoryId != null ? Number(categoryId) : null,
+      categoryName,
+      projectGroupId: projectGroupId != null ? Number(projectGroupId) : null,
+      projectGroupName,
+      projectName: row.projectName || projectGroupName,
+      level: normalizedLevel,
+      levelName: normalizedLevelName,
+      age: parseRangeField(row.age),
+      weight: parseRangeField(row.weight),
+    }
+
+    state.form.licenseNumberFile = isValidJsonArray(row.licenseNumberFile) ? JSON.parse(row.licenseNumberFile) : []
+    state.form.animalTestDateFile = isValidJsonArray(row.animalTestDateFile) ? JSON.parse(row.animalTestDateFile) : []
+    state.form.geneIdentificationFile = isValidJsonArray(row.geneIdentificationFile)
+      ? JSON.parse(row.geneIdentificationFile)
+      : []
+    state.form.ethicsCheckFile = isValidJsonArray(row.ethicsCheckFile) ? JSON.parse(row.ethicsCheckFile) : []
+    state.form.ethicsAdviceFile = isValidJsonArray(row.ethicsAdviceFile) ? JSON.parse(row.ethicsAdviceFile) : []
+
+    licenseNumberFileList.value = (state.form.licenseNumberFile || []).map((f: any) => ({ url: f.url, name: f.name }))
+    animalTestDateFileList.value = (state.form.animalTestDateFile || []).map((f: any) => ({ url: f.url, name: f.name }))
+    geneIdentificationFileList.value = (state.form.geneIdentificationFile || []).map((f: any) => ({ url: f.url, name: f.name }))
+    ethicsCheckFileList.value = (state.form.ethicsCheckFile || []).map((f: any) => ({ url: f.url, name: f.name }))
+    ethicsAdviceFileList.value = (state.form.ethicsAdviceFile || []).map((f: any) => ({ url: f.url, name: f.name }))
+  }
+
+  safePromise.value = false
   state.dialog.isShowDialog = true
 }
 
@@ -592,8 +666,10 @@ const onSubmit = async () => {
 
   const params = {
     ...deepClone(state.form),
-    categoryName: animalTypeList.value.find((item) => item.id == state.form.categoryId)?.name,
-    projectGroupName: projects.value.find((item) => item.id == state.form.projectGroupId)?.projectName,
+    categoryId: state.form.categoryId != null ? String(state.form.categoryId) : null,
+    categoryName: animalTypeList.value.find((item) => item.id == state.form.categoryId)?.name || state.form.categoryName,
+    projectGroupName:
+      projects.value.find((item) => item.id == state.form.projectGroupId)?.projectName || state.form.projectGroupName,
     startDate: dayjs(state.form.startDate).format('YYYY-MM-DD'),
     comeTime: state.form.comeTime ? dayjs(state.form.comeTime).format('YYYY-MM-DD') : '',
     licenseNumberFile: JSON.stringify(state.form.licenseNumberFile),
@@ -614,7 +690,7 @@ const onSubmit = async () => {
     }
   })
 
-  const post = platAnimalCageApplicationApi.create
+  const post = state.dialog.type === 'edit' ? platAnimalCageApplicationApi.reSubmit : platAnimalCageApplicationApi.create
   const [err]: ToResponse = await to(post(params))
   if (err) {
     submitting.value = false // 重置提交状态
@@ -766,4 +842,4 @@ defineExpose({
   top: 16px;
   right: 16px;
 }
-</style>
+</style>

+ 70 - 3
src/view/animal/application/index.vue

@@ -185,6 +185,22 @@
               </p>
               <footer class="flex justify-between mt16">
                 <span class="title">
+                  <el-button
+                    v-if="item.approveStatus === ApproveStatus.APPROVING.toString() && userInfos.id === item.userId"
+                    style="height: 25px"
+                    type="danger"
+                    @click.stop="onWithdraw(item.id)"
+                  >
+                    撤回
+                  </el-button>
+                  <el-button
+                    v-if="item.approveStatus === ApproveStatus.REVOKE.toString() && userInfos.id === item.userId"
+                    style="height: 25px"
+                    type="primary"
+                    @click.stop="onResubmit(item)"
+                  >
+                    重新提交
+                  </el-button>
                   <el-button
                     v-if="
                       item.approveStatus === ApproveStatus.PASS.toString() &&
@@ -252,20 +268,25 @@
   import dayjs from 'dayjs'
   import { useRouter, useRoute } from 'vue-router'
   import { Calendar, Close } from '@element-plus/icons-vue'
+  import { showConfirmDialog } from 'vant'
 
   import { formatDate } from '/@/utils/formatTime'
   import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+  import { useExecutionApi } from '/@/api/platform/execution'
+  import { usePlatformApi } from '/@/api/platform/home'
   import { ApproveStatus, ReturnStatus, LeavelList, ApproveStatusList } from '/@/constants/pageConstants'
   import { useUserInfos } from '/@/hooks/useUserInfos'
   import { ElMessage } from 'element-plus'
 
   const ApplicationModal = defineAsyncComponent(() => import('/@/view/animal/application/components/Application.vue'))
   const DetailModal = defineAsyncComponent(() => import('/@/view/animal/application/components/Detail.vue'))
+  const platformApi = usePlatformApi()
   const ReturnCageDialog = defineAsyncComponent(
     () => import('/@/view/animal/application/components/ReturnCageDialog.vue'),
   )
 
   const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+  const executionApi = useExecutionApi()
 
   const { userInfos } = useUserInfos()
   const router = useRouter()
@@ -416,17 +437,63 @@
     returnCageDialogRef.value.handleOpenRefundableDialog(row.id)
   }
 
-  const handleApplication = async () => {
+  const openCageApplicationModal = async (type: 'add' | 'edit', row?: any) => {
     const response = await platAnimalCageApplicationApi.checkAnimalCert({});
     const { check, message } = response?.data;
-    console.log("1111", check)
     if (check) {
-      cageApplicationModalRef.value.openDialog();
+      cageApplicationModalRef.value.openDialog(type, row);
     } else {
       ElMessage.error(message + ' ')
     }
   }
 
+
+
+  const handleApplication = async () => {
+    openCageApplicationModal('add')
+  }
+
+  const onWithdraw = async (id: number) => {
+    try {
+      await showConfirmDialog({
+        title: '提示',
+        message: '确认撤回?',
+        confirmButtonText: '确认',
+        cancelButtonText: '取消',
+      })
+    } catch {
+      return
+    }
+
+       const [platformErr, platformRes]: ToResponse = await to(platformApi.getUserPlatformList({}))
+    if (platformErr) return
+    const platformList = ((platformRes as any)?.data?.list || (platformRes as any)?.data || []) as any[]
+    const experimentPlatform =
+      platformList.find((item: any) => item.platName === '实验平台') ||
+      platformList.find((item: any) => item.platformName === '实验平台')
+    if (!experimentPlatform) {
+      ElMessage.error('未找到实验平台,请联系管理员')
+      return
+    }
+
+    const params = {
+      businessCode: id.toString(),
+      platformId: experimentPlatform.id,
+      defCode: 'plat_cage_applications',
+    }
+
+    const [err]: ToResponse = await to(executionApi.withdraw(params))
+    if (err) return
+    const [e]: ToResponse = await to(platAnimalCageApplicationApi.quashApprove({ id }))
+    if (e) return
+    ElMessage.success('撤回成功')
+    handleRefresh()
+  }
+
+  const onResubmit = (row: any) => {
+    openCageApplicationModal('edit', row)
+  }
+
   const handleRefresh = () => {
     resetQueryParams()
     onLoad()

+ 247 - 10
src/view/animal/applicationRemoval/components/addEdit.vue

@@ -30,7 +30,6 @@
                 :rules="rules.takeawayReason" />
               <van-field v-model="state.form.takeawayTransport" label="运输方式" placeholder="请输入" :readonly="isReadOnly" required
                 :rules="rules.takeawayTransport" />
-<!--               <van-field v-model="state.form.accessCardNumber" label="门禁卡序列号" placeholder="请输入" :readonly="isReadOnly" />-->
               <van-field v-model="state.form.takeawayMaleNumber" label="雄性" placeholder="雄性数量" type="digit" :readonly="isReadOnly">
                 <template #button>
                   <van-stepper v-model="state.form.takeawayMaleNumber" :min="0" integer :disabled="isReadOnly" />
@@ -43,9 +42,71 @@
               </van-field>
             </van-cell-group>
 
-            <h4 class="mb20 mt20">转回备注</h4>
-            <van-field v-model="isReturnLabel" label="是否转回" placeholder="请选择" :readonly="isReadOnly" :is-link="!isReadOnly" required
-                       @click="!isReadOnly && (showReturnPicker = true)" :rules="rules.isReturn"/>
+            <van-field
+              v-model="isReturnLabel"
+              label="是否转回"
+              placeholder="请选择"
+              :readonly="isReadOnly"
+              :is-link="!isReadOnly"
+              required
+              @click="!isReadOnly && (showReturnPicker = true)"
+              :rules="rules.isReturn"
+            />
+
+            <template v-if="state.dialog.type === 'detail'">
+              <h4 class="mb20 mt20">转回信息</h4>
+              <van-cell-group>
+                <van-field
+                  label="门禁卡是否归还"
+                  :model-value="accessCardReturnText"
+                  readonly
+                />
+                <van-field
+                  label="转回日期"
+                  :model-value="returnDateText"
+                  readonly
+                />
+                <van-field
+                  label="转回数量(雄性+雌性)"
+                  :model-value="returnAnimalNumberText"
+                  readonly
+                />
+                <van-field
+                  v-model="state.form.returnTransport"
+                  label="转回运输方式"
+                  readonly
+                />
+                <van-field
+                  v-model="state.form.notReturnReason"
+                  label="未返回动物情况说明"
+                  readonly
+                />
+              </van-cell-group>
+
+              <h4 class="mb20 mt20">淘汰上报信息</h4>
+              <van-cell-group>
+                <van-field
+                  label="淘汰日期"
+                  :model-value="dieTimeText"
+                  readonly
+                />
+                <van-field
+                  v-model="state.form.dieReason"
+                  label="淘汰原因"
+                  readonly
+                />
+                <van-field
+                  label="淘汰数量(雄性+雌性)"
+                  :model-value="dieAnimalNumberText"
+                  readonly
+                />
+                <van-field
+                  v-model="state.form.location"
+                  label="动物尸体存放位置"
+                  readonly
+                />
+              </van-cell-group>
+            </template>
             <div class="mt30 mb30 checkbox-wrapper">
               <van-checkbox v-model="safePromiseStatus" :disabled="isReadOnly">
                 本人已阅读并同意
@@ -56,6 +117,17 @@
               </van-checkbox>
             </div>
           </van-form>
+
+          <template v-if="showApprovalInfo">
+            <h4 class="mb20 mt20">审批记录</h4>
+            <FlowTable
+              v-if="approvalFlowId"
+              :id="approvalFlowId"
+              :businessCode="approvalBusinessCode"
+              defCode="plat_animal_takeway_applications"
+            />
+            <div v-else class="approval-empty">暂无审批记录</div>
+          </template>
         </div>
         <div class="dialog-footer">
           <van-button
@@ -112,7 +184,7 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, ref, computed } from 'vue'
+import { reactive, ref, computed, defineAsyncComponent } from 'vue'
 import to from 'await-to-js'
 import { showToast, showNotify } from 'vant'
 import type { FormInstance } from 'vant/es'
@@ -123,6 +195,7 @@ import {
   TakeawayList,
   ActionType,
   AnimalRemovalApplicationNotice,
+  ApplyLeaveApproveStatus,
 } from '/@/constants/pageConstants'
 import { deepClone } from '/@/utils/other'
 import { useUserInfos } from '/@/hooks/useUserInfos'
@@ -134,6 +207,8 @@ const emit = defineEmits(['refresh'])
 const { userInfos } = useUserInfos()
 const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
 
+const FlowTable = defineAsyncComponent(() => import('/@/components/FlowTable.vue'))
+
 const expertDialogFormRef = ref<FormInstance>()
 const animalTypeList = ref<{ id: string; name: string }[]>([])
 const safePromiseStatus = ref<boolean>(false)
@@ -145,6 +220,27 @@ const showReturnPicker = ref<boolean>(false)
 
 const isReadOnly = computed(() => state.dialog.type === 'detail')
 
+type FormModel = CreateAnimalApplyLeavePayload & {
+  categoryName?: string
+  id?: number
+  approvalId?: number
+  approveStatus?: string
+  businessCode?: string
+  createdTime?: string
+  createdName?: string
+  returnDate?: string
+  returnFemaleNumber?: number
+  returnMaleNumber?: number
+  returnTransport?: string
+  accessCardReturn?: number
+  dieFemaleNumber?: number
+  dieMaleNumber?: number
+  dieTime?: string
+  dieReason?: string
+  location?: string
+  notReturnReason?: string
+}
+
 const rules = {
   categoryId: [{ required: true, message: '动物类别不能为空' }],
   variety: [{ required: true, message: '品种品系不能为空' }],
@@ -157,7 +253,7 @@ const rules = {
   isReturn: [{ required: true, message: '是否转回不能为空' }],
 }
 
-const defaultFormFields: CreateAnimalApplyLeavePayload & { categoryName?: string } = {
+const defaultFormFields: FormModel = {
   accessCardNumber: '',
   categoryId: null,
   categoryName: '',
@@ -175,10 +271,21 @@ const defaultFormFields: CreateAnimalApplyLeavePayload & { categoryName?: string
   deptId: '',
   deptName: '',
   isReturn: 0,
+  returnDate: '',
+  returnFemaleNumber: 0,
+  returnMaleNumber: 0,
+  returnTransport: '',
+  accessCardReturn: 0,
+  dieFemaleNumber: 0,
+  dieMaleNumber: 0,
+  dieTime: '',
+  dieReason: '',
+  location: '',
+  notReturnReason: '',
 }
 
 const state = reactive<{
-  form: CreateAnimalApplyLeavePayload & { categoryName?: string }
+  form: FormModel
   safePromise: boolean
   safeRead: boolean
   loading: boolean
@@ -196,6 +303,63 @@ const state = reactive<{
   },
 })
 
+const showApprovalInfo = computed(() => {
+  return Boolean(state.form.approvalId || state.form.businessCode)
+})
+
+const approveStatusTag = computed(() => {
+  if (!state.form.approveStatus) return null
+  if (state.form.approveStatus === ApplyLeaveApproveStatus.SUBMIT) return { text: '提交', type: 'primary' as const }
+  if (state.form.approveStatus === ApplyLeaveApproveStatus.WAIT_APPROVE) return { text: '待审核', type: 'primary' as const }
+  if (state.form.approveStatus === ApplyLeaveApproveStatus.PASS) return { text: '通过', type: 'success' as const }
+  if (state.form.approveStatus === ApplyLeaveApproveStatus.REVOKE) return { text: '撤回', type: 'success' as const }
+  if (state.form.approveStatus === ApplyLeaveApproveStatus.REFUSE) return { text: '审核不通过', type: 'danger' as const }
+  return { text: state.form.approveStatus, type: 'primary' as const }
+})
+
+const createdTimeText = computed(() => {
+  if (!state.form.createdTime) return '-'
+  return formatDate(new Date(state.form.createdTime), 'YYYY-mm-dd HH:MM:SS')
+})
+
+const approvalFlowId = computed(() => {
+  return Number(state.form.approvalId || 0)
+})
+
+const approvalBusinessCode = computed(() => {
+  return state.form.businessCode ? String(state.form.businessCode) : ''
+})
+
+const formatYmd = (val?: string) => {
+  if (!val) return ''
+  const str = String(val)
+  return str.length >= 10 ? str.slice(0, 10) : str
+}
+
+const returnDateText = computed(() => formatYmd(state.form.returnDate))
+
+const dieTimeText = computed(() => formatYmd(state.form.dieTime))
+
+const returnAnimalNumberText = computed(() => {
+  const maleNumber = state.form.returnMaleNumber || 0
+  const femaleNumber = state.form.returnFemaleNumber || 0
+  const total = maleNumber + femaleNumber
+  return `${maleNumber} + ${femaleNumber} = ${total}`
+})
+
+const dieAnimalNumberText = computed(() => {
+  const maleNumber = state.form.dieMaleNumber || 0
+  const femaleNumber = state.form.dieFemaleNumber || 0
+  const total = maleNumber + femaleNumber
+  return `${maleNumber} + ${femaleNumber} = ${total}`
+})
+
+const accessCardReturnText = computed(() => {
+  if (state.form.accessCardReturn === 1) return '是'
+  if (state.form.accessCardReturn === 0) return '否'
+  return ''
+})
+
 const getDicts = async () => {
   const [_, res]: ToResponse = await to(platAnimalCageApplicationApi.getAnimalTypeList({}))
 
@@ -204,6 +368,54 @@ const getDicts = async () => {
   }
 }
 
+const loadRebackInfo = async (takeawayId?: number) => {
+  if (!takeawayId) return
+  const [err, res]: ToResponse = await to(
+    platAnimalCageApplicationApi.getPlatAnimalTakeawayRebackList({ takeawayId }),
+  )
+  if (err || !res) return
+
+  const rawList = (res as any).data
+  const list = Array.isArray(rawList?.list)
+    ? rawList.list
+    : Array.isArray(rawList)
+      ? rawList
+      : []
+
+  const turnBackList = list.filter((item: any) => String(item.takeawayType) === '10')
+  const dieList = list.filter((item: any) => String(item.takeawayType) === '20')
+
+  const turnBack = turnBackList[turnBackList.length - 1]
+  const die = dieList[dieList.length - 1]
+
+  if (turnBack) {
+    state.form.returnDate = turnBack.returnDate || ''
+    state.form.returnTransport = turnBack.returnTransport || ''
+    if (typeof turnBack.accessCardReturn === 'number') {
+      state.form.accessCardReturn = turnBack.accessCardReturn
+    }
+    state.form.returnFemaleNumber = turnBack.returnFemaleNumber || 0
+    state.form.returnMaleNumber = turnBack.returnMaleNumber || 0
+    if (turnBack.notReturnReason) {
+      state.form.notReturnReason = turnBack.notReturnReason
+    }
+  }
+
+  if (die) {
+    state.form.dieTime = die.dieTime || ''
+    state.form.dieReason = die.dieReason || ''
+    state.form.location = die.location || ''
+    const dieFemale = die.returnFemaleNumber ?? die.dieFemaleNumber
+    const dieMale = die.returnMaleNumber ?? die.dieMaleNumber
+    if (typeof dieFemale === 'number') {
+      state.form.dieFemaleNumber = dieFemale
+    }
+    if (typeof dieMale === 'number') {
+      state.form.dieMaleNumber = dieMale
+    }
+  }
+}
+
 const openDialog = async (type: ActionType, sourceData?: TakeawayList) => {
   await getDicts()
   state.dialog.type = type
@@ -237,6 +449,10 @@ const openDialog = async (type: ActionType, sourceData?: TakeawayList) => {
     }
   }
 
+  if (type === 'detail') {
+    await loadRebackInfo(state.form.id)
+  }
+
   if (type === 'add') {
     state.dialog.title = '新增实验动物带离单'
     if (animalTypeList.value.length > 0) {
@@ -276,6 +492,17 @@ const closeDialog = () => {
     deptId: '',
     deptName: '',
     isReturn: 0,
+     returnDate: '',
+     returnFemaleNumber: 0,
+     returnMaleNumber: 0,
+     returnTransport: '',
+     accessCardReturn: 0,
+     dieFemaleNumber: 0,
+     dieMaleNumber: 0,
+     dieTime: '',
+     dieReason: '',
+     location: '',
+     notReturnReason: '',
   }
   safePromiseStatus.value = false
   selectedDate.value = null
@@ -334,10 +561,12 @@ const onSubmit = async () => {
   }
 
   const post = platAnimalCageApplicationApi.createAnimalTakeawayApplications
+  const submit = platAnimalCageApplicationApi.submitAnimalTakeawayApplications
   const [err]: ToResponse = await to(
-    post(
+    (state.dialog.type === 'edit' ? submit : post)(
       filterFields({
         ...deepClone(state.form),
+        categoryId: state.form.categoryId?.toString() || null,
         deptId: userInfos.value.deptId,
         projectGroupId: state.form.projectGroupId?.toString(),
         takeawayMaleNumber: Number(state.form.takeawayMaleNumber) || 0,
@@ -367,7 +596,7 @@ const onSubmit = async () => {
 const onCategoryConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
   if (selectedOptions.length > 0) {
     const selected = selectedOptions[0]
-    state.form.categoryId = selected.id
+    state.form.categoryId = selected.id?.toString?.() ?? String(selected.id)
     state.form.categoryName = selected.name
   }
   showCategoryPicker.value = false
@@ -468,6 +697,14 @@ defineExpose({
     }
   }
 
+  .approval-empty {
+    padding: 16px;
+    text-align: center;
+    color: #969799;
+    background-color: #fff;
+    border-radius: 8px;
+  }
+
   .dialog-footer {
     padding: 16px;
     padding-bottom: calc(16px + env(safe-area-inset-bottom));
@@ -529,4 +766,4 @@ defineExpose({
   top: 16px;
   right: 16px;
 }
-</style>
+</style>

+ 2 - 1
src/view/animal/applicationRemoval/components/dieModal.vue

@@ -13,7 +13,8 @@
               <van-field v-model="state.form.dieTime" label="淘汰日期" placeholder="请选择时间" readonly is-link
                 @click="showDieDatePicker = true" />
 
-              <van-field v-model="state.form.dieReason" label="淘汰原因" placeholder="请输入" type="textarea" rows="3" />
+              <van-field v-model="state.form.dieReason" label="淘汰原因" placeholder="请输入" type="textarea" rows="2" />
+              <van-field v-model="state.form.location" label="动物尸体存放位置" placeholder="请输入" type="textarea" rows="2" />
 
               <van-field label="雄性">
                 <template #input>

+ 7 - 21
src/view/animal/applicationRemoval/components/turnBack.vue

@@ -9,33 +9,24 @@
         <div class="drawer-body">
           <van-form ref="expertDialogFormRef" @submit="onSubmit">
             <van-cell-group inset>
-              <van-field label="是否转回" readonly>
-                <template #input>
-                  <van-radio-group v-model="isReturn" direction="horizontal">
-                    <van-radio :name="1">是</van-radio>
-                    <van-radio :name="0">否</van-radio>
-                  </van-radio-group>
-                </template>
-              </van-field>
-
-              <van-field v-if="isReturn === 1" v-model="state.form.returnDate" label="转回日期" placeholder="请选择时间" readonly
+              <van-field v-model="state.form.returnDate" label="转回日期" placeholder="请选择时间" readonly
                 is-link @click="showReturnDatePicker = true" />
 
-              <van-field v-if="isReturn === 1" v-model="state.form.returnTransport" label="运输方式" placeholder="请输入" />
+              <van-field  v-model="state.form.returnTransport" label="运输方式" placeholder="请输入" />
 
-              <van-field v-if="isReturn === 1" label="雄性">
+              <van-field label="雄性">
                 <template #input>
                   <van-stepper v-model="state.form.returnMaleNumber" integer :min="0" />
                 </template>
               </van-field>
 
-              <van-field v-if="isReturn === 1" label="雌性">
+              <van-field label="雌性">
                 <template #input>
                   <van-stepper v-model="state.form.returnFemaleNumber" integer :min="0" />
                 </template>
               </van-field>
 
-              <van-field v-if="isReturn === 0" v-model="state.form.notReturnReason" label="未返回动物情况说明"
+              <van-field v-model="state.form.notReturnReason" label="未返回动物情况说明"
                 placeholder="请输入" type="textarea" rows="3" />
 
               <van-field label="门禁卡是否归还">
@@ -79,7 +70,6 @@ const emit = defineEmits(['refresh'])
 const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
 
 const expertDialogFormRef = ref<FormInstance>()
-const isReturn = ref<number>(1)
 const showReturnDatePicker = ref(false)
 
 const createDefaultForm = (): TurnBackPayload => ({
@@ -107,7 +97,6 @@ const state = reactive<{
 })
 
 const validateForm = () => {
-  if (isReturn.value === 1) {
     if (!state.form.returnDate) {
       showNotify({ type: 'warning', message: '请选择转回日期' })
       return false
@@ -122,7 +111,7 @@ const validateForm = () => {
       showNotify({ type: 'warning', message: '请添加雄性或雌性数量' })
       return false
     }
-  } else {
+
     if (!state.form.notReturnReason) {
       showNotify({ type: 'warning', message: '请输入未返回动物情况说明' })
       return false
@@ -131,7 +120,6 @@ const validateForm = () => {
     state.form.returnTransport = ''
     state.form.returnMaleNumber = 0
     state.form.returnFemaleNumber = 0
-  }
 
   return true
 }
@@ -142,7 +130,7 @@ const openDialog = async (sourceData: TakeawayList) => {
     ...createDefaultForm(),
     takeawayId: sourceData.id,
   }
-  isReturn.value = 1
+
   state.dialog.isShowDialog = true
 }
 
@@ -150,7 +138,6 @@ const closeDialog = () => {
   expertDialogFormRef.value?.resetValidation?.()
   state.form = createDefaultForm()
   state.dialog.isShowDialog = false
-  isReturn.value = 1
   showReturnDatePicker.value = false
 }
 
@@ -167,7 +154,6 @@ const onSubmit = async () => {
     returnDate: state.form.returnDate ? dayjs(state.form.returnDate).format('YYYY-MM-DD') : '',
     returnMaleNumber: Number(state.form.returnMaleNumber) || 0,
     returnFemaleNumber: Number(state.form.returnFemaleNumber) || 0,
-    takeawayType: isReturn.value === 1 ? '10' : '11',
   })
 
   const [err]: ToResponse = await to(post(payload))

+ 80 - 3
src/view/animal/applicationRemoval/index.vue

@@ -208,6 +208,24 @@
                 class="mt16"
                 :gutter="20"
               >
+                <el-col :span="24">
+                  <el-button
+                    style="width: 100%"
+                    v-if="item.approveStatus === ApplyLeaveApproveStatus.WAIT_APPROVE && userInfos.id === item.userId"
+                    type="danger"
+                    @click.stop="onWithdraw(item.id)"
+                  >
+                    撤回
+                  </el-button>
+                  <el-button
+                    style="width: 100%"
+                    v-if="item.approveStatus === ApplyLeaveApproveStatus.REVOKE && userInfos.id === item.userId"
+                    type="primary"
+                    @click.stop="onResubmit(item)"
+                  >
+                    重新提交
+                  </el-button>
+                </el-col>
                 <el-col :span="12">
                   <el-button
                     style="width: 100%"
@@ -237,15 +255,15 @@
   </div>
   <AddEdit
     ref="addEditRef"
-    @refresh="onLoad(true)"
+    @refresh="handleRefresh"
   />
   <TurnBackModal
     ref="turnBackModalRef"
-    @refresh="onLoad(true)"
+    @refresh="handleRefresh"
   />
   <DieModal
     ref="dieModalRef"
-    @refresh="onLoad(true)"
+    @refresh="handleRefresh"
   />
 
   <van-popup
@@ -268,14 +286,21 @@
   import to from 'await-to-js'
   import dayjs from 'dayjs'
   import { Calendar, Close } from '@element-plus/icons-vue'
+  import { ElMessage, ElMessageBox } from 'element-plus'
 
   import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+  import { useExecutionApi } from '/@/api/platform/execution'
+  import { usePlatformApi } from '/@/api/platform/home'
   import { ApplyLeaveApproveStatus, TakeawayList } from '/@/constants/pageConstants'
+  import { useUserInfos } from '/@/hooks/useUserInfos'
   import AddEdit from './components/addEdit.vue'
   import TurnBackModal from './components/turnBack.vue'
   import DieModal from './components/dieModal.vue'
 
   const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+  const executionApi = useExecutionApi()
+  const platformApi = usePlatformApi()
+  const { userInfos } = useUserInfos()
 
   const addEditRef = ref<InstanceType<typeof AddEdit>>()
   const turnBackModalRef = ref<InstanceType<typeof TurnBackModal>>()
@@ -359,11 +384,21 @@
           state.finished = true
         }
       } else {
+        state.queryParams.pageNum = 2
+        state.finished = list.length < state.queryParams.pageSize
         state.list = list
       }
     }
   }
 
+  const handleRefresh = () => {
+    state.list = []
+    state.queryParams.pageNum = 1
+    state.finished = false
+    state.loading = true
+    onLoad(true)
+  }
+
   const search = () => {
     onLoad(true)
   }
@@ -442,6 +477,48 @@
     addEditRef.value.openDialog('add')
   }
 
+  const onResubmit = (row: TakeawayList) => {
+    addEditRef.value?.openDialog('edit', row)
+  }
+
+  const onWithdraw = async (id: number) => {
+    try {
+      await ElMessageBox.confirm('确认撤回?', '提示', {
+        confirmButtonText: '确认',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+    } catch {
+      return
+    }
+
+    const [platformErr, platformRes]: ToResponse = await to(platformApi.getUserPlatformList({}))
+    if (platformErr) return
+    const platformList = ((platformRes as any)?.data?.list || (platformRes as any)?.data || []) as any[]
+    const experimentPlatform =
+      platformList.find((item: any) => item.platName === '实验平台') ||
+      platformList.find((item: any) => item.platformName === '实验平台')
+    if (!experimentPlatform) {
+      ElMessage.error('未找到实验平台,请联系管理员')
+      return
+    }
+
+    const params = {
+      businessCode: id.toString(),
+      platformId: experimentPlatform.id,
+      defCode: 'plat_animal_takeway_applications',
+    }
+
+    const [err]: ToResponse = await to(executionApi.withdraw(params))
+    if (err) return
+
+    const [e]: ToResponse = await to(platAnimalCageApplicationApi.quashApproveAnimalTakeawayApplications({ id }))
+    if (e) return
+
+    ElMessage.success('撤回成功')
+    handleRefresh()
+  }
+
   const handleReturn = (row: TakeawayList) => {
     turnBackModalRef.value?.openDialog(row)
   }

+ 122 - 0
src/view/todo/component/plat_animal_takeway_reback.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="facilities-dialog-container">
+    <h4 class="mb8 mt8">转回情况</h4>
+    <van-cell-group>
+      <van-cell title="是否转回" :value="isReturnText" />
+      <van-cell title="门禁卡是否归还" :value="accessCardReturnText" />
+
+      <template v-if="isReturn === 1">
+        <van-cell title="转回日期" :value="state.form.returnDate || '-'" />
+        <van-cell title="运输方式" :value="state.form.returnTransport || '-'" />
+        <van-cell title="数量(雄性+雌性=总数)">
+          <template #value>
+            <div class="num-row">
+              <span>{{ state.form.returnMaleNumber || 0 }}</span>
+              <span class="num-sep">+</span>
+              <span>{{ state.form.returnFemaleNumber || 0 }}</span>
+              <span class="num-sep">=</span>
+              <span class="num-total">{{ animalNumber }}</span>
+            </div>
+          </template>
+        </van-cell>
+      </template>
+
+      <van-cell v-else title="未返回动物情况说明" :value="state.form.notReturnReason || '-'" />
+    </van-cell-group>
+  </div>
+</template>
+
+<script setup lang="ts" name="systemProDialog">
+import { computed, reactive, watch } from 'vue'
+import to from 'await-to-js'
+
+import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+import type { TurnBackPayload } from '/@/constants/pageConstants'
+
+const props = defineProps({
+  code: { type: String, default: '' },
+})
+
+const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+
+const defaultFormFields: TurnBackPayload = {
+  takeawayId: 0,
+  returnDate: '',
+  returnTransport: '',
+  accessCardReturn: 1,
+  returnFemaleNumber: 0,
+  returnMaleNumber: 0,
+  notReturnReason: '',
+  takeawayType: '10',
+}
+
+const state = reactive<{
+  form: TurnBackPayload
+}>({
+  form: defaultFormFields,
+})
+
+const animalNumber = computed(() => {
+  const maleNumber = state.form.returnMaleNumber || 0
+  const famaleNumber = state.form.returnFemaleNumber || 0
+  return maleNumber + famaleNumber
+})
+
+const isReturn = computed<number>(() => {
+  const returnTotal = (state.form.returnMaleNumber || 0) + (state.form.returnFemaleNumber || 0)
+  const hasReturnInfo = Boolean(state.form.returnDate) || Boolean(state.form.returnTransport) || returnTotal > 0
+  const hasNotReturnReason = Boolean(state.form.notReturnReason)
+
+  if (hasNotReturnReason && !hasReturnInfo) return 0
+  if (hasReturnInfo) return 1
+  return 1
+})
+
+const isReturnText = computed(() => (isReturn.value === 1 ? '是' : '否'))
+
+const accessCardReturnText = computed(() => (state.form.accessCardReturn === 1 ? '是' : '否'))
+
+const initForm = async (code: string) => {
+  if (!code) return
+  const [err, res]: ToResponse = await to(
+    platAnimalCageApplicationApi.getPlatAnimalTakeawayRebackDetail({ id: parseInt(code) }),
+  )
+  if (err) return
+  state.form = {
+    ...state.form,
+    ...res.data,
+  }
+}
+
+watch(
+  () => props.code,
+  (val) => {
+    initForm(val)
+  },
+  {
+    deep: true,
+    immediate: true,
+  },
+)
+</script>
+
+<style lang="scss" scoped>
+  .facilities-dialog-container {
+    padding: 10px;
+  }
+
+  .num-row {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+  }
+
+  .num-sep {
+    color: #969799;
+  }
+
+  .num-total {
+    font-weight: 600;
+    color: #323233;
+  }
+</style>

+ 3 - 0
src/view/todo/detail.vue

@@ -34,6 +34,8 @@
       :code="state.form.businessCode" />
     <PlatformAnimalAppointmentEthics v-if="state.form.defCode === 'platform_animal_appointment_ethics'"
       :code="state.form.businessCode" />
+    <PlatAnimalTakewayReback v-if="state.form.defCode === 'plat_animal_takeway_reback'"
+      :code="state.form.businessCode" />
     <h4>审批记录</h4>
     <FlowTable :id="state.form.id" :businessCode="state.form.businessCode" :defCode="state.form.defCode" />
     <h4 v-if="state.type === 'approval'">审批意见</h4>
@@ -78,6 +80,7 @@ const PlatPlatformRenewal = defineAsyncComponent(() => import('/@/view/todo/comp
 const PlatAnimalTakewayApplications = defineAsyncComponent(() => import('/@/view/todo/component/plat_animal_takeway_applications.vue'))
 const PlatPlatformOutRoomAppoint = defineAsyncComponent(() => import('/@/view/todo/component/plat_platform_out_room_appoint.vue'))
 const PlatformAnimalAppointmentEthics = defineAsyncComponent(() => import('/@/view/todo/component/platform_animal_appointment_ethics.vue'))
+const PlatAnimalTakewayReback = defineAsyncComponent(() => import('/@/view/todo/component/plat_animal_takeway_reback.vue'))
 const executionApi = useExecutionApi()
 const flowApi = useFlowApi()
 const router = useRouter()