Application.vue 28 KB

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