Explorar o código

Merge branch 'feature/ZUNYIYIKED-303' of dashoo/labsop_h5 into master

徐凯 hai 4 meses
pai
achega
ab75531726

+ 45 - 1
src/api/platform/animal/index.ts

@@ -61,6 +61,50 @@ export function usePlatAnimalCageApplicationApi() {
     //获取我的历史笼位列表
     getMyCageHistoryList(params?: Object) {
       return request.postRequest(basePath, 'PlatAnimalCageApplication', 'GetCageReleaseApplications', params)
-    }
+    },
+    // 获取申请带离列表
+    getAnimalTakeawayApplicationsList(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalTakeawayApplications', 'List', params)
+    },
+    // 导出申请带离列表
+    getAnimalTakeawayApplicationsListExport(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalTakeawayApplications', 'ExportList', params)
+    },
+    // 创建申请带离
+    createAnimalTakeawayApplications(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalTakeawayApplications', 'Create', params)
+    },
+    // 更新申请带离
+    updateAnimalTakeawayApplications(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalTakeawayApplications', 'Update', params)
+    },
+    // 删除申请带离
+    deleteAnimalTakeawayApplications(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalTakeawayApplications', 'Delete', params)
+    },
+    // 获取申请带离详情
+    getAnimalTakeawayApplicationsDetail(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalTakeawayApplications', 'Info', params)
+    },
+    // 获取归还列表
+    getPlatAnimalTakeawayRebackList(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalTakeawayReback', 'List', params)
+    },
+    // 创建归还
+    createPlatAnimalTakeawayReback(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalTakeawayReback', 'Create', params)
+    },
+    // 获取归还详情
+    getPlatAnimalTakeawayRebackDetail(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalTakeawayReback', 'Info', params)
+    },
+    // 更新归还
+    updatePlatAnimalTakeawayReback(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalTakeawayReback', 'Update', params)
+    },
+    // 删除归还
+    deletePlatAnimalTakeawayReback(params?: Object) {
+      return request.postRequest(basePath, 'PlatAnimalTakeawayReback', 'Delete', params)
+    },
   }
 }

+ 95 - 0
src/constants/pageConstants.ts

@@ -99,3 +99,98 @@ export enum FeeStatus {
   NOT_PAID = '10',
   PAID = '20'
 }
+
+// 申请带离审批状态
+export enum ApplyLeaveApproveStatus {
+  SUBMIT = '10', // 提交
+  WAIT_APPROVE = '20', // 待审核
+  PASS = '30', // 审核通过
+  REVOKE = '35', // 撤回
+  REFUSE = '40', // 审核不通过
+}
+
+export interface TakeawayList {
+  accessCardNumber: string // 门禁卡序列号
+  accessCardReturn: number // 是否归还门禁卡
+  approvalId: number // 审批ID
+  approveStatus: string // 审批状态
+  businessCode: string // 业务编码
+  categoryId: string // 动物种类ID
+  createdBy: number // 创建人ID
+  createdName: string // 创建人姓名
+  createdTime: string // 创建时间
+  deletedTime: null // 删除时间
+  deptId: string // 部门ID
+  deptName: string // 部门名称
+  dieFemaleNumber: number // 死亡雌性数量
+  dieMaleNumber: number // 死亡雄性数量
+  id: number // 主键ID
+  phone: string // 联系电话
+  projectGroupId: number // 项目组ID
+  projectGroupName: string // 项目组名称
+  returnDate: null // 归还日期
+  returnFemaleNumber: number // 归还雌性数量
+  returnMaleNumber: number // 归还雄性数量
+  returnTransport: string // 归还方式
+  takeawayAddress: string // 转出地点
+  takeawayDate: string // 转出日期
+  takeawayMaleNumber: number // 转出雄性数量
+  takeawayReason: string // 转出原因
+  takeawayTransport: string // 转出方式
+  takewayFemaleNumber: number // 转出雌性数量
+  updatedBy: number // 更新人ID
+  updatedName: string // 更新人姓名
+  updatedTime: string // 更新时间
+  userId: number // 用户ID
+  userName: string // 用户姓名
+}
+
+
+export interface CreateAnimalApplyLeavePayload {
+  accessCardNumber: string // 门禁卡序列号
+  categoryId: string | null // 动物种类
+  projectGroupId: number | null // 项目组ID
+  projectGroupName: string // 项目组名称
+  takeawayDate: string // 转出日期
+  takeawayAddress: string // 转出地点
+  takeawayReason: string // 转出原因
+  takeawayTransport: string // 转出方式
+  takewayFemaleNumber: number // 申请雌性数量
+  takeawayMaleNumber: number // 申请雄性数量
+  userName: string // 用户名称
+  phone: string // 用户电话
+  deptId: string // 用户部门
+  deptName: string // 用户部门名称
+}
+
+export interface TurnBackPayload {
+  takeawayId: number // 申请带离ID
+  categoryId: string | null // 动物种类
+  returnDate: string // 转回日期
+  returnTransport: string // 转回方式
+  accessCardReturn: number // 是否归还门禁卡
+  returnFemaleNumber: number // 返回雌性数量
+  returnMaleNumber: number // 返回雄性数量
+  notReturnReason: string // 未返回动物情况说明
+  takeawayType: string // 带离类型 带离状态:10=带回(默认),20=淘汰
+}
+
+export interface ApplyLeaveDiePayload {
+  takeawayId: number // 申请带离ID
+  dieTime: string // 死亡时间
+  dieReason: string // 死亡原因
+  takeawayType: string // 带离类型 带离状态:10=带回(默认),20=淘汰
+  returnFemaleNumber: number // 返回雌性数量
+  returnMaleNumber: number // 返回雄性数量
+}
+
+export type ActionType = 'add' | 'edit' | 'detail'
+
+export const AnimalRemovalApplicationNotice = `
+  1.为防止动物逃逸和避免动物外出感染,需自行准备转运盒。转运途中如出现任何问题,实验动物房不承担任何责任。
+  2.请务必将实验动物打包好后再带离临床医学公共实验中心,一旦在屏障外进行实验后严禁再将动物拿回屏障区内。若私自将屏障区外的实验动物带入屏障区,由此带来的损失将由其导师承担。
+  3.转运需自行准备实验动物饮食。
+  4.实验动物一经离开临床医学公共实验中心,发生的逃逸或抓伤咬伤等生物安全事故由带离实验动物的本人自行负责。
+  5.带离临床医学公共实验中心的动物,如若死亡,需按照相关规定规范处理,不可随意丢弃。
+  6.转运回来的实验动物,将重新进行检疫隔离,如运输包装、方式等不规范,实验动物房有权拒收该批转运回来的实验动物。
+`

