浏览代码

feature:下机流程完善

liuzhenlin 1 月之前
父节点
当前提交
856b3bf83f

+ 1 - 1
src/constants/pageConstants.ts

@@ -148,7 +148,7 @@ export interface TakeawayList {
 export interface CreateAnimalApplyLeavePayload {
   accessCardNumber: string // 门禁卡序列号
   categoryId: string | null // 动物种类
-  variety: string // 动物品系
+  variety: string // 动物类别
   projectGroupId: number | null // 项目组ID
   projectGroupName: string // 项目组名称
   takeawayDate: string // 转出日期

+ 510 - 728
src/view/animal/application/components/Application.vue

@@ -1,319 +1,143 @@
 <template>
   <div class="application-dialog-container">
-    <van-popup
-      v-model:show="state.dialog.isShowDialog"
-      position="bottom"
-      :style="{ height: '90vh' }"
-      round
-      :closeable="true"
-      @close="onCancel"
-      :close-on-click-overlay="false"
-    >
+    <van-popup v-model:show="state.dialog.isShowDialog" position="bottom" :style="{ height: '90vh' }" round
+      :closeable="true" @close="onCancel" :close-on-click-overlay="false">
       <div class="popup-wrapper">
         <h3 class="popup-title">{{ state.dialog.title }}</h3>
         <div class="popup-content">
-          <van-form
-        ref="expertDialogFormRef"
-            @submit="onSubmit"
-      >
-        <h4 class="mb8 mt8">基本信息</h4>
+          <van-form ref="expertDialogFormRef" @submit="onSubmit">
+            <h4 class="mb8 mt8">基本信息</h4>
             <van-cell-group>
-              <van-field
-                v-model="state.form.projectName"
-              label="课题名称"
-                placeholder="请选择"
-                readonly
-                is-link
-                required
-                @click="showProjectPicker = true"
-                :rules="rules.projectGroupId"
-              />
-              <van-field
-                v-model="userInfos.userName"
-                label="姓名"
-                placeholder="姓名"
-                readonly
-              />
-              <van-field
-                v-model="userInfos.deptName"
-                label="部门"
-                placeholder="部门"
-                readonly
-              />
-              <van-field
-                v-model="userInfos.phone"
-                label="联系方式"
-                placeholder="联系方式"
-                readonly
-              />
+              <van-field v-model="state.form.projectName" label="课题名称" placeholder="请选择" readonly is-link required
+                @click="showProjectPicker = true" :rules="rules.projectGroupId" />
+              <van-field v-model="userInfos.userName" label="姓名" placeholder="姓名" readonly />
+              <van-field v-model="userInfos.deptName" label="部门" placeholder="部门" readonly />
+              <van-field v-model="userInfos.phone" label="联系方式" placeholder="联系方式" readonly />
             </van-cell-group>
 
-        <h4 class="mb8 mt10">实验动物笼位预约信息</h4>
+            <h4 class="mb8 mt10">实验动物笼位预约信息</h4>
             <van-cell-group>
-              <van-field
-                v-model="state.form.number"
-              label="笼位数量"
-                placeholder="笼位数量"
-                type="digit"
-                required
-                :rules="rules.number"
-            >
+              <van-field v-model="state.form.number" label="笼位数量" placeholder="笼位数量" type="digit" required
+                :rules="rules.number">
                 <template #button>
-                  <van-stepper
-                v-model="state.form.number"
-                :min="1"
-                    integer
-                  />
+                  <van-stepper v-model="state.form.number" :min="1" integer />
                 </template>
               </van-field>
-              <van-field
-                v-model="state.form.startDate"
-                label="开始使用时间"
-                placeholder="请选择时间"
-                readonly
-                is-link
-                required
-                @click="showStartDatePicker = true"
-                :rules="rules.startDate"
-              />
-              <van-field
-                v-model="state.form.categoryName"
-              label="品种品系"
-                placeholder="请选择"
-                readonly
-                is-link
-                required
-                @click="showCategoryPicker = true"
-                :rules="rules.categoryId"
-              />
-              <van-field
-                v-model="state.form.variety"
-                label="品种品系"
-                placeholder="请输入品种品系"
-              />
-              <van-field
-                v-model="state.form.levelName"
-              label="饲养区域"
-                placeholder="请选择"
-                readonly
-                is-link
-                @click="showLevelPicker = true"
-              />
-              <van-field
-                v-model="state.form.age"
-              label="周龄"
-                placeholder="请输入周龄"
-                type="digit"
-            >
+              <van-field v-model="state.form.startDate" label="开始使用时间" placeholder="请选择时间" readonly is-link required
+                @click="showStartDatePicker = true" :rules="rules.startDate" />
+              <van-field v-model="state.form.categoryName" label="动物类别" placeholder="请选择" readonly is-link required
+                @click="showCategoryPicker = true" :rules="rules.categoryId" />
+              <van-field v-model="state.form.variety" label="品种品系" placeholder="请输入品种品系" required
+                :rules="rules.variety" />
+              <van-field v-model="state.form.levelName" label="饲养区域" placeholder="请选择" readonly is-link
+                @click="showLevelPicker = true" />
+              <van-field v-model="state.form.age" label="周龄" placeholder="请输入周龄" type="digit">
                 <template #button>
-                  <van-stepper
-                v-model="state.form.age"
-                :min="0"
-                    integer
-                  />
+                  <van-stepper v-model="state.form.age" :min="0" integer />
                 </template>
               </van-field>
-              <van-field
-                v-model="state.form.weight"
-              label="体重"
-                placeholder="请输入体重"
-                type="number"
-            >
+              <van-field v-model="state.form.weight" label="体重" placeholder="请输入体重" type="number">
                 <template #button>
-                  <van-stepper
-                v-model="state.form.weight"
-                :min="0"
-              />
+                  <van-stepper v-model="state.form.weight" :min="0" />
                 </template>
               </van-field>
-              <van-field
-                v-model="state.form.maleNumber"
-              label="雄性"
-                placeholder="雄性数量"
-                type="digit"
-              >
+              <van-field v-model="state.form.maleNumber" label="雄性" placeholder="雄性数量" type="digit">
                 <template #button>
-                  <van-stepper
-                v-model="state.form.maleNumber"
-                :min="0"
-                    integer
-                  />
+                  <van-stepper v-model="state.form.maleNumber" :min="0" integer />
                 </template>
               </van-field>
-              <van-field
-                v-model="state.form.famaleNumber"
-              label="雌性"
-                placeholder="雌性数量"
-                type="digit"
-              >
+              <van-field v-model="state.form.famaleNumber" label="雌性" placeholder="雌性数量" type="digit">
                 <template #button>
-                  <van-stepper
-                v-model="state.form.famaleNumber"
-                :min="0"
-                    integer
-                  />
+                  <van-stepper v-model="state.form.famaleNumber" :min="0" integer />
                 </template>
               </van-field>
-              <van-field
-                v-model="state.form.totalNumber"
-              label="合计"
-                placeholder="合计"
-                readonly
-                type="digit"
-              >
+              <van-field v-model="state.form.totalNumber" label="合计" placeholder="合计" readonly type="digit">
                 <!-- <template #button>
                   <van-stepper v-model="state.form.totalNumber" :min="0" integer disabled />
                 </template> -->
               </van-field>
-              <van-field
-                v-model="state.form.feedingDay"
-              label="饲养总天数"
-                placeholder="饲养总天数"
-                type="digit"
-                required
-                :rules="rules.feedingDay"
-            >
+              <van-field v-model="state.form.feedingDay" label="饲养总天数" placeholder="饲养总天数" type="digit" required
+                :rules="rules.feedingDay">
                 <template #button>
-                  <van-stepper
-                v-model="state.form.feedingDay"
-                :min="1"
-                    integer
-                  />
+                  <van-stepper v-model="state.form.feedingDay" :min="1" integer />
                 </template>
               </van-field>
             </van-cell-group>
 
-        <h4 class="mb8 mt20">采购渠道</h4>
+            <h4 class="mb8 mt20">采购渠道</h4>
             <van-cell-group>
-              <van-field
-              label="采购渠道"
-                required
-                :rules="rules.buyFrom"
-              >
+              <van-field label="采购渠道" required :rules="rules.buyFrom">
                 <template #input>
-                  <van-radio-group
-                    v-model="state.form.buyFrom"
-                    direction="horizontal"
-                  >
+                  <van-radio-group v-model="state.form.buyFrom" direction="horizontal">
                     <van-radio :name="ProcurementChannels.PURCHASED_BY_OTHERS">动物房代购</van-radio>
                     <van-radio :name="ProcurementChannels.PURCHASED_BY_MYSELF">自行购买</van-radio>
                   </van-radio-group>
                 </template>
               </van-field>
-              <van-field
-          v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF"
-                v-model="state.form.comeFromUnit"
-              label="外购来源单位"
-                placeholder="请输入外购来源单位"
-                required
-                :rules="rules.comeFromUnit"
-              />
-              <van-field
-                v-model="state.form.comeTime"
-                label="动物到达时间"
-                placeholder="请选择到达时间"
-                readonly
-                is-link
-                @click="showComeTimePicker = true"
-              />
+              <van-field v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF"
+                v-model="state.form.comeFromUnit" label="外购来源单位" placeholder="请输入外购来源单位" required
+                :rules="rules.comeFromUnit" />
+              <van-field v-model="state.form.comeTime" label="动物到达时间" placeholder="请选择到达时间" readonly is-link
+                @click="showComeTimePicker = true" />
             </van-cell-group>
 
             <!-- 自行购买时的文件上传 -->
             <template v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
               <h4 class="mb8 mt20">文件上传</h4>
               <van-cell-group>
-                <van-cell
-                  title="生产许可证副本"
-                  required
-                  :rules="rules.licenseNumberFile"
-                >
-                  <van-uploader
-                    v-model="licenseNumberFileList"
+                <van-cell title="生产许可证副本" required :rules="rules.licenseNumberFile">
+                  <van-uploader v-model="licenseNumberFileList"
                     :after-read="(file) => handleFileUpload(file, UploadFileType.LICENSE_NUMBER)"
-                    :before-delete="() => handleRemove(UploadFileType.LICENSE_NUMBER)"
-                    :max-count="1"
-                :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX"
-                  />
+                    :before-delete="() => handleRemove(UploadFileType.LICENSE_NUMBER)" :max-count="1"
+                    :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX" />
                   <div class="upload-tip">支持格式:jpg png pdf等,单个文件不超过20MB</div>
                 </van-cell>
-                <van-cell
-                  title="近三个月动物质量检测证明"
-                  required
-                  :rules="rules.animalTestDateFile"
-                >
-                  <van-uploader
-                    v-model="animalTestDateFileList"
+                <van-cell title="近三个月动物质量检测证明" required :rules="rules.animalTestDateFile">
+                  <van-uploader v-model="animalTestDateFileList"
                     :after-read="(file) => handleFileUpload(file, UploadFileType.ANIMAL_TEST_DATE)"
-                    :before-delete="() => handleRemove(UploadFileType.ANIMAL_TEST_DATE)"
-                    :max-count="1"
-                :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX"
-                  />
+                    :before-delete="() => handleRemove(UploadFileType.ANIMAL_TEST_DATE)" :max-count="1"
+                    :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX" />
                   <div class="upload-tip">支持格式:jpg png pdf等,单个文件不超过20MB</div>
                 </van-cell>
                 <van-cell title="基因鉴定报告">
-                  <van-uploader
-                    v-model="geneIdentificationFileList"
+                  <van-uploader v-model="geneIdentificationFileList"
                     :after-read="(file) => handleFileUpload(file, UploadFileType.ENV_TEST_DATE)"
-                    :before-delete="() => handleRemove(UploadFileType.ENV_TEST_DATE)"
-                    :max-count="1"
-                :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX"
-                  />
+                    :before-delete="() => handleRemove(UploadFileType.ENV_TEST_DATE)" :max-count="1"
+                    :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX" />
                   <div class="upload-tip">支持格式:jpg png pdf等,单个文件不超过20MB</div>
                 </van-cell>
               </van-cell-group>
             </template>
 
-        <h4 class="mb8 mt20">特殊要求和附件</h4>
+            <h4 class="mb8 mt20">特殊要求和附件</h4>
             <van-cell-group>
               <van-field label="是否有特殊饲养要求">
                 <template #input>
-                  <van-radio-group
-                    v-model="state.form.hasFeedingSpecial"
-                    direction="horizontal"
-                  >
+                  <van-radio-group v-model="state.form.hasFeedingSpecial" direction="horizontal">
                     <van-radio :name="FeedingSpecial.HAVE_FEEDING_SPECIAL">有</van-radio>
                     <van-radio :name="FeedingSpecial.NO_FEEDING_SPECIAL">无</van-radio>
                   </van-radio-group>
                 </template>
               </van-field>
-              <van-field
-            v-if="state.form.hasFeedingSpecial === FeedingSpecial.HAVE_FEEDING_SPECIAL"
-                v-model="state.form.feedingSpecialDesc"
-              label="特殊饲养要求"
-                placeholder="输入特殊饲养要求,如每天更换垫料等"
-                required
-                :rules="rules.feedingSpecialDesc"
-              />
+              <van-field v-if="state.form.hasFeedingSpecial === FeedingSpecial.HAVE_FEEDING_SPECIAL"
+                v-model="state.form.feedingSpecialDesc" label="特殊饲养要求" placeholder="输入特殊饲养要求,如每天更换垫料等" required
+                :rules="rules.feedingSpecialDesc" />
             </van-cell-group>
 
             <h4 class="mb8 mt20">附件上传</h4>
             <van-cell-group>
-              <van-cell
-                title="实验动物福利伦理审查申请表"
-                required
-                :rules="rules.ethicsCheckFile"
-              >
-                <van-uploader
-                  v-model="ethicsCheckFileList"
+              <van-cell title="实验动物福利伦理审查申请表" required :rules="rules.ethicsCheckFile">
+                <van-uploader v-model="ethicsCheckFileList"
                   :after-read="(file) => handleFileUpload(file, UploadFileType.ETHICS_CHECK_FILE)"
-                  :before-delete="() => handleRemove(UploadFileType.ETHICS_CHECK_FILE)"
-                  :max-count="1"
-                  :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX"
-                />
+                  :before-delete="() => handleRemove(UploadFileType.ETHICS_CHECK_FILE)" :max-count="1"
+                  :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX" />
                 <div class="upload-tip">支持格式:jpg png pdf等,单个文件不超过20MB</div>
               </van-cell>
-              <van-cell
-                title="实验动物福利伦理审查意见表"
-                required
-                :rules="rules.ethicsAdviceFile"
-              >
-                <van-uploader
-                  v-model="ethicsAdviceFileList"
+              <van-cell title="实验动物福利伦理审查意见表" required :rules="rules.ethicsAdviceFile">
+                <van-uploader v-model="ethicsAdviceFileList"
                   :after-read="(file) => handleFileUpload(file, UploadFileType.ETHICS_ADVICE_FILE)"
-                  :before-delete="() => handleRemove(UploadFileType.ETHICS_ADVICE_FILE)"
-                  :max-count="1"
-                  :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX"
-                />
+                  :before-delete="() => handleRemove(UploadFileType.ETHICS_ADVICE_FILE)" :max-count="1"
+                  :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX" />
                 <div class="upload-tip">支持格式:jpg png pdf等,单个文件不超过20MB</div>
               </van-cell>
             </van-cell-group>
@@ -328,12 +152,7 @@
           </van-form>
         </div>
         <div class="dialog-footer">
-          <van-button
-            type="primary"
-            @click="onSubmit"
-            block
-            native-type="submit"
-          >
+          <van-button type="primary" @click="onSubmit" block native-type="submit">
             提交
           </van-button>
         </div>
@@ -341,555 +160,518 @@
     </van-popup>
 
     <!-- 课题选择器 -->
-    <van-popup
-      v-model:show="showProjectPicker"
-      position="bottom"
-    >
-      <van-picker
-        :columns="projects"
-        :columns-field-names="{ text: 'projectName', value: 'id' }"
-        @confirm="onProjectConfirm"
-        @cancel="showProjectPicker = false"
-      />
+    <van-popup v-model:show="showProjectPicker" position="bottom">
+      <van-picker :columns="projects" :columns-field-names="{ text: 'projectName', value: 'id' }"
+        @confirm="onProjectConfirm" @cancel="showProjectPicker = false" />
     </van-popup>
 
-    <!-- 品种品系选择器 -->
-    <van-popup
-      v-model:show="showCategoryPicker"
-      position="bottom"
-    >
-      <van-picker
-        :columns="animalTypeList"
-        :columns-field-names="{ text: 'name', value: 'id' }"
-        @confirm="onCategoryConfirm"
-        @cancel="showCategoryPicker = false"
-      />
+    <!-- 动物类别选择器 -->
+    <van-popup v-model:show="showCategoryPicker" position="bottom">
+      <van-picker :columns="animalTypeList" :columns-field-names="{ text: 'name', value: 'id' }"
+        @confirm="onCategoryConfirm" @cancel="showCategoryPicker = false" />
     </van-popup>
 
     <!-- 饲养区域选择器 -->
-    <van-popup
-      v-model:show="showLevelPicker"
-      position="bottom"
-    >
-      <van-picker
-        :columns="LeavelList"
-        :columns-field-names="{ text: 'name', value: 'id' }"
-        @confirm="onLevelConfirm"
-        @cancel="showLevelPicker = false"
-      />
+    <van-popup v-model:show="showLevelPicker" position="bottom">
+      <van-picker :columns="LeavelList" :columns-field-names="{ text: 'name', value: 'id' }" @confirm="onLevelConfirm"
+        @cancel="showLevelPicker = false" />
     </van-popup>
 
     <!-- 开始使用时间选择器 -->
-    <van-popup
-      v-model:show="showStartDatePicker"
-      position="bottom"
-      :style="{ height: '80vh' }"
-      round
-    >
-      <van-calendar
-        v-model:show="showStartDatePicker"
-        @confirm="onStartDateConfirm"
-        :min-date="new Date()"
-      />
+    <van-popup v-model:show="showStartDatePicker" position="bottom" :style="{ height: '80vh' }" round>
+      <van-calendar v-model:show="showStartDatePicker" @confirm="onStartDateConfirm" :min-date="new Date()" />
     </van-popup>
 
     <!-- 动物到达时间选择器 -->
-    <van-popup
-      v-model:show="showComeTimePicker"
-      position="bottom"
-      :style="{ height: '80vh' }"
-      round
-    >
-      <van-calendar
-        v-model:show="showComeTimePicker"
-        @confirm="onComeTimeConfirm"
-      />
+    <van-popup v-model:show="showComeTimePicker" position="bottom" :style="{ height: '80vh' }" round>
+      <van-calendar v-model:show="showComeTimePicker" @confirm="onComeTimeConfirm" />
     </van-popup>
   </div>
 </template>
 
 <script setup lang="ts" name="systemProDialog">
-  import { reactive, ref, watch } from 'vue'
-  import to from 'await-to-js'
-  import { showToast, showNotify } from 'vant'
-  import type { FormInstance } from 'vant/es'
-  import dayjs from 'dayjs'
-  import { storeToRefs } from 'pinia'
-  import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
-  import { LeavelList, SUPPORT_FILE_UPLOAD_TYPE_MAX } from '/@/constants/pageConstants'
-  import { deepClone } from '/@/utils/other'
-  import { useUserInfo } from '/@/stores/userInfo'
-  import { ProcurementChannels, FeedingSpecial, UploadFileType } from '/@/constants/pageConstants'
-  import { handleUpload } from '/@/utils/upload'
-  import { formatDate } from '/@/utils/formatTime'
-
-  const stores = useUserInfo()
-  const { userInfos } = storeToRefs(stores)
-
-  // 定义子组件向父组件传值/事件
-  const emit = defineEmits(['refresh'])
-
-  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
-
-  const expertDialogFormRef = ref<FormInstance>()
-  const projectGroupList = ref<any[]>([])
-  const projects = ref<any[]>([])
-  const showProjectPicker = ref(false)
-  const showCategoryPicker = ref(false)
-  const showLevelPicker = ref(false)
-  const showStartDatePicker = ref(false)
-  const showComeTimePicker = ref(false)
-
-  const rules = {
-    projectGroupId: [{ required: true, message: '课题名称不能为空' }],
-    categoryId: [{ required: true, message: '品种品系不能为空' }],
-    number: [{ required: true, message: '笼位数量不能为空' }],
-    startDate: [{ required: true, message: '开始使用时间不能为空' }],
-    buyFrom: [{ required: true, message: '采购渠道不能为空' }],
-    feedingDay: [{ required: true, message: '饲养总天数不能为空' }],
-    comeFromUnit: [{ required: true, message: '外购来源单位不能为空' }],
-    feedingSpecialDesc: [{ required: true, message: '特殊饲养要求不能为空' }],
-    ethicsCheckFile: [{ required: true, message: '实验动物福利伦理审查申请表不能为空' }],
-    ethicsAdviceFile: [{ required: true, message: '实验动物福利伦理审查意见表不能为空' }],
-    licenseNumberFile: [{ required: true, message: '生产许可证副本不能为空' }],
-    animalTestDateFile: [{ required: true, message: '近三个月动物质量检测证明不能为空' }],
-  }
-
-  const licenseNumberFileList = ref<any[]>([])
-  const animalTestDateFileList = ref<any[]>([])
-  const geneIdentificationFileList = ref<any[]>([])
-  const ethicsCheckFileList = ref<any[]>([])
-  const ethicsAdviceFileList = ref<any[]>([])
-
-  const safePromise = ref<boolean>(false)
-  const animalTypeList = ref<any[]>([])
-
-  const defaultFormData = {
-      projectGroupName: '',
-      projectGroupId: null,
-    projectName: '',
-      categoryName: '',
-    variety: '',
-      categoryId: null,
-      level: null,
-    levelName: '',
-      number: 1,
-      startDate: '',
-      maleNumber: 0,
-      famaleNumber: 0,
-      weight: 0,
-      age: 0,
-      feedingDay: 0,
-      buyFrom: ProcurementChannels.PURCHASED_BY_OTHERS,
-      comeTime: '',
-      comeFromUnit: '',
-      licenseNumberFile: [],
-      animalTestDateFile: [],
-      geneIdentificationFile: [],
-      hasFeedingSpecial: FeedingSpecial.HAVE_FEEDING_SPECIAL,
-      feedingSpecialDesc: '',
-      ethicsCheckFile: [],
-      ethicsAdviceFile: [],
-      deptName: '',
-      phone: '',
-      totalNumber: 0,
-  }
-
-  const state = reactive({
-    SUPPORT_FILE_UPLOAD_TYPE_MAX,
-    form: { ...defaultFormData },
-    dialog: {
-      isShowDialog: false,
-      type: '',
-      title: '',
-      submitTxt: '',
-    },
+import { reactive, ref, watch } from 'vue'
+import to from 'await-to-js'
+import { showToast, showNotify } from 'vant'
+import type { FormInstance } from 'vant/es'
+import dayjs from 'dayjs'
+import { storeToRefs } from 'pinia'
+import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+import { LeavelList, SUPPORT_FILE_UPLOAD_TYPE_MAX } from '/@/constants/pageConstants'
+import { deepClone } from '/@/utils/other'
+import { useUserInfo } from '/@/stores/userInfo'
+import { ProcurementChannels, FeedingSpecial, UploadFileType } from '/@/constants/pageConstants'
+import { handleUpload } from '/@/utils/upload'
+import { formatDate } from '/@/utils/formatTime'
+
+const stores = useUserInfo()
+const { userInfos } = storeToRefs(stores)
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['refresh'])
+
+const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+
+const expertDialogFormRef = ref<FormInstance>()
+const projectGroupList = ref<any[]>([])
+const projects = ref<any[]>([])
+const showProjectPicker = ref(false)
+const showCategoryPicker = ref(false)
+const showLevelPicker = ref(false)
+const showStartDatePicker = ref(false)
+const showComeTimePicker = ref(false)
+
+const rules = {
+  projectGroupId: [{ required: true, message: '课题名称不能为空' }],
+  categoryId: [{ required: true, message: '动物类别不能为空' }],
+  variety: [{ required: true, message: '品种品系不能为空' }],
+  number: [{ required: true, message: '笼位数量不能为空' }],
+  startDate: [{ required: true, message: '开始使用时间不能为空' }],
+  buyFrom: [{ required: true, message: '采购渠道不能为空' }],
+  feedingDay: [{ required: true, message: '饲养总天数不能为空' }],
+  comeFromUnit: [{ required: true, message: '外购来源单位不能为空' }],
+  feedingSpecialDesc: [{ required: true, message: '特殊饲养要求不能为空' }],
+  ethicsCheckFile: [{ required: true, message: '实验动物福利伦理审查申请表不能为空' }],
+  ethicsAdviceFile: [{ required: true, message: '实验动物福利伦理审查意见表不能为空' }],
+  licenseNumberFile: [{ required: true, message: '生产许可证副本不能为空' }],
+  animalTestDateFile: [{ required: true, message: '近三个月动物质量检测证明不能为空' }],
+}
+
+const licenseNumberFileList = ref<any[]>([])
+const animalTestDateFileList = ref<any[]>([])
+const geneIdentificationFileList = ref<any[]>([])
+const ethicsCheckFileList = ref<any[]>([])
+const ethicsAdviceFileList = ref<any[]>([])
+
+const safePromise = ref<boolean>(false)
+const animalTypeList = ref<any[]>([])
+
+const defaultFormData = {
+  projectGroupName: '',
+  projectGroupId: null,
+  projectName: '',
+  categoryName: '',
+  variety: '',
+  categoryId: null,
+  level: null,
+  levelName: '',
+  number: 1,
+  startDate: '',
+  maleNumber: 0,
+  famaleNumber: 0,
+  weight: 0,
+  age: 0,
+  feedingDay: 0,
+  buyFrom: ProcurementChannels.PURCHASED_BY_OTHERS,
+  comeTime: '',
+  comeFromUnit: '',
+  licenseNumberFile: [],
+  animalTestDateFile: [],
+  geneIdentificationFile: [],
+  hasFeedingSpecial: FeedingSpecial.HAVE_FEEDING_SPECIAL,
+  feedingSpecialDesc: '',
+  ethicsCheckFile: [],
+  ethicsAdviceFile: [],
+  deptName: '',
+  phone: '',
+  totalNumber: 0,
+}
+
+const state = reactive({
+  SUPPORT_FILE_UPLOAD_TYPE_MAX,
+  form: { ...defaultFormData },
+  dialog: {
+    isShowDialog: false,
+    type: '',
+    title: '',
+    submitTxt: '',
+  },
+})
+
+const getDicts = () => {
+  Promise.all([
+    platAnimalCageApplicationApi.getAnimalTypeList({}),
+    platAnimalCageApplicationApi.getProjectGroup({}),
+  ]).then(([animalType, projectGroup]) => {
+    animalTypeList.value = animalType.data
+    if (projectGroup && projectGroup.data) {
+      projectGroupList.value = projectGroup.data
+      const currentProject = projectGroup.data[0]?.projects
+      if (currentProject) {
+        projects.value = currentProject
+      }
+    }
   })
+}
+
+// 重置表单数据
+const resetForm = () => {
+  state.form = { ...defaultFormData }
+  expertDialogFormRef.value?.resetValidation()
+  licenseNumberFileList.value = []
+  animalTestDateFileList.value = []
+  geneIdentificationFileList.value = []
+  ethicsCheckFileList.value = []
+  ethicsAdviceFileList.value = []
+  safePromise.value = false
+}
+
+// 打开弹窗
+const openDialog = () => {
+  resetForm()
+  getDicts()
+  state.dialog.title = '新增实验动物笼位申请'
+  state.dialog.isShowDialog = true
+}
+
+// 关闭弹窗
+const closeDialog = () => {
+  expertDialogFormRef.value?.resetValidation()
+  state.dialog.isShowDialog = false
+  licenseNumberFileList.value = []
+  animalTestDateFileList.value = []
+  geneIdentificationFileList.value = []
+  ethicsCheckFileList.value = []
+  ethicsAdviceFileList.value = []
+  safePromise.value = false
+}
+
+// 取消
+const onCancel = () => {
+  closeDialog()
+}
+
+const handleFileUpload = async (file: any, type: UploadFileType) => {
+  // 处理单个文件或文件数组
+  const files = Array.isArray(file) ? file : [file]
+
+  for (const item of files) {
+    // 检查文件大小
+    if (item.file && item.file.size / 1024 / 1024 > 20) {
+      showNotify({
+        type: 'warning',
+        message: '上传文件大小不能超过 20MB!',
+      })
+      // 移除文件
+      if (type === UploadFileType.LICENSE_NUMBER) {
+        licenseNumberFileList.value = licenseNumberFileList.value.filter((f) => f !== item)
+      } else if (type === UploadFileType.ANIMAL_TEST_DATE) {
+        animalTestDateFileList.value = animalTestDateFileList.value.filter((f) => f !== item)
+      } else if (type === UploadFileType.ENV_TEST_DATE) {
+        geneIdentificationFileList.value = geneIdentificationFileList.value.filter((f) => f !== item)
+      } else if (type === UploadFileType.ETHICS_CHECK_FILE) {
+        ethicsCheckFileList.value = ethicsCheckFileList.value.filter((f) => f !== item)
+      } else if (type === UploadFileType.ETHICS_ADVICE_FILE) {
+        ethicsAdviceFileList.value = ethicsAdviceFileList.value.filter((f) => f !== item)
+      }
+      return
+    }
 
-  const getDicts = () => {
-    Promise.all([
-      platAnimalCageApplicationApi.getAnimalTypeList({}),
-      platAnimalCageApplicationApi.getProjectGroup({}),
-    ]).then(([animalType, projectGroup]) => {
-      animalTypeList.value = animalType.data
-      if (projectGroup && projectGroup.data) {
-        projectGroupList.value = projectGroup.data
-        const currentProject = projectGroup.data[0]?.projects
-        if (currentProject) {
-          projects.value = currentProject
-        }
+    if (item.file) {
+      item.status = 'uploading'
+      const [err, res]: ToResponse = await to(handleUpload(item.file))
+      if (err) {
+        item.status = 'failed'
+        showNotify({
+          type: 'danger',
+          message: '上传失败',
+        })
+        return
       }
-    })
+      item.status = 'done'
+      item.url = res
+      item.name = item.file.name
+
+      // 保存到 form
+      if (type === UploadFileType.LICENSE_NUMBER) {
+        state.form.licenseNumberFile = [{ name: item.name, url: res }]
+      } else if (type === UploadFileType.ANIMAL_TEST_DATE) {
+        state.form.animalTestDateFile = [{ name: item.name, url: res }]
+      } else if (type === UploadFileType.ENV_TEST_DATE) {
+        state.form.geneIdentificationFile = [{ name: item.name, url: res }]
+      } else if (type === UploadFileType.ETHICS_CHECK_FILE) {
+        state.form.ethicsCheckFile = [{ name: item.name, url: res }]
+      } else if (type === UploadFileType.ETHICS_ADVICE_FILE) {
+        state.form.ethicsAdviceFile = [{ name: item.name, url: res }]
+      }
+    }
   }
+}
 
-  // 重置表单数据
-  const resetForm = () => {
-    state.form = { ...defaultFormData }
-    expertDialogFormRef.value?.resetValidation()
+const handleRemove = (type: UploadFileType) => {
+  if (type === UploadFileType.LICENSE_NUMBER) {
     licenseNumberFileList.value = []
+    state.form.licenseNumberFile = []
+  } else if (type === UploadFileType.ANIMAL_TEST_DATE) {
     animalTestDateFileList.value = []
+    state.form.animalTestDateFile = []
+  } else if (type === UploadFileType.ENV_TEST_DATE) {
     geneIdentificationFileList.value = []
+    state.form.geneIdentificationFile = []
+  } else if (type === UploadFileType.ETHICS_CHECK_FILE) {
     ethicsCheckFileList.value = []
+    state.form.ethicsCheckFile = []
+  } else if (type === UploadFileType.ETHICS_ADVICE_FILE) {
     ethicsAdviceFileList.value = []
-    safePromise.value = false
+    state.form.ethicsAdviceFile = []
   }
-
-  // 打开弹窗
-  const openDialog = () => {
-    resetForm()
-    getDicts()
-    state.dialog.title = '新增实验动物笼位申请'
-    state.dialog.isShowDialog = true
+  return true
+}
+
+const onProjectConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
+  if (selectedOptions.length > 0) {
+    const selected = selectedOptions[0]
+    state.form.projectGroupId = selected.id
+    state.form.projectName = selected.projectName
   }
-
-  // 关闭弹窗
-  const closeDialog = () => {
-    expertDialogFormRef.value?.resetValidation()
-    state.dialog.isShowDialog = false
-    licenseNumberFileList.value = []
-    animalTestDateFileList.value = []
-    geneIdentificationFileList.value = []
-    ethicsCheckFileList.value = []
-    ethicsAdviceFileList.value = []
-    safePromise.value = false
+  showProjectPicker.value = false
+}
+
+const onCategoryConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
+  if (selectedOptions.length > 0) {
+    const selected = selectedOptions[0]
+    state.form.categoryId = selected.id
+    state.form.categoryName = selected.name
   }
-
-  // 取消
-  const onCancel = () => {
-    closeDialog()
+  showCategoryPicker.value = false
+}
+
+const onLevelConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
+  if (selectedOptions.length > 0) {
+    const selected = selectedOptions[0]
+    state.form.level = selected.id
+    state.form.levelName = selected.name
   }
-
-  const handleFileUpload = async (file: any, type: UploadFileType) => {
-    // 处理单个文件或文件数组
-    const files = Array.isArray(file) ? file : [file]
-
-    for (const item of files) {
-      // 检查文件大小
-      if (item.file && item.file.size / 1024 / 1024 > 20) {
-        showNotify({
-          type: 'warning',
-          message: '上传文件大小不能超过 20MB!',
-        })
-        // 移除文件
-        if (type === UploadFileType.LICENSE_NUMBER) {
-          licenseNumberFileList.value = licenseNumberFileList.value.filter((f) => f !== item)
-        } else if (type === UploadFileType.ANIMAL_TEST_DATE) {
-          animalTestDateFileList.value = animalTestDateFileList.value.filter((f) => f !== item)
-        } else if (type === UploadFileType.ENV_TEST_DATE) {
-          geneIdentificationFileList.value = geneIdentificationFileList.value.filter((f) => f !== item)
-        } else if (type === UploadFileType.ETHICS_CHECK_FILE) {
-          ethicsCheckFileList.value = ethicsCheckFileList.value.filter((f) => f !== item)
-        } else if (type === UploadFileType.ETHICS_ADVICE_FILE) {
-          ethicsAdviceFileList.value = ethicsAdviceFileList.value.filter((f) => f !== item)
-        }
-        return
-      }
-
-      if (item.file) {
-        item.status = 'uploading'
-        const [err, res]: ToResponse = await to(handleUpload(item.file))
-        if (err) {
-          item.status = 'failed'
-          showNotify({
-            type: 'danger',
-            message: '上传失败',
-          })
-          return
-        }
-        item.status = 'done'
-        item.url = res
-        item.name = item.file.name
-
-        // 保存到 form
-        if (type === UploadFileType.LICENSE_NUMBER) {
-          state.form.licenseNumberFile = [{ name: item.name, url: res }]
-        } else if (type === UploadFileType.ANIMAL_TEST_DATE) {
-          state.form.animalTestDateFile = [{ name: item.name, url: res }]
-        } else if (type === UploadFileType.ENV_TEST_DATE) {
-          state.form.geneIdentificationFile = [{ name: item.name, url: res }]
-        } else if (type === UploadFileType.ETHICS_CHECK_FILE) {
-          state.form.ethicsCheckFile = [{ name: item.name, url: res }]
-        } else if (type === UploadFileType.ETHICS_ADVICE_FILE) {
-          state.form.ethicsAdviceFile = [{ name: item.name, url: res }]
+  showLevelPicker.value = false
+}
+
+const onStartDateConfirm = (date: Date) => {
+  state.form.startDate = formatDate(date, 'YYYY-mm-dd')
+  showStartDatePicker.value = false
+}
+
+const onComeTimeConfirm = (date: Date) => {
+  state.form.comeTime = formatDate(date, 'YYYY-mm-dd')
+  showComeTimePicker.value = false
+}
+
+// 提交
+const onSubmit = async () => {
+  try {
+    await expertDialogFormRef.value?.validate()
+  } catch (error: any) {
+    // 显示表单验证错误
+    let errorMessage = '请完善必填信息'
+    if (error) {
+      // Vant 表单验证错误可能是数组格式
+      if (Array.isArray(error) && error.length > 0) {
+        const firstError = error[0]
+        if (firstError && firstError.message) {
+          errorMessage = firstError.message
         }
+      } else if (error.message) {
+        errorMessage = error.message
       }
     }
+    showNotify({
+      type: 'warning',
+      message: errorMessage,
+    })
+    return
   }
 
-  const handleRemove = (type: UploadFileType) => {
-    if (type === UploadFileType.LICENSE_NUMBER) {
-      licenseNumberFileList.value = []
-      state.form.licenseNumberFile = []
-    } else if (type === UploadFileType.ANIMAL_TEST_DATE) {
-      animalTestDateFileList.value = []
-      state.form.animalTestDateFile = []
-    } else if (type === UploadFileType.ENV_TEST_DATE) {
-      geneIdentificationFileList.value = []
-      state.form.geneIdentificationFile = []
-    } else if (type === UploadFileType.ETHICS_CHECK_FILE) {
-      ethicsCheckFileList.value = []
-      state.form.ethicsCheckFile = []
-    } else if (type === UploadFileType.ETHICS_ADVICE_FILE) {
-      ethicsAdviceFileList.value = []
-      state.form.ethicsAdviceFile = []
-    }
-    return true
-  }
-
-  const onProjectConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
-    if (selectedOptions.length > 0) {
-      const selected = selectedOptions[0]
-      state.form.projectGroupId = selected.id
-      state.form.projectName = selected.projectName
-    }
-    showProjectPicker.value = false
-  }
-
-  const onCategoryConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
-    if (selectedOptions.length > 0) {
-      const selected = selectedOptions[0]
-      state.form.categoryId = selected.id
-      state.form.categoryName = selected.name
-    }
-    showCategoryPicker.value = false
-  }
-
-  const onLevelConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
-    if (selectedOptions.length > 0) {
-      const selected = selectedOptions[0]
-      state.form.level = selected.id
-      state.form.levelName = selected.name
-    }
-    showLevelPicker.value = false
-  }
-
-  const onStartDateConfirm = (date: Date) => {
-    state.form.startDate = formatDate(date, 'YYYY-mm-dd')
-    showStartDatePicker.value = false
+  if (!safePromise.value) {
+    showNotify({
+      type: 'warning',
+      message: '请阅读并勾选安全承诺!',
+    })
+    return
   }
 
-  const onComeTimeConfirm = (date: Date) => {
-    state.form.comeTime = formatDate(date, 'YYYY-mm-dd')
-    showComeTimePicker.value = false
+  if (!state.form.maleNumber && !state.form.famaleNumber) {
+    showNotify({
+      type: 'warning',
+      message: '请输入雄性或雌性数量!',
+    })
+    return
   }
 
-  // 提交
-  const onSubmit = async () => {
-    try {
-      await expertDialogFormRef.value?.validate()
-    } catch (error: any) {
-      // 显示表单验证错误
-      let errorMessage = '请完善必填信息'
-      if (error) {
-        // Vant 表单验证错误可能是数组格式
-        if (Array.isArray(error) && error.length > 0) {
-          const firstError = error[0]
-          if (firstError && firstError.message) {
-            errorMessage = firstError.message
-          }
-        } else if (error.message) {
-          errorMessage = error.message
-        }
-      }
+  // 验证自行购买时的必填项
+  if (state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF) {
+    if (!state.form.licenseNumberFile.length) {
       showNotify({
         type: 'warning',
-        message: errorMessage,
+        message: '请上传生产许可证副本!',
       })
       return
     }
-
-      if (!safePromise.value) {
+    if (!state.form.animalTestDateFile.length) {
       showNotify({
         type: 'warning',
-        message: '请阅读并勾选安全承诺!',
-      })
-        return
-      }
-
-      if (!state.form.maleNumber && !state.form.famaleNumber) {
-      showNotify({
-        type: 'warning',
-        message: '请输入雄性或雌性数量!',
+        message: '请上传近三个月动物质量检测证明!',
       })
       return
     }
+  }
 
-    // 验证自行购买时的必填项
-    if (state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF) {
-      if (!state.form.licenseNumberFile.length) {
-        showNotify({
-          type: 'warning',
-          message: '请上传生产许可证副本!',
-        })
-        return
-      }
-      if (!state.form.animalTestDateFile.length) {
-        showNotify({
-          type: 'warning',
-          message: '请上传近三个月动物质量检测证明!',
-        })
-        return
-      }
-    }
-
-    // 验证必填文件
-    if (!state.form.ethicsCheckFile.length) {
-      showNotify({
-        type: 'warning',
-        message: '请上传实验动物福利伦理审查申请表!',
-      })
-      return
-    }
-    if (!state.form.ethicsAdviceFile.length) {
-      showNotify({
-        type: 'warning',
-        message: '请上传实验动物福利伦理审查意见表!',
-      })
-        return
-      }
-
-      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,
-        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),
-        animalTestDateFile: JSON.stringify(state.form.animalTestDateFile),
-        geneIdentificationFile: JSON.stringify(state.form.geneIdentificationFile),
-        ethicsCheckFile: JSON.stringify(state.form.ethicsCheckFile),
-        ethicsAdviceFile: JSON.stringify(state.form.ethicsAdviceFile),
-      maleNumber: Number(state.form.maleNumber) || 0,
-      famaleNumber: Number(state.form.famaleNumber) || 0,
-      number: Number(state.form.number) || 1,
-      age: Number(state.form.age) || 0,
-      weight: state.form.weight ? parseFloat(String(state.form.weight)) : 0,
-      feedingDay: Number(state.form.feedingDay) || 0,
-      totalNumber: Number(state.form.totalNumber) || 0,
-      }
-
-      Object.entries(params).forEach(([key, value]) => {
-        if (value === '' || value === null) {
-          delete params[key as keyof typeof params]
-        }
-      })
-
-      const post = platAnimalCageApplicationApi.create
-      const [err]: ToResponse = await to(post(params))
-      if (err) return
-    showToast({
-      type: 'success',
-      message: '操作成功',
+  // 验证必填文件
+  if (!state.form.ethicsCheckFile.length) {
+    showNotify({
+      type: 'warning',
+      message: '请上传实验动物福利伦理审查申请表!',
+    })
+    return
+  }
+  if (!state.form.ethicsAdviceFile.length) {
+    showNotify({
+      type: 'warning',
+      message: '请上传实验动物福利伦理审查意见表!',
     })
-      closeDialog()
-      emit('refresh')
+    return
   }
 
-  watch(
-    () => [state.form.maleNumber, state.form.famaleNumber],
-    ([maleNumber, famaleNumber]) => {
-      state.form.totalNumber = (Number(maleNumber) || 0) + (Number(famaleNumber) || 0)
-    },
-    { immediate: true },
-  )
-
-  // 暴露变量
-  defineExpose({
-    openDialog,
+  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,
+    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),
+    animalTestDateFile: JSON.stringify(state.form.animalTestDateFile),
+    geneIdentificationFile: JSON.stringify(state.form.geneIdentificationFile),
+    ethicsCheckFile: JSON.stringify(state.form.ethicsCheckFile),
+    ethicsAdviceFile: JSON.stringify(state.form.ethicsAdviceFile),
+    maleNumber: Number(state.form.maleNumber) || 0,
+    famaleNumber: Number(state.form.famaleNumber) || 0,
+    number: Number(state.form.number) || 1,
+    age: Number(state.form.age) || 0,
+    weight: state.form.weight ? parseFloat(String(state.form.weight)) : 0,
+    feedingDay: Number(state.form.feedingDay) || 0,
+    totalNumber: Number(state.form.totalNumber) || 0,
+  }
+
+  Object.entries(params).forEach(([key, value]) => {
+    if (value === '' || value === null) {
+      delete params[key as keyof typeof params]
+    }
   })
+
+  const post = platAnimalCageApplicationApi.create
+  const [err]: ToResponse = await to(post(params))
+  if (err) return
+  showToast({
+    type: 'success',
+    message: '操作成功',
+  })
+  closeDialog()
+  emit('refresh')
+}
+
+watch(
+  () => [state.form.maleNumber, state.form.famaleNumber],
+  ([maleNumber, famaleNumber]) => {
+    state.form.totalNumber = (Number(maleNumber) || 0) + (Number(famaleNumber) || 0)
+  },
+  { immediate: true },
+)
+
+// 暴露变量
+defineExpose({
+  openDialog,
+})
 </script>
 <style lang="scss" scoped>
-  .application-dialog-container {
-    .popup-wrapper {
-      display: flex;
-      flex-direction: column;
-      height: 100%;
-      overflow: hidden;
-    }
+.application-dialog-container {
+  .popup-wrapper {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    overflow: hidden;
+  }
 
-    .popup-title {
-      font-size: 18px;
-      font-weight: 600;
-      text-align: center;
-      padding: 16px 0;
-      margin: 0;
-      border-bottom: 1px solid #ebedf0;
-      flex-shrink: 0;
-      background-color: #fff;
-      position: sticky;
-      top: 0;
-      z-index: 1;
-      padding-right: 40px;
-    }
+  .popup-title {
+    font-size: 18px;
+    font-weight: 600;
+    text-align: center;
+    padding: 16px 0;
+    margin: 0;
+    border-bottom: 1px solid #ebedf0;
+    flex-shrink: 0;
+    background-color: #fff;
+    position: sticky;
+    top: 0;
+    z-index: 1;
+    padding-right: 40px;
+  }
 
-    .popup-content {
-      flex: 1;
-      padding: 16px;
-      overflow-y: auto;
-      -webkit-overflow-scrolling: touch;
-    }
+  .popup-content {
+    flex: 1;
+    padding: 16px;
+    overflow-y: auto;
+    -webkit-overflow-scrolling: touch;
+  }
 
-    h4 {
-      font-size: 16px;
-      font-weight: 600;
-      margin: 16px 0 8px;
-      padding-left: 8px;
-      position: relative;
-
-      &::before {
-        content: '';
-        position: absolute;
-        left: 0;
-        top: 50%;
-        transform: translateY(-50%);
-        width: 3px;
-        height: 16px;
-        background-color: #1c9bfd;
-      }
+  h4 {
+    font-size: 16px;
+    font-weight: 600;
+    margin: 16px 0 8px;
+    padding-left: 8px;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 50%;
+      transform: translateY(-50%);
+      width: 3px;
+      height: 16px;
+      background-color: #1c9bfd;
     }
+  }
 
-    .checkbox-wrapper {
-      padding: 16px;
-      background-color: #f7f8fa;
-      border-radius: 8px;
-      margin: 16px 0;
+  .checkbox-wrapper {
+    padding: 16px;
+    background-color: #f7f8fa;
+    border-radius: 8px;
+    margin: 16px 0;
 
     .safePromise {
       white-space: pre-wrap;
-        line-height: 1.6;
-      }
+      line-height: 1.6;
     }
-
-    .dialog-footer {
-      padding: 16px;
-      padding-bottom: calc(16px + env(safe-area-inset-bottom));
-      flex-shrink: 0;
-      background-color: #fff;
-      border-top: 1px solid #ebedf0;
-      position: sticky;
-      bottom: 0;
-      z-index: 10;
-    }
-
-    .upload-tip {
-      font-size: 12px;
-      color: #969799;
-      margin-top: 8px;
-    }
-  }
-
-  :deep(.van-checkbox) {
-    white-space: pre-wrap;
-    line-height: 1.6;
   }
 
-  :deep(.van-field__label) {
-    width: 120px;
+  .dialog-footer {
+    padding: 16px;
+    padding-bottom: calc(16px + env(safe-area-inset-bottom));
+    flex-shrink: 0;
+    background-color: #fff;
+    border-top: 1px solid #ebedf0;
+    position: sticky;
+    bottom: 0;
+    z-index: 10;
   }
 
-  :deep(.van-popup__close-icon) {
-    z-index: 100;
-    top: 16px;
-    right: 16px;
+  .upload-tip {
+    font-size: 12px;
+    color: #969799;
+    margin-top: 8px;
   }
+}
+
+:deep(.van-checkbox) {
+  white-space: pre-wrap;
+  line-height: 1.6;
+}
+
+:deep(.van-field__label) {
+  width: 120px;
+}
+
+:deep(.van-popup__close-icon) {
+  z-index: 100;
+  top: 16px;
+  right: 16px;
+}
 </style>

+ 353 - 477
src/view/animal/applicationRemoval/components/addEdit.vue

@@ -1,132 +1,44 @@
 <template>
   <div class="application-dialog-container">
-    <van-popup
-      v-model:show="state.dialog.isShowDialog"
-      position="bottom"
-      :style="{ height: '90vh' }"
-      round
-      :closeable="true"
-      @close="onCancel"
-      :close-on-click-overlay="false"
-    >
+    <van-popup v-model:show="state.dialog.isShowDialog" position="bottom" :style="{ height: '90vh' }" round
+      :closeable="true" @close="onCancel" :close-on-click-overlay="false">
       <div class="popup-wrapper">
         <h3 class="popup-title">{{ state.dialog.title }}</h3>
         <div class="popup-content">
-          <van-form
-            ref="expertDialogFormRef"
-            @submit="onSubmit"
-          >
+          <van-form ref="expertDialogFormRef" @submit="onSubmit">
             <h4 class="mb20 mt8">申请人信息</h4>
             <van-cell-group>
-              <van-field
-                v-model="state.form.userName"
-                label="申请人姓名"
-                placeholder="申请人姓名"
-                readonly
-                required
-                :rules="rules.userName"
-              />
-              <van-field
-                v-model="state.form.phone"
-                label="联系电话"
-                placeholder="联系电话"
-                readonly
-              />
-              <van-field
-                v-model="state.form.projectGroupName"
-                label="课题组"
-                placeholder="请选择"
-                readonly
-                required
-                :rules="rules.projectGroupId"
-              />
-              <van-field
-                v-model="state.form.deptName"
-                label="科室"
-                placeholder="科室"
-                readonly
-              />
+              <van-field v-model="state.form.userName" label="申请人姓名" placeholder="申请人姓名" readonly required
+                :rules="rules.userName" />
+              <van-field v-model="state.form.phone" label="联系电话" placeholder="联系电话" readonly />
+              <van-field v-model="state.form.projectGroupName" label="课题组" placeholder="请选择" readonly required
+                :rules="rules.projectGroupId" />
+              <van-field v-model="state.form.deptName" label="科室" placeholder="科室" readonly />
             </van-cell-group>
 
             <h4 class="mb20 mt20">转出情况</h4>
             <van-cell-group>
-              <van-field
-                v-model="state.form.categoryName"
-                label="动物品系"
-                placeholder="请选择"
-                readonly
-                is-link
-                required
-                @click="showCategoryPicker = true"
-                :rules="rules.categoryId"
-              />
-              <van-field
-                v-model="state.form.variety"
-                label="品种品系"
-                placeholder="请输入品种品系"
-              />
-              <van-field
-                v-model="state.form.takeawayDate"
-                label="转出日期"
-                placeholder="请选择时间"
-                readonly
-                is-link
-                required
-                @click="showDatePicker = true"
-                :rules="rules.takeawayDate"
-              />
-              <van-field
-                v-model="state.form.takeawayAddress"
-                label="送往地点"
-                placeholder="请输入"
-                required
-                :rules="rules.takeawayAddress"
-              />
-              <van-field
-                v-model="state.form.takeawayReason"
-                label="带出原因"
-                placeholder="请输入"
-                required
-                :rules="rules.takeawayReason"
-              />
-              <van-field
-                v-model="state.form.takeawayTransport"
-                label="运输方式"
-                placeholder="请输入"
-                required
-                :rules="rules.takeawayTransport"
-              />
-              <van-field
-                v-model="state.form.accessCardNumber"
-                label="门禁卡序列号"
-                placeholder="请输入"
-              />
-              <van-field
-                v-model="state.form.takeawayMaleNumber"
-                label="雄性"
-                placeholder="雄性数量"
-                type="digit"
-              >
+              <van-field v-model="state.form.categoryName" label="动物类别" placeholder="请选择" readonly is-link required
+                @click="showCategoryPicker = true" :rules="rules.categoryId" />
+              <van-field v-model="state.form.variety" label="品种品系" placeholder="请输入品种品系" required
+                :rules="rules.variety" />
+              <van-field v-model="state.form.takeawayDate" label="转出日期" placeholder="请选择时间" readonly is-link required
+                @click="showDatePicker = true" :rules="rules.takeawayDate" />
+              <van-field v-model="state.form.takeawayAddress" label="送往地点" placeholder="请输入" required
+                :rules="rules.takeawayAddress" />
+              <van-field v-model="state.form.takeawayReason" label="带出原因" placeholder="请输入" required
+                :rules="rules.takeawayReason" />
+              <van-field v-model="state.form.takeawayTransport" label="运输方式" placeholder="请输入" required
+                :rules="rules.takeawayTransport" />
+              <van-field v-model="state.form.accessCardNumber" label="门禁卡序列号" placeholder="请输入" />
+              <van-field v-model="state.form.takeawayMaleNumber" label="雄性" placeholder="雄性数量" type="digit">
                 <template #button>
-                  <van-stepper
-                    v-model="state.form.takeawayMaleNumber"
-                    :min="0"
-                    integer
-                  />
+                  <van-stepper v-model="state.form.takeawayMaleNumber" :min="0" integer />
                 </template>
               </van-field>
-              <van-field
-                v-model="state.form.takewayFemaleNumber"
-                label="雌性"
-                placeholder="雌性数量"
-                type="digit"
-              >
+              <van-field v-model="state.form.takewayFemaleNumber" label="雌性" placeholder="雌性数量" type="digit">
                 <template #button>
-                  <van-stepper
-                    v-model="state.form.takewayFemaleNumber"
-                    :min="0"
-                    integer
-                  />
+                  <van-stepper v-model="state.form.takewayFemaleNumber" :min="0" integer />
                 </template>
               </van-field>
             </van-cell-group>
@@ -134,10 +46,7 @@
             <div class="mt30 mb30 checkbox-wrapper">
               <van-checkbox v-model="safePromiseStatus">
                 本人已阅读并同意
-                <span
-                  class="link-text"
-                  @click.stop="handleReadNotice"
-                >
+                <span class="link-text" @click.stop="handleReadNotice">
                   《实验动物带离须知》
                 </span>
                 内容
@@ -146,71 +55,37 @@
           </van-form>
         </div>
         <div class="dialog-footer">
-          <van-button
-            v-if="state.dialog.type !== 'detail'"
-            type="primary"
-            @click="onSubmit"
-            block
-            native-type="submit"
-          >
+          <van-button v-if="state.dialog.type !== 'detail'" type="primary" @click="onSubmit" block native-type="submit">
             提交
           </van-button>
         </div>
       </div>
     </van-popup>
 
-    <!-- 动物品系选择器 -->
-    <van-popup
-      v-model:show="showCategoryPicker"
-      position="bottom"
-    >
-      <van-picker
-        :columns="animalTypeList"
-        :columns-field-names="{ text: 'name', value: 'id' }"
-        @confirm="onCategoryConfirm"
-        @cancel="showCategoryPicker = false"
-      />
+    <!-- 动物类别选择器 -->
+    <van-popup v-model:show="showCategoryPicker" position="bottom">
+      <van-picker :columns="animalTypeList" :columns-field-names="{ text: 'name', value: 'id' }"
+        @confirm="onCategoryConfirm" @cancel="showCategoryPicker = false" />
     </van-popup>
 
     <!-- 日期选择器 - 使用日历组件 -->
-    <van-popup
-      v-model:show="showDatePicker"
-      position="bottom"
-      :style="{ height: '80vh' }"
-      round
-    >
-      <van-calendar
-        v-model:show="showDatePicker"
-        @confirm="onDateConfirm"
-        :min-date="new Date()"
-      />
+    <van-popup v-model:show="showDatePicker" position="bottom" :style="{ height: '80vh' }" round>
+      <van-calendar v-model:show="showDatePicker" @confirm="onDateConfirm" :min-date="new Date()" />
     </van-popup>
 
     <!-- 须知弹窗 -->
-    <van-popup
-      v-model:show="isShowNotice"
-      position="bottom"
-      round
-      :closeable="true"
-      :style="{ height: '70vh' }"
-    >
+    <van-popup v-model:show="isShowNotice" position="bottom" round :closeable="true" :style="{ height: '70vh' }">
       <div class="notice-content">
         <h4 class="notice-title">实验动物带离须知</h4>
         <div class="text">
-          <p
-            class="mb20"
+          <p class="mb20"
             v-for="(item, index) in AnimalRemovalApplicationNotice.split('\n').filter((item) => item.trim())"
-            :key="index"
-          >
+            :key="index">
             {{ item }}
           </p>
         </div>
         <div class="notice-footer">
-          <van-button
-            type="primary"
-            @click="isShowNotice = false"
-            block
-          >
+          <van-button type="primary" @click="isShowNotice = false" block>
             确定
           </van-button>
         </div>
@@ -220,47 +95,142 @@
 </template>
 
 <script setup lang="ts">
-  import { reactive, ref, computed } from 'vue'
-  import to from 'await-to-js'
-  import { showToast, showNotify } from 'vant'
-  import type { FormInstance } from 'vant/es'
-
-  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'
-  import { formatDate } from '/@/utils/formatTime'
-
-  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 showCategoryPicker = ref<boolean>(false)
-  const showDatePicker = ref<boolean>(false)
-  const selectedDate = ref<Date | null>(null)
-
-  const rules = {
-    categoryId: [{ required: true, message: '动物品系不能为空' }],
-    projectGroupId: [{ required: true, message: '课题组不能为空' }],
-    takeawayDate: [{ required: true, message: '转出日期不能为空' }],
-    userName: [{ required: true, message: '申请人姓名不能为空' }],
-    takeawayAddress: [{ required: true, message: '送往地点不能为空' }],
-    takeawayReason: [{ required: true, message: '带出原因不能为空' }],
-    takeawayTransport: [{ required: true, message: '运输方式不能为空' }],
+import { reactive, ref, computed } from 'vue'
+import to from 'await-to-js'
+import { showToast, showNotify } from 'vant'
+import type { FormInstance } from 'vant/es'
+
+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'
+import { formatDate } from '/@/utils/formatTime'
+
+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 showCategoryPicker = ref<boolean>(false)
+const showDatePicker = ref<boolean>(false)
+const selectedDate = ref<Date | null>(null)
+
+const rules = {
+  categoryId: [{ required: true, message: '动物类别不能为空' }],
+  variety: [{ required: true, message: '品种品系不能为空' }],
+  projectGroupId: [{ required: true, message: '课题组不能为空' }],
+  takeawayDate: [{ required: true, message: '转出日期不能为空' }],
+  userName: [{ required: true, message: '申请人姓名不能为空' }],
+  takeawayAddress: [{ required: true, message: '送往地点不能为空' }],
+  takeawayReason: [{ required: true, message: '带出原因不能为空' }],
+  takeawayTransport: [{ required: true, message: '运输方式不能为空' }],
+}
+
+const defaultFormFields: CreateAnimalApplyLeavePayload & { categoryName?: string } = {
+  accessCardNumber: '',
+  categoryId: null,
+  categoryName: '',
+  variety: '',
+  projectGroupId: null,
+  projectGroupName: '',
+  takeawayDate: '',
+  takeawayAddress: '',
+  takeawayReason: '',
+  takeawayTransport: '',
+  takewayFemaleNumber: 0,
+  takeawayMaleNumber: 0,
+  userName: '',
+  phone: '',
+  deptId: '',
+  deptName: '',
+}
+
+const state = reactive<{
+  form: CreateAnimalApplyLeavePayload & { categoryName?: string }
+  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 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,
+    }
+    // 设置 categoryName 用于显示
+    if (sourceData.categoryId) {
+      const category = animalTypeList.value.find((item) => item.id === sourceData.categoryId)
+      if (category) {
+        state.form.categoryName = category.name
+      }
+    }
+    // 设置日期选择器的值
+    if (sourceData.takeawayDate) {
+      const dateParts = sourceData.takeawayDate.split('-')
+      selectedDate.value = new Date(parseInt(dateParts[0]), parseInt(dateParts[1]) - 1, parseInt(dateParts[2]))
+    }
+  }
+
+  if (type === 'add') {
+    state.dialog.title = '新增实验动物带离单'
+    if (animalTypeList.value.length > 0) {
+      state.form.categoryId = animalTypeList.value[0].id
+      state.form.categoryName = animalTypeList.value[0].name
+    }
+  }
+  if (type === 'edit') {
+    state.dialog.title = '编辑实验动物带离单'
+  }
+  if (type === 'detail') {
+    state.dialog.title = '查看实验动物带离单'
+    safePromiseStatus.value = true
+  }
+
+  state.dialog.isShowDialog = true
+}
 
-  const defaultFormFields: CreateAnimalApplyLeavePayload & { categoryName?: string } = {
+const closeDialog = () => {
+  expertDialogFormRef.value?.resetValidation()
+  // 重置表单数据
+  state.form = {
     accessCardNumber: '',
     categoryId: null,
     categoryName: '',
@@ -278,316 +248,222 @@
     deptId: '',
     deptName: '',
   }
-
-  const state = reactive<{
-    form: CreateAnimalApplyLeavePayload & { categoryName?: string }
-    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 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,
-      }
-      // 设置 categoryName 用于显示
-      if (sourceData.categoryId) {
-        const category = animalTypeList.value.find((item) => item.id === sourceData.categoryId)
-        if (category) {
-          state.form.categoryName = category.name
+  safePromiseStatus.value = false
+  selectedDate.value = null
+  state.dialog.isShowDialog = false
+}
+
+const onCancel = () => {
+  closeDialog()
+}
+
+const onSubmit = async () => {
+  try {
+    await expertDialogFormRef.value?.validate()
+  } catch (error: any) {
+    // 显示表单验证错误
+    let errorMessage = '请完善必填信息'
+    if (error) {
+      // Vant 表单验证错误可能是数组格式
+      if (Array.isArray(error) && error.length > 0) {
+        const firstError = error[0]
+        if (firstError && firstError.message) {
+          errorMessage = firstError.message
         }
-      }
-      // 设置日期选择器的值
-      if (sourceData.takeawayDate) {
-        const dateParts = sourceData.takeawayDate.split('-')
-        selectedDate.value = new Date(parseInt(dateParts[0]), parseInt(dateParts[1]) - 1, parseInt(dateParts[2]))
-      }
-    }
-
-    if (type === 'add') {
-      state.dialog.title = '新增实验动物带离单'
-      if (animalTypeList.value.length > 0) {
-        state.form.categoryId = animalTypeList.value[0].id
-        state.form.categoryName = animalTypeList.value[0].name
+      } else if (error.message) {
+        errorMessage = error.message
       }
     }
-    if (type === 'edit') {
-      state.dialog.title = '编辑实验动物带离单'
-    }
-    if (type === 'detail') {
-      state.dialog.title = '查看实验动物带离单'
-      safePromiseStatus.value = true
-    }
-
-    state.dialog.isShowDialog = true
+    showNotify({
+      type: 'warning',
+      message: errorMessage,
+    })
+    return
   }
 
-  const closeDialog = () => {
-    expertDialogFormRef.value?.resetValidation()
-    // 重置表单数据
-    state.form = {
-      accessCardNumber: '',
-      categoryId: null,
-      categoryName: '',
-      variety: '',
-      projectGroupId: null,
-      projectGroupName: '',
-      takeawayDate: '',
-      takeawayAddress: '',
-      takeawayReason: '',
-      takeawayTransport: '',
-      takewayFemaleNumber: 0,
-      takeawayMaleNumber: 0,
-      userName: '',
-      phone: '',
-      deptId: '',
-      deptName: '',
-    }
-    safePromiseStatus.value = false
-    selectedDate.value = null
-    state.dialog.isShowDialog = false
+  if (!safePromiseStatus.value) {
+    showNotify({
+      type: 'warning',
+      message: '请阅读并勾选安全承诺!',
+    })
+    return
   }
 
-  const onCancel = () => {
-    closeDialog()
+  if (!state.form.takeawayMaleNumber && !state.form.takewayFemaleNumber) {
+    showNotify({
+      type: 'warning',
+      message: '请添加雄性或雌性数量!',
+    })
+    return
   }
 
-  const onSubmit = async () => {
-    try {
-      await expertDialogFormRef.value?.validate()
-    } catch (error: any) {
-      // 显示表单验证错误
-      let errorMessage = '请完善必填信息'
-      if (error) {
-        // Vant 表单验证错误可能是数组格式
-        if (Array.isArray(error) && error.length > 0) {
-          const firstError = error[0]
-          if (firstError && firstError.message) {
-            errorMessage = firstError.message
-          }
-        } else if (error.message) {
-          errorMessage = error.message
-        }
-      }
-      showNotify({
-        type: 'warning',
-        message: errorMessage,
-      })
-      return
-    }
-
-    if (!safePromiseStatus.value) {
-      showNotify({
-        type: 'warning',
-        message: '请阅读并勾选安全承诺!',
-      })
-      return
-    }
+  const post = platAnimalCageApplicationApi.createAnimalTakeawayApplications
+  const [err]: ToResponse = await to(
+    post(
+      filterFields({
+        ...deepClone(state.form),
+        deptId: userInfos.value.deptId,
+        projectGroupId: state.form.projectGroupId?.toString(),
+        takeawayMaleNumber: Number(state.form.takeawayMaleNumber) || 0,
+        takewayFemaleNumber: Number(state.form.takewayFemaleNumber) || 0,
+      }),
+    ),
+  )
+
+  if (err) return
+  showToast({
+    type: 'success',
+    message: '操作成功',
+  })
+  closeDialog()
+  emit('refresh')
+}
+
+const onCategoryConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
+  if (selectedOptions.length > 0) {
+    const selected = selectedOptions[0]
+    state.form.categoryId = selected.id
+    state.form.categoryName = selected.name
+  }
+  showCategoryPicker.value = false
+}
+
+const onDateConfirm = (date: Date) => {
+  selectedDate.value = date
+  state.form.takeawayDate = formatDate(date, 'YYYY-mm-dd')
+  showDatePicker.value = false
+}
+
+const handleReadNotice = () => {
+  isShowNotice.value = true
+}
+
+defineExpose({
+  openDialog,
+})
+</script>
+<style lang="scss" scoped>
+.application-dialog-container {
+  .popup-wrapper {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    overflow: hidden;
+  }
 
-    if (!state.form.takeawayMaleNumber && !state.form.takewayFemaleNumber) {
-      showNotify({
-        type: 'warning',
-        message: '请添加雄性或雌性数量!',
-      })
-      return
-    }
+  .popup-title {
+    font-size: 18px;
+    font-weight: 600;
+    text-align: center;
+    padding: 16px 0;
+    margin: 0;
+    border-bottom: 1px solid #ebedf0;
+    flex-shrink: 0;
+    background-color: #fff;
+    position: sticky;
+    top: 0;
+    z-index: 1;
+    padding-right: 40px; // 为关闭按钮留出空间
+  }
 
-    const post = platAnimalCageApplicationApi.createAnimalTakeawayApplications
-    const [err]: ToResponse = await to(
-      post(
-        filterFields({
-          ...deepClone(state.form),
-          deptId: userInfos.value.deptId,
-          projectGroupId: state.form.projectGroupId?.toString(),
-          takeawayMaleNumber: Number(state.form.takeawayMaleNumber) || 0,
-          takewayFemaleNumber: Number(state.form.takewayFemaleNumber) || 0,
-        }),
-      ),
-    )
-
-    if (err) return
-    showToast({
-      type: 'success',
-      message: '操作成功',
-    })
-    closeDialog()
-    emit('refresh')
+  .popup-content {
+    flex: 1;
+    padding: 16px;
+    overflow-y: auto;
+    -webkit-overflow-scrolling: touch;
   }
 
-  const onCategoryConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
-    if (selectedOptions.length > 0) {
-      const selected = selectedOptions[0]
-      state.form.categoryId = selected.id
-      state.form.categoryName = selected.name
+  h4 {
+    font-size: 16px;
+    font-weight: 600;
+    margin: 16px 0 8px;
+    padding-left: 8px;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 50%;
+      transform: translateY(-50%);
+      width: 3px;
+      height: 16px;
+      background-color: #1c9bfd;
     }
-    showCategoryPicker.value = false
   }
 
-  const onDateConfirm = (date: Date) => {
-    selectedDate.value = date
-    state.form.takeawayDate = formatDate(date, 'YYYY-mm-dd')
-    showDatePicker.value = false
+  .checkbox-wrapper {
+    padding: 16px;
+    background-color: #f7f8fa;
+    border-radius: 8px;
+    margin: 16px 0;
+
+    .link-text {
+      color: #1989fa;
+      text-decoration: underline;
+      cursor: pointer;
+    }
   }
 
-  const handleReadNotice = () => {
-    isShowNotice.value = true
+  .dialog-footer {
+    padding: 16px;
+    padding-bottom: calc(16px + env(safe-area-inset-bottom));
+    flex-shrink: 0;
+    background-color: #fff;
+    border-top: 1px solid #ebedf0;
+    position: sticky;
+    bottom: 0;
+    z-index: 10;
   }
 
-  defineExpose({
-    openDialog,
-  })
-</script>
-<style lang="scss" scoped>
-  .application-dialog-container {
-    .popup-wrapper {
-      display: flex;
-      flex-direction: column;
-      height: 100%;
-      overflow: hidden;
-    }
+  .notice-content {
+    padding: 16px;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
 
-    .popup-title {
+    .notice-title {
       font-size: 18px;
       font-weight: 600;
       text-align: center;
       padding: 16px 0;
-      margin: 0;
+      margin: 0 0 16px;
       border-bottom: 1px solid #ebedf0;
-      flex-shrink: 0;
-      background-color: #fff;
-      position: sticky;
-      top: 0;
-      z-index: 1;
-      padding-right: 40px; // 为关闭按钮留出空间
     }
 
-    .popup-content {
+    .text {
       flex: 1;
-      padding: 16px;
       overflow-y: auto;
-      -webkit-overflow-scrolling: touch;
-    }
+      padding: 0 8px;
 
-    h4 {
-      font-size: 16px;
-      font-weight: 600;
-      margin: 16px 0 8px;
-      padding-left: 8px;
-      position: relative;
-
-      &::before {
-        content: '';
-        position: absolute;
-        left: 0;
-        top: 50%;
-        transform: translateY(-50%);
-        width: 3px;
-        height: 16px;
-        background-color: #1c9bfd;
+      p {
+        text-indent: 2em;
+        line-height: 1.6;
+        margin-bottom: 12px;
       }
     }
 
-    .checkbox-wrapper {
-      padding: 16px;
-      background-color: #f7f8fa;
-      border-radius: 8px;
-      margin: 16px 0;
-
-      .link-text {
-        color: #1989fa;
-        text-decoration: underline;
-        cursor: pointer;
-      }
-    }
-
-    .dialog-footer {
-      padding: 16px;
-      padding-bottom: calc(16px + env(safe-area-inset-bottom));
-      flex-shrink: 0;
-      background-color: #fff;
+    .notice-footer {
+      padding-top: 16px;
       border-top: 1px solid #ebedf0;
-      position: sticky;
-      bottom: 0;
-      z-index: 10;
+      margin-top: 16px;
     }
-
-    .notice-content {
-      padding: 16px;
-      height: 100%;
-      display: flex;
-      flex-direction: column;
-
-      .notice-title {
-        font-size: 18px;
-        font-weight: 600;
-        text-align: center;
-        padding: 16px 0;
-        margin: 0 0 16px;
-        border-bottom: 1px solid #ebedf0;
-      }
-
-      .text {
-        flex: 1;
-        overflow-y: auto;
-        padding: 0 8px;
-
-        p {
-          text-indent: 2em;
-          line-height: 1.6;
-          margin-bottom: 12px;
-        }
-      }
-
-      .notice-footer {
-        padding-top: 16px;
-        border-top: 1px solid #ebedf0;
-        margin-top: 16px;
-      }
-    }
-  }
-
-  :deep(.van-checkbox) {
-    white-space: pre-wrap;
-    line-height: 1.6;
-  }
-
-  :deep(.van-field__label) {
-    width: 120px;
-  }
-
-  // 确保关闭按钮显示在标题上方
-  :deep(.van-popup__close-icon) {
-    z-index: 100;
-    top: 16px;
-    right: 16px;
   }
+}
+
+:deep(.van-checkbox) {
+  white-space: pre-wrap;
+  line-height: 1.6;
+}
+
+:deep(.van-field__label) {
+  width: 120px;
+}
+
+// 确保关闭按钮显示在标题上方
+:deep(.van-popup__close-icon) {
+  z-index: 100;
+  top: 16px;
+  right: 16px;
+}
 </style>

+ 1 - 1
src/view/animal/applicationRemoval/index.vue

@@ -61,7 +61,7 @@
                 </span>
               </p>
               <p class="inst-title">
-                <span>动物品系</span>
+                <span>动物类别</span>
                 <span class="title ml8">
                   {{animalTypeList.find((type) => type.id === item.categoryId)?.name}}
                 </span>

+ 1 - 0
src/view/entry/components/allocate.vue

@@ -413,6 +413,7 @@
   const closeDialog = () => {
     resetDialogState()
     state.dialog.isShowDialog = false
+    emit('refresh')
   }
   // 取消
   const onCancel = () => {

+ 5 - 5
src/view/entry/manage.vue

@@ -279,19 +279,19 @@
     </div>
     <UploadDialog
       ref="uploadRef"
-      @refresh="onLoad"
+      @refresh="search"
     />
     <DetailsDialog
       ref="detailsDialog"
-      @refresh="onLoad"
+      @refresh="search"
     />
     <AllocateDialog
       ref="allocateRef"
-      @refresh="onLoad"
+      @refresh="search"
     />
     <ConfirmDialog
       ref="confirmRef"
-      @refresh="onLoad"
+      @refresh="search"
     />
   </div>
 </template>
@@ -417,7 +417,7 @@
     state.queryParams = {
       ...state.queryParams,
       pageNum: 1,
-      pageSize: 20,
+      pageSize: 10,
     }
     state.list = []
     state.finished = false

+ 177 - 139
src/view/instr/appointList/inProgress/index.vue

@@ -9,7 +9,8 @@
 <template>
   <div class="panel-wrap">
     <van-empty v-if="appointList.length == 0" mode="list" description="暂无正在上机"></van-empty>
-    <van-list v-else v-model:loading="listloading" class="data-list" :finished="finished" finished-text="没有更多了" @load="onLoad">
+    <van-list v-else v-model:loading="listloading" class="data-list" :finished="finished" finished-text="没有更多了"
+      @load="onLoad">
       <div class="inst-item mb40" v-for="(v, index) in appointList" :key="index" @click="toDetail(v)">
         <div class="flex flex-between mb20">
           <div>
@@ -59,13 +60,8 @@
           </div>
         </div>
         <div class="flex mt20">
-          <van-button
-            style="width: 70px; height: 30px; margin: 0; font-size: 14px"
-            class="scan-txt"
-            type="danger"
-            size="small"
-            @click.native.stop="handleIsGetOff(v)"
-          >
+          <van-button style="width: 70px; height: 30px; margin: 0; font-size: 14px" class="scan-txt" type="danger"
+            size="small" @click.native.stop="handleIsGetOff(v)">
             下机
           </van-button>
         </div>
@@ -76,106 +72,90 @@
 </template>
 
 <script>
-  // import BlueTooth from '../../../components/BlueTooth'
-  import { useMyAppointApi } from '/@/api/appoint'
-  const myAppointApi = useMyAppointApi()
-  import { useInstrApi } from '/@/api/instr'
-  const instApi = useInstrApi()
-  import instAppointApi from '/@/api/instr/instAppoint'
-  import to from 'await-to-js'
-  import { showConfirmDialog, showNotify } from 'vant'
-  export default {
-    // components: { BlueTooth },
-    data() {
-      return {
-        appointList: [],
-        queryForm: {
-          pageNum: 1,
-          pageSize: 10
-        },
-        curAppointInfo: 0,
-        total: 0,
-        listloading: false,
-        finished: false
-      }
-    },
-    created() {},
-    mounted() {
-      this.queryForm.pageNum = 1
+// import BlueTooth from '../../../components/BlueTooth'
+import { useMyAppointApi } from '/@/api/appoint'
+const myAppointApi = useMyAppointApi()
+import { useInstrApi } from '/@/api/instr'
+const instApi = useInstrApi()
+import instAppointApi from '/@/api/instr/instAppoint'
+import to from 'await-to-js'
+import { showConfirmDialog, showNotify } from 'vant'
+import { useUserInfo } from '/@/stores/userInfo'
+import { scanCodeWxUrl } from '/@/constants/pageConstants'
+export default {
+  // components: { BlueTooth },
+  data() {
+    return {
+      appointList: [],
+      queryForm: {
+        pageNum: 1,
+        pageSize: 10
+      },
+      curAppointInfo: 0,
+      total: 0,
+      listloading: false,
+      finished: false
+    }
+  },
+  created() { },
+  mounted() {
+    this.queryForm.pageNum = 1
+    this.getInstList()
+  },
+  methods: {
+    // 重新加载
+    onLoad() {
+      this.queryForm.pageNum++
       this.getInstList()
     },
-    methods: {
-      // 重新加载
-      onLoad() {
-        this.queryForm.pageNum++
-        this.getInstList()
-      },
-      // 查询列表
-      async getInstList() {
-        this.listloading = true
-        const [err, res] = await to(myAppointApi.inProgressList(this.queryForm))
-        this.listloading = false
-        if (err) return
-        if (res?.code === 200) {
-          this.appointList = this.queryForm.pageNum == 1 ? [...(res?.data?.list || [])] : [...this.appointList, ...(res?.data?.list || [])]
-          this.total = res?.data?.total
-          if (this.queryForm.pageNum * this.queryForm.pageSize >= res.data.total) {
-            this.finished = true
-          }
+    // 查询列表
+    async getInstList() {
+      this.listloading = true
+      const [err, res] = await to(myAppointApi.inProgressList(this.queryForm))
+      this.listloading = false
+      if (err) return
+      if (res?.code === 200) {
+        this.appointList = this.queryForm.pageNum == 1 ? [...(res?.data?.list || [])] : [...this.appointList, ...(res?.data?.list || [])]
+        this.total = res?.data?.total
+        if (this.queryForm.pageNum * this.queryForm.pageSize >= res.data.total) {
+          this.finished = true
         }
-      },
-      async handleIsGetOff(row) {
-        const [err, res] = await to(instApi.getSettingDetail({ instId: row.instId, code: 'InstCfgUse' }))
-        if (err) return
-        // 上机时间
-        const useTime = this.calcTimeDiff(row.usedStartTime)
-        // 是否进行上机时间太短的提示
-        // 需要
-        if (res?.data?.config.shortWarningEnable) {
-          const shortWraningTime = this.getShortWraningTime(res?.data?.config.shortWarning, res?.data?.config.shortWarningUnit)
-          if (useTime > shortWraningTime) {
-            if (row.controlMode === '30') {
-              this.$refs.bluetoothRef.initBlue('close', row)
-            } else {
-              this.curAppointInfo = row
-              showConfirmDialog({
-                title: '提示',
-                message: `本次上机时长共计${useTime}分钟,确定要下机吗?`
-              })
-                .then((e) => {
-                  console.log(e)
-                  this.getOff()
-                })
-                .catch(() => {
-                  console.log('ssss')
-                })
-            }
+      }
+    },
+    async handleIsGetOff(row) {
+      const [err, res] = await to(instApi.getSettingDetail({ instId: row.instId, code: 'InstCfgUse' }))
+      if (err) return
+      // 上机时间
+      const useTime = this.calcTimeDiff(row.usedStartTime)
+      // 是否进行上机时间太短的提示
+      // 需要
+      if (res?.data?.config.shortWarningEnable) {
+        const shortWraningTime = this.getShortWraningTime(res?.data?.config.shortWarning, res?.data?.config.shortWarningUnit)
+        if (useTime > shortWraningTime) {
+          if (row.controlMode === '30') {
+            this.handleBluetoothOffLine()
           } else {
-            if (row.controlMode === '30') {
-              this.$refs.bluetoothRef.initBlue('close', row)
-            } else {
-              this.curAppointInfo = row
-              showConfirmDialog({
-                title: '提示',
-                message: `本次上机时长共计${useTime}分钟,不足${shortWraningTime}分钟,确定要下机吗?`
+            this.curAppointInfo = row
+            showConfirmDialog({
+              title: '提示',
+              message: `本次上机时长共计${useTime}分钟,确定要下机吗?`
+            })
+              .then((e) => {
+                console.log(e)
+                this.getOff()
+              })
+              .catch(() => {
+                console.log('ssss')
               })
-                .then((e) => {
-                  console.log(e)
-                  this.getOff()
-                })
-                .catch(() => {
-                  console.log('ssss')
-                })
-            }
           }
         } else {
           if (row.controlMode === '30') {
-            this.$refs.bluetoothRef.initBlue('close', row)
+            this.handleBluetoothOffLine()
           } else {
             this.curAppointInfo = row
             showConfirmDialog({
               title: '提示',
-              message: `本次上机时长共计${useTime}分钟,确定要下机吗?`
+              message: `本次上机时长共计${useTime}分钟,不足${shortWraningTime}分钟,确定要下机吗?`
             })
               .then((e) => {
                 console.log(e)
@@ -186,58 +166,116 @@
               })
           }
         }
-      },
-      // 计算时间转分钟
-      getShortWraningTime(time, unit) {
-        if (unit == 'hour') {
-          return time * 60
-        } else if (unit == 'minute') {
-          return time
-        }
-      },
-      calcTimeDiff(date) {
-        var new_date = new Date() //新建一个日期对象,默认现在的时间
-        var old_date = new Date(date) //设置过去的一个时间点,"yyyy-MM-dd HH:mm:ss"格式化日期
-        var difftime = new_date - old_date //计算时间差
-        return Math.round(difftime / 60000)
-      },
-      async getOff() {
-        const params = { appointId: this.curAppointInfo.id }
-        const [err, res] = await to(instAppointApi.getOff({ ...params }))
-        if (err) return
-        if (res.code == 200) {
-          this.queryForm.pageNum = 1
-          this.getInstList()
-          showNotify({ type: 'success', message: '下机成功' })
+      } else {
+        if (row.controlMode === '30') {
+          this.handleBluetoothOffLine()
+        } else {
+          this.curAppointInfo = row
+          showConfirmDialog({
+            title: '提示',
+            message: `本次上机时长共计${useTime}分钟,确定要下机吗?`
+          })
+            .then((e) => {
+              console.log(e)
+              this.getOff()
+            })
+            .catch(() => {
+              console.log('ssss')
+            })
         }
-      },
-      callbackOff() {
+      }
+    },
+    async handleBluetoothOffLine() {
+      const res = await useUserInfo().scanCode()
+      this.handleDeCode(res, this.handleBluetoothDeCode)
+    },
+    /**
+     *  解码
+     *  code 二维码内容
+     * func 解码后的操作函数
+     * */
+    async handleDeCode(code, func) {
+      if (this.loading) return
+      this.loading = true
+      const params = { content: code }
+      const [err, res] = await to(instApi.decode({ ...params }))
+      if (err) {
+        this.loading = false
+        showNotify({ type: 'danger', message: '解码失败,请重试' })
+        return
+      }
+      if (res?.code == 200 && res?.data?.content) {
+        func(res.data.content)
+      } else {
+        this.loading = false
+        showNotify({ type: 'danger', message: res?.message || '解码失败' })
+      }
+    },
+    //蓝牙 重新解码函数,获取解码内容之后 执行跳转操作
+    async handleBluetoothDeCode(content) {
+      if (content.includes('id')) {
+        const terminal = JSON.parse(content)?.terminal
+        const url = scanCodeWxUrl(terminal)
+        window.location.href = url
+      } else {
+        const url = scanCodeWxUrl(content)
+        window.location.href = url
+      }
+    },
+    // 计算时间转分钟
+    getShortWraningTime(time, unit) {
+      if (unit == 'hour') {
+        return time * 60
+      } else if (unit == 'minute') {
+        return time
+      }
+    },
+    calcTimeDiff(date) {
+      var new_date = new Date() //新建一个日期对象,默认现在的时间
+      var old_date = new Date(date) //设置过去的一个时间点,"yyyy-MM-dd HH:mm:ss"格式化日期
+      var difftime = new_date - old_date //计算时间差
+      return Math.round(difftime / 60000)
+    },
+    async getOff() {
+      const params = { appointId: this.curAppointInfo.id }
+      const [err, res] = await to(instAppointApi.getOff({ ...params }))
+      if (err) return
+      if (res.code == 200) {
         this.queryForm.pageNum = 1
         this.getInstList()
-      },
-      toDetail(v) {
-        this.$router.push(`/onlineInfo?appointId=${v.id}`)
+        showNotify({ type: 'success', message: '下机成功' })
       }
+    },
+    callbackOff() {
+      this.queryForm.pageNum = 1
+      this.getInstList()
+    },
+    toDetail(v) {
+      this.$router.push(`/onlineInfo?appointId=${v.id}`)
     }
   }
+}
 </script>
 <style lang="scss" scoped>
-  * {
-    box-sizing: border-box;
-  }
-  .panel-wrap {
-    height: 100%;
-    .data-list {
-      .inst-item {
-        border-radius: 10px;
-        padding: 15px;
-        box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
-        margin-bottom: 20px;
-        background-color: #fff;
-        .equ-tit {
-          width: 74px;
-        }
+* {
+  box-sizing: border-box;
+}
+
+.panel-wrap {
+  height: 100%;
+
+  .data-list {
+    .inst-item {
+      border-radius: 10px;
+      padding: 15px;
+      box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
+      margin-bottom: 20px;
+      background-color: #fff;
+
+      .equ-tit {
+        width: 74px;
       }
     }
   }
+}
 </style>

+ 172 - 136
src/view/instr/appointList/onlineInfo/index.vue

@@ -5,18 +5,12 @@
       <div class="flex flex-between mb20">
         <div class="ml10"><span class="fontSize14 bold primary-color">实验员信息</span></div>
         <div class="flex">
-          <van-button
-            v-if="showOffBtn"
-            style="width: 60px; height: 30px; margin: 0 20px 0 0; font-size: 14px"
-            class="scan-txt"
-            type="danger"
-            size="small"
-            :disabled="!details?.instId"
-            @click="handleIsGetOff"
-          >
+          <van-button v-if="showOffBtn" style="width: 60px; height: 30px; margin: 0 20px 0 0; font-size: 14px"
+            class="scan-txt" type="danger" size="small" :disabled="!details?.instId" @click="handleIsGetOff">
             下机
           </van-button>
-          <van-button style="width: 60px; height: 30px; margin: 0; font-size: 14px" class="scan-txt" type="primary" size="small" @click="backPrePage">
+          <van-button style="width: 60px; height: 30px; margin: 0; font-size: 14px" class="scan-txt" type="primary"
+            size="small" @click="backPrePage">
             返回
           </van-button>
         </div>
@@ -117,117 +111,103 @@
         </div>
       </div>
     </div>
-    <u-modal :show="modalVisible" :content="offLineContent" showCancelButton @cancel="close" @confirm="getOff" ref="uModal" :asyncClose="true"></u-modal>
+    <u-modal :show="modalVisible" :content="offLineContent" showCancelButton @cancel="close" @confirm="getOff"
+      ref="uModal" :asyncClose="true"></u-modal>
     <u-notify ref="uNotify"></u-notify>
     <!-- <blue-tooth ref="bluetoothRef" @getOff="callbackOff"></blue-tooth> -->
   </div>
 </template>
 
 <script>
-  // import BlueTooth from '../../../components/BlueTooth'
-  import { useMyAppointApi } from '/@/api/appoint'
-  const myAppointApi = useMyAppointApi()
-  import instAppointApi from '/@/api/instr/instAppoint'
-  import { useInstrApi } from '/@/api/instr'
-  const instApi = useInstrApi()
-  import to from 'await-to-js'
-  import moment from 'moment'
-  import { showConfirmDialog, showNotify } from 'vant'
-  export default {
-    name: 'LabsopIndex',
-    // components: { BlueTooth },
-    data() {
-      return {
-        offLineContent: '',
-        showOffBtn: true,
-        appointId: 0,
-        details: {},
-        modalVisible: false
+// import BlueTooth from '../../../components/BlueTooth'
+import { useMyAppointApi } from '/@/api/appoint'
+const myAppointApi = useMyAppointApi()
+import instAppointApi from '/@/api/instr/instAppoint'
+import { useInstrApi } from '/@/api/instr'
+const instApi = useInstrApi()
+import to from 'await-to-js'
+import moment from 'moment'
+import { showConfirmDialog, showNotify } from 'vant'
+import { useUserInfo } from '/@/stores/userInfo'
+import { scanCodeWxUrl } from '/@/constants/pageConstants'
+
+export default {
+  name: 'LabsopIndex',
+  // components: { BlueTooth },
+  data() {
+    return {
+      offLineContent: '',
+      showOffBtn: true,
+      appointId: 0,
+      details: {},
+      modalVisible: false
+    }
+  },
+  mounted() {
+    this.appointId = Number(this.$route.query.appointId)
+    this.getOnlineInfo()
+  },
+  methods: {
+    diffTime(start, end) {
+      const startTime = moment(start)
+      const endTime = moment(end)
+      // 计算两个时间的分钟差异
+      return endTime.diff(startTime, 'minutes')
+    },
+    // 下机的时候提示使用时长
+    calcTimeDiff(date) {
+      var new_date = new Date() //新建一个日期对象,默认现在的时间
+      var old_date = new Date(date) //设置过去的一个时间点,"yyyy-MM-dd HH:mm:ss"格式化日期
+      var difftime = new_date - old_date //计算时间差
+      return Math.round(difftime / 60000)
+    },
+    async getOnlineInfo() {
+      const params = { appointId: this.appointId }
+      const [err, res] = await to(myAppointApi.appointDetails({ ...params }))
+      if (err) return
+      if (res.code == 200 && res.data) {
+        this.details = res.data
       }
+      console.log(this.details)
     },
-    mounted() {
-      this.appointId = Number(this.$route.query.appointId)
-      this.getOnlineInfo()
+    // 返回上机信息页面
+    backPrePage() {
+      this.$router.push('/instr-appoint-record')
     },
-    methods: {
-      diffTime(start, end) {
-        const startTime = moment(start)
-        const endTime = moment(end)
-        // 计算两个时间的分钟差异
-        return endTime.diff(startTime, 'minutes')
-      },
-      // 下机的时候提示使用时长
-      calcTimeDiff(date) {
-        var new_date = new Date() //新建一个日期对象,默认现在的时间
-        var old_date = new Date(date) //设置过去的一个时间点,"yyyy-MM-dd HH:mm:ss"格式化日期
-        var difftime = new_date - old_date //计算时间差
-        return Math.round(difftime / 60000)
-      },
-      async getOnlineInfo() {
-        const params = { appointId: this.appointId }
-        const [err, res] = await to(myAppointApi.appointDetails({ ...params }))
-        if (err) return
-        if (res.code == 200 && res.data) {
-          this.details = res.data
-        }
-        console.log(this.details)
-      },
-      // 返回上机信息页面
-      backPrePage() {
-        this.$router.push('/instr-appoint-record')
-      },
-      async handleIsGetOff() {
-        const [err, res] = await to(instApi.getSettingDetail({ instId: this.details.instId, code: 'InstCfgUse' }))
-        if (err) return
-        // 上机时间
-        const useTime = this.calcTimeDiff(this.details.usedStartTime)
-        // 是否进行上机时间太短的提示
-        // 需要
-        if (res?.data?.config.shortWarningEnable) {
-          const shortWraningTime = this.getShortWraningTime(res?.data?.config.shortWarning, res?.data?.config.shortWarningUnit)
-          if (useTime > shortWraningTime) {
-            if (this.details.controlMode === '30') {
-              this.$refs.bluetoothRef.initBlue('close', this.details)
-            } else {
-              this.curAppointInfo = this.details
-              showConfirmDialog({
-                title: '提示',
-                message: `本次上机时长共计${useTime}分钟,确定要下机吗?`
-              })
-                .then((e) => {
-                  console.log(e)
-                  this.getOff()
-                })
-                .catch(() => {
-                  console.log('ssss')
-                })
-            }
+    async handleIsGetOff() {
+      const [err, res] = await to(instApi.getSettingDetail({ instId: this.details.instId, code: 'InstCfgUse' }))
+      if (err) return
+      // 上机时间
+      const useTime = this.calcTimeDiff(this.details.usedStartTime)
+      // 是否进行上机时间太短的提示
+      // 需要
+      if (res?.data?.config.shortWarningEnable) {
+        const shortWraningTime = this.getShortWraningTime(res?.data?.config.shortWarning, res?.data?.config.shortWarningUnit)
+        if (useTime > shortWraningTime) {
+          if (this.details.controlMode === '30') {
+            this.handleBluetoothOffLine()
           } else {
-            if (this.details.controlMode === '30') {
-              this.$refs.bluetoothRef.initBlue('close', this.details)
-            } else {
-              this.curAppointInfo = this.details
-              showConfirmDialog({
-                title: '提示',
-                message: `本次上机时长共计${useTime}分钟,不足${shortWraningTime}分钟,确定要下机吗?`
+            this.curAppointInfo = this.details
+            showConfirmDialog({
+              title: '提示',
+              message: `本次上机时长共计${useTime}分钟,确定要下机吗?`
+            })
+              .then((e) => {
+                console.log(e)
+                this.getOff()
+              })
+              .catch(() => {
+                console.log('ssss')
               })
-                .then((e) => {
-                  console.log(e)
-                  this.getOff()
-                })
-                .catch(() => {
-                  console.log('ssss')
-                })
-            }
           }
         } else {
           if (this.details.controlMode === '30') {
-            this.$refs.bluetoothRef.initBlue('close', this.details)
+            this.handleBluetoothOffLine()
           } else {
             this.curAppointInfo = this.details
             showConfirmDialog({
               title: '提示',
-              message: `本次上机时长共计${useTime}分钟,确定要下机吗?`
+              message: `本次上机时长共计${useTime}分钟,不足${shortWraningTime}分钟,确定要下机吗?`
             })
               .then((e) => {
                 console.log(e)
@@ -238,45 +218,101 @@
               })
           }
         }
-      },
-      // 计算时间转分钟
-      getShortWraningTime(time, unit) {
-        if (unit == 'hour') {
-          return time * 60
-        } else if (unit == 'minute') {
-          return time
-        }
-      },
-      close() {
-        this.modalVisible = false
-      },
-      async getOff() {
-        const params = { appointId: this.curAppointInfo.id }
-        const [err, res] = await to(instAppointApi.getOff({ ...params }))
-        if (err) return
-        if (res.code == 200) {
-          showNotify({ type: 'success', message: '下机成功' })
-          this.callbackOff()
+      } else {
+        if (this.details.controlMode === '30') {
+          this.handleBluetoothOffLine()
+        } else {
+          this.curAppointInfo = this.details
+          showConfirmDialog({
+            title: '提示',
+            message: `本次上机时长共计${useTime}分钟,确定要下机吗?`
+          })
+            .then((e) => {
+              console.log(e)
+              this.getOff()
+            })
+            .catch(() => {
+              console.log('ssss')
+            })
         }
-      },
-      callbackOff() {
-        this.$router.push('/instr-appoint-record')
       }
+    },
+    async handleBluetoothOffLine() {
+      const res = await useUserInfo().scanCode()
+      this.handleDeCode(res, this.handleBluetoothDeCode)
+    },
+    /**
+     *  解码
+     *  code 二维码内容
+     * func 解码后的操作函数
+     * */
+    async handleDeCode(code, func) {
+      if (this.loading) return
+      this.loading = true
+      const params = { content: code }
+      const [err, res] = await to(instApi.decode({ ...params }))
+      if (err) {
+        this.loading = false
+        showNotify({ type: 'danger', message: '解码失败,请重试' })
+        return
+      }
+      if (res?.code == 200 && res?.data?.content) {
+        func(res.data.content)
+      } else {
+        this.loading = false
+        showNotify({ type: 'danger', message: res?.message || '解码失败' })
+      }
+    },
+    //蓝牙 重新解码函数,获取解码内容之后 执行跳转操作
+    async handleBluetoothDeCode(content) {
+      if (content.includes('id')) {
+        const terminal = JSON.parse(content)?.terminal
+        const url = scanCodeWxUrl(terminal)
+        window.location.href = url
+      } else {
+        const url = scanCodeWxUrl(content)
+        window.location.href = url
+      }
+    },
+    // 计算时间转分钟
+    getShortWraningTime(time, unit) {
+      if (unit == 'hour') {
+        return time * 60
+      } else if (unit == 'minute') {
+        return time
+      }
+    },
+    close() {
+      this.modalVisible = false
+    },
+    async getOff() {
+      const params = { appointId: this.curAppointInfo.id }
+      const [err, res] = await to(instAppointApi.getOff({ ...params }))
+      if (err) return
+      if (res.code == 200) {
+        showNotify({ type: 'success', message: '下机成功' })
+        this.callbackOff()
+      }
+    },
+    callbackOff() {
+      this.$router.push('/instr-appoint-record')
     }
   }
+}
 </script>
 
 <style lang="scss" scoped>
-  .equ-container {
-    height: 100vh;
-    padding: 20px;
-    .inst-item {
-      border-radius: 10px;
-      padding: 15px;
-      box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
-      // .equ-tit {
-      //   width: 74px;
-      // }
-    }
+.equ-container {
+  height: 100vh;
+  padding: 20px;
+
+  .inst-item {
+    border-radius: 10px;
+    padding: 15px;
+    box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
+    // .equ-tit {
+    //   width: 74px;
+    // }
   }
+}
 </style>