Application.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. <template>
  2. <div class="application-dialog-container">
  3. <van-popup v-model:show="state.dialog.isShowDialog" position="bottom" :style="{ height: '90vh' }" round
  4. :closeable="true" @close="onCancel" :close-on-click-overlay="false">
  5. <div class="popup-wrapper">
  6. <h3 class="popup-title">{{ state.dialog.title }}</h3>
  7. <div class="popup-content">
  8. <van-form ref="expertDialogFormRef" @submit="onSubmit">
  9. <h4 class="mb8 mt8">基本信息</h4>
  10. <van-cell-group>
  11. <van-field v-model="state.form.projectName" label="课题名称" placeholder="请选择" readonly is-link required
  12. @click="showProjectPicker = true" :rules="rules.projectGroupId" />
  13. <van-field v-model="userInfos.userName" label="姓名" placeholder="姓名" readonly />
  14. <van-field v-model="userInfos.deptName" label="部门" placeholder="部门" readonly />
  15. <van-field v-model="userInfos.phone" label="联系方式" placeholder="联系方式" readonly />
  16. </van-cell-group>
  17. <h4 class="mb8 mt10">实验动物笼位预约信息</h4>
  18. <van-cell-group>
  19. <van-field v-model="state.form.number" label="笼位数量" placeholder="笼位数量" type="digit" required
  20. :rules="rules.number">
  21. <template #button>
  22. <van-stepper v-model="state.form.number" :min="1" integer />
  23. </template>
  24. </van-field>
  25. <van-field v-model="state.form.startDate" label="开始使用时间" placeholder="请选择时间" readonly is-link required
  26. @click="showStartDatePicker = true" :rules="rules.startDate" />
  27. <van-field v-model="state.form.categoryName" label="动物类别" placeholder="请选择" readonly is-link required
  28. @click="showCategoryPicker = true" :rules="rules.categoryId" />
  29. <van-field v-model="state.form.variety" label="品种品系" placeholder="请输入品种品系" required
  30. :rules="rules.variety" />
  31. <van-field v-model="state.form.levelName" label="饲养区域" placeholder="请选择" readonly is-link
  32. @click="showLevelPicker = true" />
  33. <van-field v-model="state.form.age" label="周龄" placeholder="请输入周龄" type="digit">
  34. <template #button>
  35. <van-stepper v-model="state.form.age" :min="0" integer />
  36. </template>
  37. </van-field>
  38. <van-field v-model="state.form.weight" label="体重" placeholder="请输入体重" type="number">
  39. <template #button>
  40. <van-stepper v-model="state.form.weight" :min="0" />
  41. </template>
  42. </van-field>
  43. <van-field v-model="state.form.maleNumber" label="雄性" placeholder="雄性数量" type="digit">
  44. <template #button>
  45. <van-stepper v-model="state.form.maleNumber" :min="0" integer />
  46. </template>
  47. </van-field>
  48. <van-field v-model="state.form.famaleNumber" label="雌性" placeholder="雌性数量" type="digit">
  49. <template #button>
  50. <van-stepper v-model="state.form.famaleNumber" :min="0" integer />
  51. </template>
  52. </van-field>
  53. <van-field v-model="state.form.totalNumber" label="合计" placeholder="合计" readonly type="digit">
  54. <!-- <template #button>
  55. <van-stepper v-model="state.form.totalNumber" :min="0" integer disabled />
  56. </template> -->
  57. </van-field>
  58. <van-field v-model="state.form.feedingDay" label="饲养总天数" placeholder="饲养总天数" type="digit" required
  59. :rules="rules.feedingDay">
  60. <template #button>
  61. <van-stepper v-model="state.form.feedingDay" :min="1" integer />
  62. </template>
  63. </van-field>
  64. </van-cell-group>
  65. <h4 class="mb8 mt20">采购渠道</h4>
  66. <van-cell-group>
  67. <van-field label="采购渠道" required :rules="rules.buyFrom">
  68. <template #input>
  69. <van-radio-group v-model="state.form.buyFrom" direction="horizontal">
  70. <van-radio :name="ProcurementChannels.PURCHASED_BY_OTHERS">动物房代购</van-radio>
  71. <van-radio :name="ProcurementChannels.PURCHASED_BY_MYSELF">自行购买</van-radio>
  72. </van-radio-group>
  73. </template>
  74. </van-field>
  75. <van-field v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF"
  76. v-model="state.form.comeFromUnit" label="外购来源单位" placeholder="请输入外购来源单位" required
  77. :rules="rules.comeFromUnit" />
  78. <van-field v-model="state.form.comeTime" label="动物到达时间" placeholder="请选择到达时间" readonly is-link
  79. @click="showComeTimePicker = true" />
  80. </van-cell-group>
  81. <!-- 自行购买时的文件上传 -->
  82. <template v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
  83. <h4 class="mb8 mt20">文件上传</h4>
  84. <van-cell-group>
  85. <van-cell title="生产许可证副本" required :rules="rules.licenseNumberFile">
  86. <van-uploader v-model="licenseNumberFileList"
  87. :after-read="(file) => handleFileUpload(file, UploadFileType.LICENSE_NUMBER)"
  88. :before-delete="() => handleRemove(UploadFileType.LICENSE_NUMBER)" :max-count="1"
  89. :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX" />
  90. <div class="upload-tip">支持格式:jpg png pdf等,单个文件不超过20MB</div>
  91. </van-cell>
  92. <van-cell title="近三个月动物质量检测证明" required :rules="rules.animalTestDateFile">
  93. <van-uploader v-model="animalTestDateFileList"
  94. :after-read="(file) => handleFileUpload(file, UploadFileType.ANIMAL_TEST_DATE)"
  95. :before-delete="() => handleRemove(UploadFileType.ANIMAL_TEST_DATE)" :max-count="1"
  96. :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX" />
  97. <div class="upload-tip">支持格式:jpg png pdf等,单个文件不超过20MB</div>
  98. </van-cell>
  99. <van-cell title="基因鉴定报告">
  100. <van-uploader v-model="geneIdentificationFileList"
  101. :after-read="(file) => handleFileUpload(file, UploadFileType.ENV_TEST_DATE)"
  102. :before-delete="() => handleRemove(UploadFileType.ENV_TEST_DATE)" :max-count="1"
  103. :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX" />
  104. <div class="upload-tip">支持格式:jpg png pdf等,单个文件不超过20MB</div>
  105. </van-cell>
  106. </van-cell-group>
  107. </template>
  108. <h4 class="mb8 mt20">特殊要求和附件</h4>
  109. <van-cell-group>
  110. <van-field label="是否有特殊饲养要求">
  111. <template #input>
  112. <van-radio-group v-model="state.form.hasFeedingSpecial" direction="horizontal">
  113. <van-radio :name="FeedingSpecial.HAVE_FEEDING_SPECIAL">有</van-radio>
  114. <van-radio :name="FeedingSpecial.NO_FEEDING_SPECIAL">无</van-radio>
  115. </van-radio-group>
  116. </template>
  117. </van-field>
  118. <van-field v-if="state.form.hasFeedingSpecial === FeedingSpecial.HAVE_FEEDING_SPECIAL"
  119. v-model="state.form.feedingSpecialDesc" label="特殊饲养要求" placeholder="输入特殊饲养要求,如每天更换垫料等" required
  120. :rules="rules.feedingSpecialDesc" />
  121. </van-cell-group>
  122. <h4 class="mb8 mt20">附件上传</h4>
  123. <van-cell-group>
  124. <van-cell title="实验动物福利伦理审查申请表" required :rules="rules.ethicsCheckFile">
  125. <van-uploader v-model="ethicsCheckFileList"
  126. :after-read="(file) => handleFileUpload(file, UploadFileType.ETHICS_CHECK_FILE)"
  127. :before-delete="() => handleRemove(UploadFileType.ETHICS_CHECK_FILE)" :max-count="1"
  128. :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX" />
  129. <div class="upload-tip">支持格式:jpg png pdf等,单个文件不超过20MB</div>
  130. </van-cell>
  131. <van-cell title="实验动物福利伦理审查意见表" required :rules="rules.ethicsAdviceFile">
  132. <van-uploader v-model="ethicsAdviceFileList"
  133. :after-read="(file) => handleFileUpload(file, UploadFileType.ETHICS_ADVICE_FILE)"
  134. :before-delete="() => handleRemove(UploadFileType.ETHICS_ADVICE_FILE)" :max-count="1"
  135. :accept="state.SUPPORT_FILE_UPLOAD_TYPE_MAX" />
  136. <div class="upload-tip">支持格式:jpg png pdf等,单个文件不超过20MB</div>
  137. </van-cell>
  138. </van-cell-group>
  139. <div class="mt30 mb30 checkbox-wrapper">
  140. <van-checkbox v-model="safePromise">
  141. <div class="safePromise">
  142. 本人(以上所述课题的负责人)谨此声明:本项目所包含的实验动物、实验方法、实验材料及试剂无放射性、感染性和化学毒性,所有参与实验人员在实验过程中自愿遵守遵义医科大学附属医院实验动物房的管理制度和操作流程,愿意根据其规定的付费方式向遵义医科大学附属医院实验动物房支付所有的费用。
  143. </div>
  144. </van-checkbox>
  145. </div>
  146. </van-form>
  147. </div>
  148. <div class="dialog-footer">
  149. <van-button type="primary" @click="onSubmit" block native-type="submit">
  150. 提交
  151. </van-button>
  152. </div>
  153. </div>
  154. </van-popup>
  155. <!-- 课题选择器 -->
  156. <van-popup v-model:show="showProjectPicker" position="bottom">
  157. <van-picker :columns="projects" :columns-field-names="{ text: 'projectName', value: 'id' }"
  158. @confirm="onProjectConfirm" @cancel="showProjectPicker = false" />
  159. </van-popup>
  160. <!-- 动物类别选择器 -->
  161. <van-popup v-model:show="showCategoryPicker" position="bottom">
  162. <van-picker :columns="animalTypeList" :columns-field-names="{ text: 'name', value: 'id' }"
  163. @confirm="onCategoryConfirm" @cancel="showCategoryPicker = false" />
  164. </van-popup>
  165. <!-- 饲养区域选择器 -->
  166. <van-popup v-model:show="showLevelPicker" position="bottom">
  167. <van-picker :columns="LeavelList" :columns-field-names="{ text: 'name', value: 'id' }" @confirm="onLevelConfirm"
  168. @cancel="showLevelPicker = false" />
  169. </van-popup>
  170. <!-- 开始使用时间选择器 -->
  171. <van-popup v-model:show="showStartDatePicker" position="bottom" :style="{ height: '80vh' }" round>
  172. <van-calendar v-model:show="showStartDatePicker" @confirm="onStartDateConfirm" :min-date="new Date()" />
  173. </van-popup>
  174. <!-- 动物到达时间选择器 -->
  175. <van-popup v-model:show="showComeTimePicker" position="bottom" :style="{ height: '80vh' }" round>
  176. <van-calendar v-model:show="showComeTimePicker" @confirm="onComeTimeConfirm" />
  177. </van-popup>
  178. </div>
  179. </template>
  180. <script setup lang="ts" name="systemProDialog">
  181. import { reactive, ref, watch } from 'vue'
  182. import to from 'await-to-js'
  183. import { showToast, showNotify } from 'vant'
  184. import type { FormInstance } from 'vant/es'
  185. import dayjs from 'dayjs'
  186. import { storeToRefs } from 'pinia'
  187. import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
  188. import { LeavelList, SUPPORT_FILE_UPLOAD_TYPE_MAX } from '/@/constants/pageConstants'
  189. import { deepClone } from '/@/utils/other'
  190. import { useUserInfo } from '/@/stores/userInfo'
  191. import { ProcurementChannels, FeedingSpecial, UploadFileType } from '/@/constants/pageConstants'
  192. import { handleUpload } from '/@/utils/upload'
  193. import { formatDate } from '/@/utils/formatTime'
  194. const stores = useUserInfo()
  195. const { userInfos } = storeToRefs(stores)
  196. // 定义子组件向父组件传值/事件
  197. const emit = defineEmits(['refresh'])
  198. const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
  199. const expertDialogFormRef = ref<FormInstance>()
  200. const projectGroupList = ref<any[]>([])
  201. const projects = ref<any[]>([])
  202. const showProjectPicker = ref(false)
  203. const showCategoryPicker = ref(false)
  204. const showLevelPicker = ref(false)
  205. const showStartDatePicker = ref(false)
  206. const showComeTimePicker = ref(false)
  207. const rules = {
  208. projectGroupId: [{ required: true, message: '课题名称不能为空' }],
  209. categoryId: [{ required: true, message: '动物类别不能为空' }],
  210. variety: [{ required: true, message: '品种品系不能为空' }],
  211. number: [{ required: true, message: '笼位数量不能为空' }],
  212. startDate: [{ required: true, message: '开始使用时间不能为空' }],
  213. buyFrom: [{ required: true, message: '采购渠道不能为空' }],
  214. feedingDay: [{ required: true, message: '饲养总天数不能为空' }],
  215. comeFromUnit: [{ required: true, message: '外购来源单位不能为空' }],
  216. feedingSpecialDesc: [{ required: true, message: '特殊饲养要求不能为空' }],
  217. ethicsCheckFile: [{ required: true, message: '实验动物福利伦理审查申请表不能为空' }],
  218. ethicsAdviceFile: [{ required: true, message: '实验动物福利伦理审查意见表不能为空' }],
  219. licenseNumberFile: [{ required: true, message: '生产许可证副本不能为空' }],
  220. animalTestDateFile: [{ required: true, message: '近三个月动物质量检测证明不能为空' }],
  221. }
  222. const licenseNumberFileList = ref<any[]>([])
  223. const animalTestDateFileList = ref<any[]>([])
  224. const geneIdentificationFileList = ref<any[]>([])
  225. const ethicsCheckFileList = ref<any[]>([])
  226. const ethicsAdviceFileList = ref<any[]>([])
  227. const safePromise = ref<boolean>(false)
  228. const animalTypeList = ref<any[]>([])
  229. const defaultFormData = {
  230. projectGroupName: '',
  231. projectGroupId: null,
  232. projectName: '',
  233. categoryName: '',
  234. variety: '',
  235. categoryId: null,
  236. level: null,
  237. levelName: '',
  238. number: 1,
  239. startDate: '',
  240. maleNumber: 0,
  241. famaleNumber: 0,
  242. weight: 0,
  243. age: 0,
  244. feedingDay: 0,
  245. buyFrom: ProcurementChannels.PURCHASED_BY_OTHERS,
  246. comeTime: '',
  247. comeFromUnit: '',
  248. licenseNumberFile: [],
  249. animalTestDateFile: [],
  250. geneIdentificationFile: [],
  251. hasFeedingSpecial: FeedingSpecial.HAVE_FEEDING_SPECIAL,
  252. feedingSpecialDesc: '',
  253. ethicsCheckFile: [],
  254. ethicsAdviceFile: [],
  255. deptName: '',
  256. phone: '',
  257. totalNumber: 0,
  258. }
  259. const state = reactive({
  260. SUPPORT_FILE_UPLOAD_TYPE_MAX,
  261. form: { ...defaultFormData },
  262. dialog: {
  263. isShowDialog: false,
  264. type: '',
  265. title: '',
  266. submitTxt: '',
  267. },
  268. })
  269. const getDicts = () => {
  270. Promise.all([
  271. platAnimalCageApplicationApi.getAnimalTypeList({}),
  272. platAnimalCageApplicationApi.getProjectGroup({}),
  273. ]).then(([animalType, projectGroup]) => {
  274. animalTypeList.value = animalType.data
  275. if (projectGroup && projectGroup.data) {
  276. projectGroupList.value = projectGroup.data
  277. const currentProject = projectGroup.data[0]?.projects
  278. if (currentProject) {
  279. projects.value = currentProject
  280. }
  281. }
  282. })
  283. }
  284. // 重置表单数据
  285. const resetForm = () => {
  286. state.form = { ...defaultFormData }
  287. expertDialogFormRef.value?.resetValidation()
  288. licenseNumberFileList.value = []
  289. animalTestDateFileList.value = []
  290. geneIdentificationFileList.value = []
  291. ethicsCheckFileList.value = []
  292. ethicsAdviceFileList.value = []
  293. safePromise.value = false
  294. }
  295. // 打开弹窗
  296. const openDialog = () => {
  297. resetForm()
  298. getDicts()
  299. state.dialog.title = '新增实验动物笼位申请'
  300. state.dialog.isShowDialog = true
  301. }
  302. // 关闭弹窗
  303. const closeDialog = () => {
  304. expertDialogFormRef.value?.resetValidation()
  305. state.dialog.isShowDialog = false
  306. licenseNumberFileList.value = []
  307. animalTestDateFileList.value = []
  308. geneIdentificationFileList.value = []
  309. ethicsCheckFileList.value = []
  310. ethicsAdviceFileList.value = []
  311. safePromise.value = false
  312. }
  313. // 取消
  314. const onCancel = () => {
  315. closeDialog()
  316. }
  317. const handleFileUpload = async (file: any, type: UploadFileType) => {
  318. // 处理单个文件或文件数组
  319. const files = Array.isArray(file) ? file : [file]
  320. for (const item of files) {
  321. // 检查文件大小
  322. if (item.file && item.file.size / 1024 / 1024 > 20) {
  323. showNotify({
  324. type: 'warning',
  325. message: '上传文件大小不能超过 20MB!',
  326. })
  327. // 移除文件
  328. if (type === UploadFileType.LICENSE_NUMBER) {
  329. licenseNumberFileList.value = licenseNumberFileList.value.filter((f) => f !== item)
  330. } else if (type === UploadFileType.ANIMAL_TEST_DATE) {
  331. animalTestDateFileList.value = animalTestDateFileList.value.filter((f) => f !== item)
  332. } else if (type === UploadFileType.ENV_TEST_DATE) {
  333. geneIdentificationFileList.value = geneIdentificationFileList.value.filter((f) => f !== item)
  334. } else if (type === UploadFileType.ETHICS_CHECK_FILE) {
  335. ethicsCheckFileList.value = ethicsCheckFileList.value.filter((f) => f !== item)
  336. } else if (type === UploadFileType.ETHICS_ADVICE_FILE) {
  337. ethicsAdviceFileList.value = ethicsAdviceFileList.value.filter((f) => f !== item)
  338. }
  339. return
  340. }
  341. if (item.file) {
  342. item.status = 'uploading'
  343. const [err, res]: ToResponse = await to(handleUpload(item.file))
  344. if (err) {
  345. item.status = 'failed'
  346. showNotify({
  347. type: 'danger',
  348. message: '上传失败',
  349. })
  350. return
  351. }
  352. item.status = 'done'
  353. item.url = res
  354. item.name = item.file.name
  355. // 保存到 form
  356. if (type === UploadFileType.LICENSE_NUMBER) {
  357. state.form.licenseNumberFile = [{ name: item.name, url: res }]
  358. } else if (type === UploadFileType.ANIMAL_TEST_DATE) {
  359. state.form.animalTestDateFile = [{ name: item.name, url: res }]
  360. } else if (type === UploadFileType.ENV_TEST_DATE) {
  361. state.form.geneIdentificationFile = [{ name: item.name, url: res }]
  362. } else if (type === UploadFileType.ETHICS_CHECK_FILE) {
  363. state.form.ethicsCheckFile = [{ name: item.name, url: res }]
  364. } else if (type === UploadFileType.ETHICS_ADVICE_FILE) {
  365. state.form.ethicsAdviceFile = [{ name: item.name, url: res }]
  366. }
  367. }
  368. }
  369. }
  370. const handleRemove = (type: UploadFileType) => {
  371. if (type === UploadFileType.LICENSE_NUMBER) {
  372. licenseNumberFileList.value = []
  373. state.form.licenseNumberFile = []
  374. } else if (type === UploadFileType.ANIMAL_TEST_DATE) {
  375. animalTestDateFileList.value = []
  376. state.form.animalTestDateFile = []
  377. } else if (type === UploadFileType.ENV_TEST_DATE) {
  378. geneIdentificationFileList.value = []
  379. state.form.geneIdentificationFile = []
  380. } else if (type === UploadFileType.ETHICS_CHECK_FILE) {
  381. ethicsCheckFileList.value = []
  382. state.form.ethicsCheckFile = []
  383. } else if (type === UploadFileType.ETHICS_ADVICE_FILE) {
  384. ethicsAdviceFileList.value = []
  385. state.form.ethicsAdviceFile = []
  386. }
  387. return true
  388. }
  389. const onProjectConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
  390. if (selectedOptions.length > 0) {
  391. const selected = selectedOptions[0]
  392. state.form.projectGroupId = selected.id
  393. state.form.projectName = selected.projectName
  394. }
  395. showProjectPicker.value = false
  396. }
  397. const onCategoryConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
  398. if (selectedOptions.length > 0) {
  399. const selected = selectedOptions[0]
  400. state.form.categoryId = selected.id
  401. state.form.categoryName = selected.name
  402. }
  403. showCategoryPicker.value = false
  404. }
  405. const onLevelConfirm = ({ selectedOptions }: { selectedOptions: any[] }) => {
  406. if (selectedOptions.length > 0) {
  407. const selected = selectedOptions[0]
  408. state.form.level = selected.id
  409. state.form.levelName = selected.name
  410. }
  411. showLevelPicker.value = false
  412. }
  413. const onStartDateConfirm = (date: Date) => {
  414. state.form.startDate = formatDate(date, 'YYYY-mm-dd')
  415. showStartDatePicker.value = false
  416. }
  417. const onComeTimeConfirm = (date: Date) => {
  418. state.form.comeTime = formatDate(date, 'YYYY-mm-dd')
  419. showComeTimePicker.value = false
  420. }
  421. // 提交
  422. const onSubmit = async () => {
  423. try {
  424. await expertDialogFormRef.value?.validate()
  425. } catch (error: any) {
  426. // 显示表单验证错误
  427. let errorMessage = '请完善必填信息'
  428. if (error) {
  429. // Vant 表单验证错误可能是数组格式
  430. if (Array.isArray(error) && error.length > 0) {
  431. const firstError = error[0]
  432. if (firstError && firstError.message) {
  433. errorMessage = firstError.message
  434. }
  435. } else if (error.message) {
  436. errorMessage = error.message
  437. }
  438. }
  439. showNotify({
  440. type: 'warning',
  441. message: errorMessage,
  442. })
  443. return
  444. }
  445. if (!safePromise.value) {
  446. showNotify({
  447. type: 'warning',
  448. message: '请阅读并勾选安全承诺!',
  449. })
  450. return
  451. }
  452. if (!state.form.maleNumber && !state.form.famaleNumber) {
  453. showNotify({
  454. type: 'warning',
  455. message: '请输入雄性或雌性数量!',
  456. })
  457. return
  458. }
  459. // 验证自行购买时的必填项
  460. if (state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF) {
  461. if (!state.form.licenseNumberFile.length) {
  462. showNotify({
  463. type: 'warning',
  464. message: '请上传生产许可证副本!',
  465. })
  466. return
  467. }
  468. if (!state.form.animalTestDateFile.length) {
  469. showNotify({
  470. type: 'warning',
  471. message: '请上传近三个月动物质量检测证明!',
  472. })
  473. return
  474. }
  475. }
  476. // 验证必填文件
  477. if (!state.form.ethicsCheckFile.length) {
  478. showNotify({
  479. type: 'warning',
  480. message: '请上传实验动物福利伦理审查申请表!',
  481. })
  482. return
  483. }
  484. if (!state.form.ethicsAdviceFile.length) {
  485. showNotify({
  486. type: 'warning',
  487. message: '请上传实验动物福利伦理审查意见表!',
  488. })
  489. return
  490. }
  491. const params = {
  492. ...deepClone(state.form),
  493. categoryName: animalTypeList.value.find((item) => item.id == state.form.categoryId)?.name,
  494. projectGroupName: projects.value.find((item) => item.id == state.form.projectGroupId)?.projectName,
  495. startDate: dayjs(state.form.startDate).format('YYYY-MM-DD'),
  496. comeTime: state.form.comeTime ? dayjs(state.form.comeTime).format('YYYY-MM-DD') : '',
  497. licenseNumberFile: JSON.stringify(state.form.licenseNumberFile),
  498. animalTestDateFile: JSON.stringify(state.form.animalTestDateFile),
  499. geneIdentificationFile: JSON.stringify(state.form.geneIdentificationFile),
  500. ethicsCheckFile: JSON.stringify(state.form.ethicsCheckFile),
  501. ethicsAdviceFile: JSON.stringify(state.form.ethicsAdviceFile),
  502. maleNumber: Number(state.form.maleNumber) || 0,
  503. famaleNumber: Number(state.form.famaleNumber) || 0,
  504. number: Number(state.form.number) || 1,
  505. age: Number(state.form.age) || 0,
  506. weight: state.form.weight ? parseFloat(String(state.form.weight)) : 0,
  507. feedingDay: Number(state.form.feedingDay) || 0,
  508. totalNumber: Number(state.form.totalNumber) || 0,
  509. }
  510. Object.entries(params).forEach(([key, value]) => {
  511. if (value === '' || value === null) {
  512. delete params[key as keyof typeof params]
  513. }
  514. })
  515. const post = platAnimalCageApplicationApi.create
  516. const [err]: ToResponse = await to(post(params))
  517. if (err) return
  518. showToast({
  519. type: 'success',
  520. message: '操作成功',
  521. })
  522. closeDialog()
  523. emit('refresh')
  524. }
  525. watch(
  526. () => [state.form.maleNumber, state.form.famaleNumber],
  527. ([maleNumber, famaleNumber]) => {
  528. state.form.totalNumber = (Number(maleNumber) || 0) + (Number(famaleNumber) || 0)
  529. },
  530. { immediate: true },
  531. )
  532. // 暴露变量
  533. defineExpose({
  534. openDialog,
  535. })
  536. </script>
  537. <style lang="scss" scoped>
  538. .application-dialog-container {
  539. .popup-wrapper {
  540. display: flex;
  541. flex-direction: column;
  542. height: 100%;
  543. overflow: hidden;
  544. }
  545. .popup-title {
  546. font-size: 18px;
  547. font-weight: 600;
  548. text-align: center;
  549. padding: 16px 0;
  550. margin: 0;
  551. border-bottom: 1px solid #ebedf0;
  552. flex-shrink: 0;
  553. background-color: #fff;
  554. position: sticky;
  555. top: 0;
  556. z-index: 1;
  557. padding-right: 40px;
  558. }
  559. .popup-content {
  560. flex: 1;
  561. padding: 16px;
  562. overflow-y: auto;
  563. -webkit-overflow-scrolling: touch;
  564. }
  565. h4 {
  566. font-size: 16px;
  567. font-weight: 600;
  568. margin: 16px 0 8px;
  569. padding-left: 8px;
  570. position: relative;
  571. &::before {
  572. content: '';
  573. position: absolute;
  574. left: 0;
  575. top: 50%;
  576. transform: translateY(-50%);
  577. width: 3px;
  578. height: 16px;
  579. background-color: #1c9bfd;
  580. }
  581. }
  582. .checkbox-wrapper {
  583. padding: 16px;
  584. background-color: #f7f8fa;
  585. border-radius: 8px;
  586. margin: 16px 0;
  587. .safePromise {
  588. white-space: pre-wrap;
  589. line-height: 1.6;
  590. }
  591. }
  592. .dialog-footer {
  593. padding: 16px;
  594. padding-bottom: calc(16px + env(safe-area-inset-bottom));
  595. flex-shrink: 0;
  596. background-color: #fff;
  597. border-top: 1px solid #ebedf0;
  598. position: sticky;
  599. bottom: 0;
  600. z-index: 10;
  601. }
  602. .upload-tip {
  603. font-size: 12px;
  604. color: #969799;
  605. margin-top: 8px;
  606. }
  607. }
  608. :deep(.van-checkbox) {
  609. white-space: pre-wrap;
  610. line-height: 1.6;
  611. }
  612. :deep(.van-field__label) {
  613. width: 120px;
  614. }
  615. :deep(.van-popup__close-icon) {
  616. z-index: 100;
  617. top: 16px;
  618. right: 16px;
  619. }
  620. </style>