+ 1 - 0
src/layout/animal.vue

@@ -11,6 +11,7 @@
   <van-tabbar route :placeholder="true" v-if="route.path !== '/entry/add'">
     <van-tabbar-item replace to="/my-cage" icon="send-gift-o">我的笼位</van-tabbar-item>
     <van-tabbar-item replace to="/animal-application" icon="send-gift-o">笼位申请</van-tabbar-item>
+    <van-tabbar-item replace to="/animal-application-removal" icon="send-gift-o">申请带离</van-tabbar-item>
     <van-tabbar-item replace to="/animal-return" icon="send-gift-o">笼位退还</van-tabbar-item>
   </van-tabbar>
 </template>

+ 8 - 0
src/router.ts

@@ -318,6 +318,14 @@ const routes = [
           title: '笼位申请'
         }
       },
+      {
+        name: 'animalApplicationRemoval',
+        path: '/animal-application-removal',
+        component: () => import('/@/view/animal/applicationRemoval/index.vue'),
+        meta: {
+          title: '申请带离'
+        }
+      },
       {
         name: 'animalReturn',
         path: '/animal-return',

+ 16 - 0
src/utils/func.ts

@@ -0,0 +1,16 @@
+/**
+ * 过滤表单字段
+ * @param payload 表单数据
+ * @returns 过滤后的表单数据
+ */
+export const filterFields = (payload: Record<string, any>) => {
+  const filteredFields = Object.fromEntries(
+    Object.entries(payload).filter(([_, value]) => {
+      if (value === null || value === undefined || value === '') return false
+      if (typeof value === 'number' && value === 0) return true
+      return true
+    }),
+  )
+
+  return filteredFields
+}

+ 483 - 0
src/view/animal/applicationRemoval/components/addEdit.vue

@@ -0,0 +1,483 @@
+<template>
+  <div class="application-dialog-container">
+    <el-dialog
+      :title="state.dialog.title"
+      @close="onCancel"
+      :close-on-click-modal="false"
+      v-model="state.dialog.isShowDialog"
+      width="90%"
+    >
+      <el-form
+        ref="expertDialogFormRef"
+        :model="state.form"
+        :rules="rules"
+        size="default"
+        label-width="140px"
+        label-position="top"
+      >
+        <h4 class="mb20 mt8">申请人信息</h4>
+        <el-row class="mb20">
+          <el-col :span="24">
+            <el-form-item
+              label="申请人姓名"
+              prop="userName"
+            >
+              <el-input
+                v-model="state.form.userName"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row>
+          <el-col :span="24">
+            <el-form-item
+              label="联系电话"
+              prop="phone"
+            >
+              <el-input
+                v-model="state.form.phone"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row>
+          <el-col :span="24">
+            <el-form-item
+              label="课题组"
+              prop="projectGroupId"
+            >
+              <el-input
+                v-model="state.form.projectGroupName"
+                disabled
+                placeholder="请选择"
+                class="w100"
+              ></el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row>
+          <el-col :span="24">
+            <el-form-item
+              label="科室"
+              prop="deptId"
+            >
+              <el-input
+                v-model="state.form.deptName"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <h4 class="mb20 mt20">转出情况</h4>
+        <el-row
+          class="mb20"
+        >
+          <el-col :span="24">
+            <el-form-item
+              label="动物品系"
+              prop="categoryId"
+            >
+              <el-select
+                v-model="state.form.categoryId"
+                placeholder="请选择"
+                :disabled="state.dialog.type === 'detail'"
+              >
+                <el-option
+                  v-for="item in animalTypeList"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row>
+          <el-col :span="24">
+            <el-form-item
+              label="转出日期"
+              prop="takeawayDate"
+            >
+              <el-date-picker
+                v-model="state.form.takeawayDate"
+                type="date"
+                placeholder="请选择时间"
+                clearable
+                value-format="YYYY-MM-DD"
+                style="width: 100%"
+                :disabled="state.dialog.type === 'detail'"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row
+          class="mb20"
+        >
+          <el-col :span="24">
+            <el-form-item
+              label="送往地点"
+              prop="takeawayAddress"
+            >
+              <el-input
+                v-model="state.form.takeawayAddress"
+                placeholder="请输入"
+                :disabled="state.dialog.type === 'detail'"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row>
+          <el-col :span="24">
+            <el-form-item
+              label="带出原因"
+              prop="takeawayReason"
+            >
+              <el-input
+                v-model="state.form.takeawayReason"
+                placeholder="请输入"
+                :disabled="state.dialog.type === 'detail'"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row
+          class="mb20"
+        >
+          <el-col :span="24">
+            <el-form-item
+              label="运输方式"
+              prop="takeawayTransport"
+            >
+              <el-input
+                v-model="state.form.takeawayTransport"
+                placeholder="请输入"
+                :disabled="state.dialog.type === 'detail'"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row>
+          <el-col :span="24">
+            <el-form-item
+              label="门禁卡序列号"
+              prop="accessCardNumber"
+            >
+              <el-input
+                v-model="state.form.accessCardNumber"
+                placeholder="请输入"
+                :disabled="state.dialog.type === 'detail'"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row
+          class="mt10"
+        >
+          <el-col :span="24">
+            <el-form-item
+              label="雄性"
+              prop="takeawayMaleNumber"
+            >
+              <el-input-number
+                style="width: 100%"
+                placeholder="雄性数量"
+                v-model="state.form.takeawayMaleNumber"
+                :min="0"
+                :disabled="state.dialog.type === 'detail'"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row>
+          <el-col :span="24">
+            <el-form-item
+              label="雌性"
+              prop="takewayFemaleNumber"
+            >
+              <el-input-number
+                style="width: 100%"
+                placeholder="雌性数量"
+                v-model="state.form.takewayFemaleNumber"
+                :min="0"
+                :disabled="state.dialog.type === 'detail'"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row
+          class="mt10"
+        >
+          <el-col
+            :span="24"
+            class="mt30 mb30"
+          >
+            <el-checkbox
+              v-model="safePromiseStatus"
+              :disabled="state.dialog.type === 'detail'"
+            >
+              本人已阅读并同意
+              <el-link
+                type="primary"
+                :underline="false"
+                @click="handleReadNotice"
+              >
+                《实验动物带离须知》
+              </el-link>
+              内容
+            </el-checkbox>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button
+            type="info"
+            @click="onCancel"
+            size="default"
+          >
+            取 消
+          </el-button>
+          <el-button
+            v-if="state.dialog.type !== 'detail'"
+            color="#2c78ff"
+            @click="onSubmit()"
+            size="default"
+          >
+            提交
+          </el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <el-dialog
+      v-model="isShowNotice"
+      title="实验动物带离须知"
+      width="90%"
+    >
+      <div class="text">
+        <p
+          class="mb20"
+          v-for="item in AnimalRemovalApplicationNotice.split('\n')"
+          :key="item"
+        >
+          {{ item }}
+        </p>
+      </div>
+      <template #footer>
+        <el-button
+          type="primary"
+          @click="isShowNotice = false"
+        >
+          确定
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref, computed } from 'vue'
+  import to from 'await-to-js'
+  import { ElMessage } from 'element-plus'
+  import type { FormRules, FormInstance } from 'element-plus'
+
+  import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+  import {
+    CreateAnimalApplyLeavePayload,
+    TakeawayList,
+    ActionType,
+    AnimalRemovalApplicationNotice,
+  } from '/@/constants/pageConstants'
+  import { deepClone } from '/@/utils/other'
+  import { useUserInfos } from '/@/hooks/useUserInfos'
+  import { filterFields } from '/@/utils/func'
+
+  const emit = defineEmits(['refresh'])
+
+  const { userInfos } = useUserInfos()
+  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+
+  const expertDialogFormRef = ref<FormInstance>()
+  const animalTypeList = ref<{ id: string; name: string }[]>([])
+  const safePromiseStatus = ref<boolean>(false)
+  const isShowNotice = ref<boolean>(false)
+
+  const rules = reactive<FormRules<CreateAnimalApplyLeavePayload>>({
+    categoryId: [{ required: true, message: '请选择动物种类', trigger: 'blur' }],
+    projectGroupId: [{ required: true, message: '请选择项目组', trigger: 'blur' }],
+    takeawayDate: [{ required: true, message: '请选择转出日期', trigger: 'blur' }],
+    userName: [{ required: true, message: '请输入申请人姓名', trigger: 'blur' }],
+    takeawayAddress: [{ required: true, message: '不能为空', trigger: 'change' }],
+    takeawayReason: [{ required: true, message: '不能为空', trigger: 'change' }],
+    takeawayTransport: [{ required: true, message: '不能为空', trigger: 'change' }],
+  })
+
+  const defaultFormFields: CreateAnimalApplyLeavePayload = {
+    accessCardNumber: '',
+    categoryId: null,
+    projectGroupId: null,
+    projectGroupName: '',
+    takeawayDate: '',
+    takeawayAddress: '',
+    takeawayReason: '',
+    takeawayTransport: '',
+    takewayFemaleNumber: 0,
+    takeawayMaleNumber: 0,
+    userName: '',
+    phone: '',
+    deptId: '',
+    deptName: '',
+  }
+
+  const state = reactive<{
+    form: CreateAnimalApplyLeavePayload
+    safePromise: boolean
+    safeRead: boolean
+    dialog: { isShowDialog: boolean; type: string; title: string; submitTxt: string }
+  }>({
+    form: defaultFormFields,
+    safePromise: false,
+    safeRead: false,
+    dialog: {
+      isShowDialog: false,
+      type: '',
+      title: '',
+      submitTxt: '',
+    },
+  })
+
+  const animalNumber = computed(() => {
+    const maleNumber = state.form.takeawayMaleNumber || 0
+    const famaleNumber = state.form.takewayFemaleNumber || 0
+    return maleNumber + famaleNumber
+  })
+
+  const getDicts = async () => {
+    const [_, res]: ToResponse = await to(platAnimalCageApplicationApi.getAnimalTypeList({}))
+
+    if (res) {
+      animalTypeList.value = res.data
+    }
+  }
+
+  const openDialog = async (type: ActionType, sourceData?: TakeawayList) => {
+    await getDicts()
+    state.dialog.type = type
+    state.form.userName = userInfos.value.nickName
+    state.form.phone = userInfos.value.phone
+    state.form.deptName = userInfos.value.deptName
+    state.form.projectGroupId = userInfos.value.projectGroup?.id
+    state.form.projectGroupName = userInfos.value.projectGroup?.pgName
+    state.form.deptName = userInfos.value.deptName
+
+    if (sourceData) {
+      state.form = {
+        ...state.form,
+        ...sourceData,
+      }
+    }
+
+    if (type === 'add') {
+      state.dialog.title = '新增实验动物带离单'
+      state.form.categoryId = animalTypeList.value[0].id
+    }
+    if (type === 'edit') {
+      state.dialog.title = '编辑实验动物带离单'
+    }
+    if (type === 'detail') {
+      state.dialog.title = '查看实验动物带离单'
+      safePromiseStatus.value = true
+    }
+
+    state.dialog.isShowDialog = true
+  }
+
+  const closeDialog = () => {
+    expertDialogFormRef.value.resetFields()
+    state.form = defaultFormFields
+    state.dialog.isShowDialog = false
+  }
+
+  const onCancel = () => {
+    closeDialog()
+  }
+
+  const onSubmit = async () => {
+    expertDialogFormRef.value.validate(async (valid: boolean) => {
+      if (!valid) return
+
+      if (!safePromiseStatus.value) {
+        ElMessage.error('请阅读并勾选安全承诺!')
+        return
+      }
+
+      if (!state.form.takeawayMaleNumber && !state.form.takewayFemaleNumber) {
+        ElMessage.error('请添加雄性或雌性数量!')
+        return
+      }
+
+      const post = platAnimalCageApplicationApi.createAnimalTakeawayApplications
+      const [err]: ToResponse = await to(
+        post(
+          filterFields({
+            ...deepClone(state.form),
+            deptId: userInfos.value.deptId,
+            projectGroupId: state.form.projectGroupId?.toString(),
+          }),
+        ),
+      )
+
+      if (err) return
+      ElMessage.success('操作成功')
+      closeDialog()
+      emit('refresh')
+    })
+  }
+
+  const handleReadNotice = () => {
+    isShowNotice.value = true
+  }
+
+  defineExpose({
+    openDialog,
+  })
+</script>
+<style lang="scss" scoped>
+  .application-dialog-container {
+    .el-select {
+      width: 100%;
+    }
+  }
+  h4 {
+    font-size: 18px;
+  }
+  ul {
+    padding-left: 20px;
+  }
+  .text {
+    p {
+      text-indent: 2em;
+    }
+  }
+  .el-upload + .el-button {
+    vertical-align: top;
+  }
+  :deep(.el-checkbox) {
+    white-space: pre-wrap;
+  }
+</style>

