Application.vue 34 KB

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