+ 210 - 0
src/view/animal/applicationRemoval/components/dieModal.vue

@@ -0,0 +1,210 @@
+<template>
+  <div class="application-dialog-container">
+    <el-dialog
+      :title="state.dialog.title"
+      @close="onCancel"
+      :close-on-click-modal="false"
+      v-model="state.dialog.isShowDialog"
+      width="90%"
+    >
+      <el-form
+        ref="expertDialogFormRef"
+        :model="state.form"
+        :rules="rules"
+        size="default"
+        label-width="140px"
+        label-position="top"
+      >
+        <el-row class="mb20">
+          <el-col :span="24">
+            <el-form-item
+              label="淘汰日期"
+              prop="dieTime"
+            >
+              <el-date-picker
+                v-model="state.form.dieTime"
+                type="date"
+                placeholder="请选择时间"
+                clearable
+                value-format="YYYY-MM-DD"
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row>
+          <el-col :span="24">
+            <el-form-item
+              label="淘汰原因"
+              prop="dieReason"
+            >
+              <el-input
+                v-model="state.form.dieReason"
+                placeholder="请输入"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row>
+          <el-col :span="24">
+            <el-form-item
+              label="雄性"
+              prop="returnMaleNumber"
+            >
+              <el-input-number
+                style="width: 100%"
+                placeholder="雄性数量"
+                v-model="state.form.returnMaleNumber"
+                :min="0"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row>
+          <el-col :span="24">
+            <el-form-item
+              label="雌性"
+              prop="returnFemaleNumber"
+            >
+              <el-input-number
+                style="width: 100%"
+                placeholder="雌性数量"
+                v-model="state.form.returnFemaleNumber"
+                :min="0"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button
+            type="info"
+            @click="onCancel"
+            size="default"
+          >
+            取 消
+          </el-button>
+          <el-button
+            color="#2c78ff"
+            @click="onSubmit()"
+            size="default"
+          >
+            提交
+          </el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref } from 'vue'
+  import to from 'await-to-js'
+  import { ElMessage } from 'element-plus'
+  import type { FormRules, FormInstance } from 'element-plus'
+
+  import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+  import { TakeawayList, ApplyLeaveDiePayload } from '/@/constants/pageConstants'
+  import { filterFields } from '/@/utils/func'
+
+  const emit = defineEmits(['refresh'])
+
+  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+
+  const expertDialogFormRef = ref<FormInstance>()
+
+  const rules = reactive<FormRules<ApplyLeaveDiePayload>>({
+    dieTime: [{ required: true, message: '请选择日期', trigger: 'blur' }],
+    returnMaleNumber: [{ required: true, message: '请输入雄性数量', trigger: 'blur' }],
+    returnFemaleNumber: [{ required: true, message: '请输入雌性数量', trigger: 'blur' }],
+    dieReason: [{ required: true, message: '请输入死亡原因', trigger: 'blur' }],
+  })
+
+  const defaultFormFields: ApplyLeaveDiePayload = {
+    takeawayId: 0,
+    returnFemaleNumber: 0,
+    returnMaleNumber: 0,
+    takeawayType: '20',
+    dieTime: '',
+    dieReason: '',
+  }
+
+  const state = reactive<{
+    form: ApplyLeaveDiePayload
+    safePromise: boolean
+    safeRead: boolean
+    dialog: { isShowDialog: boolean; title: string; submitTxt: string }
+  }>({
+    form: defaultFormFields,
+    safePromise: false,
+    safeRead: false,
+    dialog: {
+      isShowDialog: false,
+      title: '',
+      submitTxt: '',
+    },
+  })
+
+  const openDialog = async (sourceData: TakeawayList) => {
+    state.form.takeawayId = sourceData.id
+    state.dialog.title = '淘汰动物记录上报'
+    state.dialog.isShowDialog = true
+  }
+
+  const closeDialog = () => {
+    expertDialogFormRef.value.resetFields()
+    state.form = defaultFormFields
+    state.dialog.isShowDialog = false
+  }
+
+  const onCancel = () => {
+    closeDialog()
+  }
+
+  const onSubmit = async () => {
+    expertDialogFormRef.value.validate(async (valid: boolean) => {
+      if (!valid) return
+
+      const post = platAnimalCageApplicationApi.createPlatAnimalTakeawayReback
+      const [err]: ToResponse = await to(post(filterFields(state.form)))
+
+      if (err) return
+      ElMessage.success('操作成功')
+      closeDialog()
+      emit('refresh')
+    })
+  }
+
+  defineExpose({
+    openDialog,
+  })
+</script>
+<style lang="scss" scoped>
+  .application-dialog-container {
+    .el-select {
+      width: 100%;
+    }
+  }
+  h4 {
+    font-size: 18px;
+  }
+  ul {
+    padding-left: 20px;
+  }
+  .text {
+    p {
+      text-indent: 2em;
+    }
+  }
+  .el-upload + .el-button {
+    vertical-align: top;
+  }
+  :deep(.el-checkbox) {
+    white-space: pre-wrap;
+  }
+</style>

+ 313 - 0
src/view/animal/applicationRemoval/components/turnBack.vue

@@ -0,0 +1,313 @@
+<template>
+  <div class="application-dialog-container">
+    <el-dialog
+      :title="state.dialog.title"
+      @close="onCancel"
+      :close-on-click-modal="false"
+      v-model="state.dialog.isShowDialog"
+      width="90%"
+    >
+      <el-form
+        ref="expertDialogFormRef"
+        :model="state.form"
+        :rules="rules"
+        size="default"
+        label-width="140px"
+        label-position="top"
+      >
+        <el-row
+          class="mb20"
+          :gutter="20"
+        >
+          <el-col :span="24">
+            <el-form-item label="是否转回">
+              <el-radio-group
+                :disabled="state.dialog.type === 'view'"
+                v-model="isReturn"
+              >
+                <el-radio
+                  :label="1"
+                  size="large"
+                >
+                  是
+                </el-radio>
+                <el-radio
+                  :label="0"
+                  size="large"
+                >
+                  否
+                </el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row class="mb20">
+          <el-col :span="24">
+            <el-form-item
+              label="动物品系"
+              prop="categoryId"
+            >
+              <el-select
+                v-model="state.form.categoryId"
+                placeholder="请选择"
+              >
+                <el-option
+                  v-for="item in animalTypeList"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row v-if="isReturn === 1">
+          <el-col :span="24">
+            <el-form-item
+              label="转回日期"
+              prop="returnDate"
+            >
+              <el-date-picker
+                v-model="state.form.returnDate"
+                type="date"
+                placeholder="请选择时间"
+                clearable
+                value-format="YYYY-MM-DD"
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row
+          class="mb20"
+          v-if="isReturn === 0"
+        >
+          <el-col :span="24">
+            <el-form-item
+              label="未返回动物情况说明"
+              prop="notReturnReason"
+            >
+              <el-input
+                v-model="state.form.notReturnReason"
+                placeholder="请输入"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row v-if="isReturn === 1">
+          <el-col :span="24">
+            <el-form-item
+              label="运输方式"
+              prop="returnTransport"
+            >
+              <el-input
+                v-model="state.form.returnTransport"
+                placeholder="请输入"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row
+          class="mb20"
+          v-if="isReturn === 1"
+        >
+          <el-col :span="24">
+            <el-form-item
+              label="雄性"
+              prop="returnMaleNumber"
+            >
+              <el-input-number
+                style="width: 100%"
+                placeholder="雄性数量"
+                v-model="state.form.returnMaleNumber"
+                :min="0"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row v-if="isReturn === 1">
+          <el-col :span="24">
+            <el-form-item
+              label="雌性"
+              prop="returnFemaleNumber"
+            >
+              <el-input-number
+                style="width: 100%"
+                placeholder="雌性数量"
+                v-model="state.form.returnFemaleNumber"
+                :min="0"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row
+          class="mb20"
+          :gutter="20"
+        >
+          <el-col :span="12">
+            <el-form-item
+              label="门禁卡是否归还"
+              prop="accessCardReturn"
+            >
+              <el-radio-group v-model="state.form.accessCardReturn">
+                <el-radio :label="1">是</el-radio>
+                <el-radio :label="0">否</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button
+            type="info"
+            @click="onCancel"
+            size="default"
+          >
+            取 消
+          </el-button>
+          <el-button
+            color="#2c78ff"
+            @click="onSubmit()"
+            size="default"
+          >
+            提交
+          </el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref } from 'vue'
+  import to from 'await-to-js'
+  import { ElMessage } from 'element-plus'
+  import type { FormRules, FormInstance } from 'element-plus'
+
+  import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+  import { TurnBackPayload, TakeawayList } from '/@/constants/pageConstants'
+  import { filterFields } from '/@/utils/func'
+
+  const emit = defineEmits(['refresh'])
+
+  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+
+  const expertDialogFormRef = ref<FormInstance>()
+  const animalTypeList = ref<{ id: string; name: string }[]>([])
+  const isReturn = ref<number>(1)
+
+  const rules = reactive<FormRules<TurnBackPayload>>({
+    categoryId: [{ required: true, message: '请选择动物种类', trigger: 'blur' }],
+    returnDate: [{ required: true, message: '请选择转出日期', trigger: 'blur' }],
+
+    notReturnReason: [{ required: true, message: '请输入未返回动物情况说明', trigger: 'blur' }],
+  })
+
+  const defaultFormFields: TurnBackPayload = {
+    takeawayId: 0,
+    categoryId: null,
+    returnDate: '',
+    returnTransport: '',
+    accessCardReturn: 1,
+    returnFemaleNumber: 0,
+    returnMaleNumber: 0,
+    notReturnReason: '',
+    takeawayType: '10',
+  }
+
+  const state = reactive<{
+    form: TurnBackPayload
+    dialog: { isShowDialog: boolean; type: string; title: string; submitTxt: string }
+  }>({
+    form: defaultFormFields,
+    dialog: {
+      isShowDialog: false,
+      type: '',
+      title: '',
+      submitTxt: '',
+    },
+  })
+
+  const getDicts = async () => {
+    const [_, res]: ToResponse = await to(platAnimalCageApplicationApi.getAnimalTypeList({}))
+
+    if (res) {
+      animalTypeList.value = res.data
+    }
+  }
+
+  const openDialog = async (sourceData: TakeawayList) => {
+    await getDicts()
+    state.dialog.title = '登记转回'
+    state.form.takeawayId = sourceData.id
+    state.form.categoryId = animalTypeList.value[0].id
+    state.dialog.isShowDialog = true
+  }
+
+  const closeDialog = () => {
+    expertDialogFormRef.value.resetFields()
+    state.form = defaultFormFields
+    state.dialog.isShowDialog = false
+  }
+
+  const onCancel = () => {
+    closeDialog()
+  }
+
+  const onSubmit = async () => {
+    expertDialogFormRef.value.validate(async (valid: boolean) => {
+      if (!valid) return
+
+      if (isReturn.value === 1) {
+        if (!state.form.returnMaleNumber && !state.form.returnFemaleNumber) {
+          ElMessage.error('请添加雄性或雌性数量!')
+          return
+        }
+      }
+
+      const post = platAnimalCageApplicationApi.createPlatAnimalTakeawayReback
+      const [err]: ToResponse = await to(post(filterFields(state.form)))
+
+      if (err) return
+      ElMessage.success('操作成功')
+      closeDialog()
+      emit('refresh')
+    })
+  }
+
+  defineExpose({
+    openDialog,
+  })
+</script>
+<style lang="scss" scoped>
+  .application-dialog-container {
+    .el-select {
+      width: 100%;
+    }
+  }
+  h4 {
+    font-size: 18px;
+  }
+  ul {
+    padding-left: 20px;
+  }
+  .text {
+    p {
+      text-indent: 2em;
+    }
+  }
+  .el-upload + .el-button {
+    vertical-align: top;
+  }
+  :deep(.el-checkbox) {
+    white-space: pre-wrap;
+  }
+</style>

+ 452 - 0
src/view/animal/applicationRemoval/index.vue

@@ -0,0 +1,452 @@
+<template>
+  <div class="entry-container">
+    <div
+      class="search-wrap"
+      ref="searchWrapRef"
+    >
+      <el-form
+        :model="state.queryParams"
+        ref="queryRef"
+      >
+        <el-form-item prop="applyDate">
+          <el-date-picker
+            v-model="state.queryParams.applyDate"
+            type="date"
+            style="width: 100%"
+            placeholder="申请时间"
+            clearable
+            @change="search"
+          />
+        </el-form-item>
+        <el-form-item prop="isMyself">
+          <el-select
+            v-model="state.queryParams.isMyself"
+            style="width: 100%"
+            placeholder="申请人"
+            clearable
+            @change="search"
+          >
+            <el-option
+              label="我申请的"
+              :value="1"
+            ></el-option>
+            <el-option
+              label="全部"
+              :value="0"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div style="text-align: right">
+        <el-button
+          @click="handleExport"
+          color="#2c78ff"
+        >
+          导出
+        </el-button>
+        <el-button
+          color="#2c78ff"
+          @click="openCageApplicationModal()"
+        >
+          申请带离
+        </el-button>
+      </div>
+    </div>
+
+    <div class="list-container">
+      <van-list
+        v-model:loading="state.loading"
+        :finished="state.finished"
+        finished-text="没有更多了"
+        @load="onLoad"
+      >
+        <van-cell
+          v-for="item in state.list"
+          :key="item"
+          @click="handleCheckDetail(item)"
+        >
+          <template #default>
+            <div class="list">
+              <header class="flex justify-between">
+                <strong class="title">{{ `申请带离单` }}</strong>
+              </header>
+              <p class="inst-title">
+                <span>申请编码</span>
+                <span class="title ml8">{{ item.businessCode }}</span>
+              </p>
+              <p class="inst-title">
+                <span>申请人</span>
+                <span class="title ml8">
+                  {{ item.userName }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>申请时间</span>
+                <span class="title ml8">
+                  {{ formatToChineseDate(item.createdTime) }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>带离时间</span>
+                <span class="title ml8">
+                  {{ formatToChineseDate(item.takeawayDate) }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>带离原因</span>
+                <span class="title ml8">
+                  {{ item.takeawayReason }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>动物种类</span>
+                <span class="title ml8">
+                  {{ animalTypeList.find((type) => type.id === item.categoryId)?.name }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>申请状态</span>
+                <span class="title ml8">
+                  <van-tag
+                    v-if="item.approveStatus == ApplyLeaveApproveStatus.SUBMIT"
+                    type="primary"
+                  >
+                    提交
+                  </van-tag>
+                  <van-tag
+                    v-else-if="item.approveStatus == ApplyLeaveApproveStatus.WAIT_APPROVE"
+                    type="primary"
+                  >
+                    待审核
+                  </van-tag>
+                  <van-tag
+                    v-else-if="item.approveStatus == ApplyLeaveApproveStatus.PASS"
+                    type="success"
+                  >
+                    通过
+                  </van-tag>
+                  <van-tag
+                    v-else-if="item.approveStatus == ApplyLeaveApproveStatus.REVOKE"
+                    type="success"
+                  >
+                    撤回
+                  </van-tag>
+                  <van-tag
+                    v-else-if="item.approveStatus == ApplyLeaveApproveStatus.REFUSE"
+                    type="danger"
+                  >
+                    审核不通过
+                  </van-tag>
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>带离状态</span>
+                <span class="title ml8">
+                  <van-tag
+                    v-if="item.approveStatus == ApplyLeaveApproveStatus.PASS"
+                    type="success"
+                  >
+                    已带离
+                  </van-tag>
+                  <van-tag
+                    v-else-if="
+                      item.approveStatus == ApplyLeaveApproveStatus.WAIT_APPROVE ||
+                      item.approveStatus === ApplyLeaveApproveStatus.REFUSE
+                    "
+                    type="primary"
+                  >
+                    待带离
+                  </van-tag>
+                  <van-tag
+                    v-else-if="takeawayComplate(item)"
+                    type="success"
+                  >
+                    已结束
+                  </van-tag>
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>带离数量</span>
+                <span class="title ml8">
+                  {{ item.takeawayMaleNumber + item.takewayFemaleNumber }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>淘汰数量</span>
+                <span class="title ml8">
+                  {{ item.dieMaleNumber + item.dieFemaleNumber }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>转回数量</span>
+                <span class="title ml8">
+                  {{ item.returnFemaleNumber + item.returnMaleNumber }}
+                </span>
+              </p>
+              <el-row
+                class="mt16"
+                :gutter="20"
+              >
+                <el-col :span="12">
+                  <el-button
+                    style="width: 100%"
+                    v-if="item.approveStatus === ApplyLeaveApproveStatus.PASS"
+                    type="primary"
+                    @click.stop="handleReturn(item)"
+                  >
+                    转回
+                  </el-button>
+                </el-col>
+                <el-col :span="12">
+                  <el-button
+                    style="width: 100%"
+                    v-if="item.approveStatus === ApplyLeaveApproveStatus.PASS"
+                    type="primary"
+                    @click.stop="handleDie(item)"
+                  >
+                    淘汰动物上报
+                  </el-button>
+                </el-col>
+              </el-row>
+            </div>
+          </template>
+        </van-cell>
+      </van-list>
+    </div>
+  </div>
+  <AddEdit
+    ref="addEditRef"
+    @refresh="onLoad"
+  />
+  <TurnBackModal
+    ref="turnBackModalRef"
+    @refresh="onLoad"
+  />
+  <DieModal
+    ref="dieModalRef"
+    @refresh="onLoad"
+  />
+</template>
+
+<script lang="ts" setup>
+  import { ref, reactive, onMounted } from 'vue'
+  import to from 'await-to-js'
+  import dayjs from 'dayjs'
+
+  import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+  import { ApplyLeaveApproveStatus, TakeawayList } from '/@/constants/pageConstants'
+  import AddEdit from './components/addEdit.vue'
+  import TurnBackModal from './components/turnBack.vue'
+  import DieModal from './components/dieModal.vue'
+
+  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+
+  const addEditRef = ref<InstanceType<typeof AddEdit>>()
+  const turnBackModalRef = ref<InstanceType<typeof TurnBackModal>>()
+  const dieModalRef = ref<InstanceType<typeof DieModal>>()
+
+  const animalTypeList = ref([])
+  const dateTime = ref<any>([])
+
+  const state = reactive({
+    queryParams: {
+      pageNum: 1,
+      pageSize: 10,
+      isMyself: 0,
+      applyDate: '',
+      startDate: '',
+      endDate: '',
+    },
+    loading: true,
+    list: [] as any[],
+    finished: false,
+  })
+
+  const getDicts = () => {
+    Promise.all([platAnimalCageApplicationApi.getAnimalTypeList({})]).then(([animalType]) => {
+      animalTypeList.value = animalType.data
+    })
+  }
+
+  const setListPayload = (isExport?: boolean) => {
+    const payload = {
+      ...state.queryParams,
+      pageSize: isExport ? 99999 : state.queryParams.pageSize,
+    }
+
+    if (dateTime.value && dateTime.value[0]) {
+      payload.startDate = dayjs(dateTime.value[0]).format('YYYY-MM-DD')
+    }
+
+    if (dateTime.value && dateTime.value[1]) {
+      payload.endDate = dayjs(dateTime.value[1]).format('YYYY-MM-DD')
+    }
+
+    Object.entries(payload).forEach(([key, value]) => {
+      if (value === '' || value === null) {
+        delete payload[key as keyof typeof payload]
+      }
+    })
+
+    return payload
+  }
+
+  const onLoad = async (isSearch?: boolean) => {
+    const [err, res]: ToResponse = await to(
+      platAnimalCageApplicationApi.getAnimalTakeawayApplicationsList({
+        ...setListPayload(),
+        pageNum: isSearch ? 1 : state.queryParams.pageNum,
+      }),
+    )
+    if (err) return
+
+    if (res && res.data && res.data.list) {
+      const list = res.data.list || []
+      state.loading = false
+
+      if (!isSearch) {
+        for (const item of list) {
+          state.list.push(item)
+        }
+        state.queryParams.pageNum++
+        if (list.length < state.queryParams.pageSize) {
+          state.finished = true
+        }
+      } else {
+        state.list = list
+      }
+    }
+  }
+
+  const search = () => {
+    onLoad(true)
+  }
+
+  const handleExport = async () => {
+    const [err, res]: ToResponse = await to(
+      platAnimalCageApplicationApi.getAnimalTakeawayApplicationsListExport({
+        ...setListPayload(true),
+        pageSize: 99999,
+        base64Enable: 1,
+      }),
+    )
+
+    if (err) return
+
+    if (res && res.data) {
+      const { base64, name } = res.data
+      const link = document.createElement('a')
+      link.href = `data:application/octet-stream;base64,${base64}`
+      link.download = name
+      link.style.display = 'none'
+      document.body.appendChild(link)
+      link.click()
+      document.body.removeChild(link)
+    }
+  }
+
+  const handleCheckDetail = (row: TakeawayList) => {
+    addEditRef.value?.openDialog('detail', row)
+  }
+
+  const formatToChineseDate = (dateStr: string) => {
+    const date = new Date(dateStr)
+    const year = date.getFullYear()
+    const month = String(date.getMonth() + 1).padStart(2, '0')
+    const day = String(date.getDate()).padStart(2, '0')
+
+    return `${year}年${month}月${day}日`
+  }
+
+  const takeawayComplate = (data: TakeawayList) => {
+    const {
+      dieFemaleNumber,
+      dieMaleNumber,
+      returnFemaleNumber,
+      returnMaleNumber,
+      takeawayMaleNumber,
+      takewayFemaleNumber,
+    } = data
+
+    const total = takeawayMaleNumber + takewayFemaleNumber
+    const returnTotal = returnFemaleNumber + returnMaleNumber
+    const dieTotal = dieFemaleNumber + dieMaleNumber
+
+    return returnTotal >= total || dieTotal >= total || returnTotal + dieTotal >= total
+  }
+
+  const openCageApplicationModal = () => {
+    addEditRef.value.openDialog('add')
+  }
+
+  const handleReturn = (row: TakeawayList) => {
+    turnBackModalRef.value?.openDialog(row)
+  }
+
+  const handleDie = (row: TakeawayList) => {
+    dieModalRef.value?.openDialog(row)
+  }
+
+  onMounted(() => {
+    getDicts()
+    onLoad()
+  })
+</script>
+
+<style lang="scss" scoped>
+  .entry-container {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    .search-wrap {
+      background: #fff;
+      margin-bottom: 10px;
+      padding: 15px;
+    }
+    .list-container {
+      overflow-y: auto;
+      padding: 10px;
+      border-radius: 4px;
+      flex: 1;
+    }
+    .van-list {
+      .van-cell {
+        background-color: #fff;
+        + .van-cell {
+          margin-top: 10px;
+        }
+        header,
+        footer {
+          color: #333;
+        }
+        .title {
+          flex: 1;
+          white-space: nowrap;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          text-align: left;
+        }
+        .inst-title {
+          color: #333;
+          text-align: left;
+          flex: 1;
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+          margin-top: 4px;
+        }
+        .time {
+          color: #f69a4d;
+        }
+      }
+    }
+    .inst-title {
+      :first-child {
+        color: rgb(120, 120, 120);
+      }
+    }
+  }
+  :deep(.van-tag) {
+    color: #fff !important;
+  }
+</style>

+ 1 - 1
src/view/instr/list-follow.vue

@@ -1,5 +1,5 @@
 <template>
-  <List following="20" />
+  <List following="10" />
 </template>
 
 <script lang="ts" setup>

+ 1 - 1
src/view/instr/list.vue

@@ -262,7 +262,7 @@
   const props = defineProps({
     following: {
       type: String,
-      default: '10',
+      default: '20',
     },
   })
   const router = useRouter()