13
0

Application.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  1. <template>
  2. <div class="application-dialog-container">
  3. <el-dialog :title="state.dialog.title" @close="onCancel" :close-on-click-modal="false" :destroy-on-close="true"
  4. v-model="state.dialog.isShowDialog" width="800px">
  5. <el-form ref="expertDialogFormRef" :model="state.form" :rules="rules" size="default" label-width="140px"
  6. label-position="top">
  7. <LaText size="16" type="important" bold class="mb16">基本信息</LaText>
  8. <el-row :gutter="20">
  9. <el-col :span="12" class="mb16">
  10. <el-form-item label="课题名称" prop="projectGroupId">
  11. <el-select :disabled="state.dialog.type === 'view'" v-model="state.form.projectGroupId" placeholder="请选择">
  12. <el-option v-for="item in projects" :key="item.id" :label="item.projectName" :value="item.id" />
  13. </el-select>
  14. </el-form-item>
  15. </el-col>
  16. <el-col :span="12" class="mb16">
  17. <el-form-item label="姓名" prop="group">
  18. <el-input v-if="state.dialog.type === 'add'" v-model="userInfos.nickName" disabled />
  19. <el-input v-else v-model="state.form.userName" disabled />
  20. </el-form-item>
  21. </el-col>
  22. </el-row>
  23. <el-row :gutter="20">
  24. <el-col :span="12" class="mb16">
  25. <el-form-item label="部门" prop="deptName">
  26. <el-input v-if="state.dialog.type === 'add'" v-model="userInfos.deptName" disabled />
  27. <el-input v-else v-model="state.form.deptName" disabled />
  28. </el-form-item>
  29. </el-col>
  30. <el-col :span="12" class="mb16">
  31. <el-form-item label="联系方式" prop="phone">
  32. <el-input v-if="state.dialog.type === 'add'" v-model="userInfos.phone" disabled />
  33. <el-input v-else v-model="state.form.phone" disabled />
  34. </el-form-item>
  35. </el-col>
  36. </el-row>
  37. <LaText class="mb16 mt20" size="16" type="important" bold>实验动物笼位预约信息</LaText>
  38. <el-row class="mt10" :gutter="20">
  39. <el-col :span="12" class="mb16">
  40. <el-form-item label="笼位数量" prop="number">
  41. <el-input-number :disabled="state.dialog.type === 'view'" v-model="state.form.number" style="width: 100%"
  42. :min="1" />
  43. </el-form-item>
  44. </el-col>
  45. <el-col :span="12" class="mb16">
  46. <el-form-item label="开始使用时间" prop="startDate">
  47. <el-date-picker :disabled="state.dialog.type === 'view'" v-model="state.form.startDate" type="date"
  48. placeholder="请选择开始使用时间" clearable style="width: 100%" />
  49. </el-form-item>
  50. </el-col>
  51. </el-row>
  52. <el-row class="mt10" :gutter="20">
  53. <el-col :span="12" class="mb16">
  54. <el-form-item label="动物类别" prop="categoryId">
  55. <el-select :disabled="state.dialog.type === 'view'" v-model="state.form.categoryId" placeholder="请选择">
  56. <el-option v-for="item in animalTypeList" :key="item.id" :label="item.name" :value="item.id" />
  57. </el-select>
  58. </el-form-item>
  59. </el-col>
  60. <el-col :span="12" class="mb16">
  61. <el-form-item label="品种品系" prop="variety">
  62. <el-input :disabled="state.dialog.type === 'view'" v-model="state.form.variety" placeholder="请输入品种品系" />
  63. </el-form-item>
  64. </el-col>
  65. </el-row>
  66. <el-row class="mt10" :gutter="20">
  67. <el-col :span="12" class="mb16">
  68. <el-form-item label="饲养区域" prop="level">
  69. <el-select :disabled="state.dialog.type === 'view'" v-model="state.form.level" placeholder="请选择">
  70. <el-option v-for="item in LeavelList" :key="item.id" :label="item.name" :value="item.id" />
  71. </el-select>
  72. </el-form-item>
  73. </el-col>
  74. <el-col :span="12" class="mb16">
  75. <el-form-item label="周龄(w)" prop="age">
  76. <div style="display: flex; align-items: center; gap: 8px;">
  77. <el-input-number :disabled="state.dialog.type === 'view'" v-model="state.form.age.min"
  78. placeholder="最小周龄" style="width: 45%" :min="0" />
  79. <span style="color: #999;">至</span>
  80. <el-input-number :disabled="state.dialog.type === 'view'" v-model="state.form.age.max"
  81. placeholder="最大周龄" style="width: 45%" :min="0" />
  82. </div>
  83. </el-form-item>
  84. </el-col>
  85. <el-col :span="12" class="mb16">
  86. <el-form-item label="体重(g)" prop="weight">
  87. <div style="display: flex; align-items: center; gap: 8px;">
  88. <el-input-number :disabled="state.dialog.type === 'view'" v-model="state.form.weight.min"
  89. placeholder="最小体重" style="width: 45%" :min="0" :precision="2" />
  90. <span style="color: #999;">至</span>
  91. <el-input-number :disabled="state.dialog.type === 'view'" v-model="state.form.weight.max"
  92. placeholder="最大体重" style="width: 45%" :min="0" :precision="2" />
  93. </div>
  94. </el-form-item>
  95. </el-col>
  96. <el-col :span="12" class="mb16">
  97. <el-form-item label="饲养总天数" prop="feedingDay">
  98. <el-input-number :disabled="state.dialog.type === 'view'" v-model="state.form.feedingDay"
  99. style="width: 100%" :min="1" />
  100. </el-form-item>
  101. </el-col>
  102. </el-row>
  103. <el-row class="mt10" :gutter="20">
  104. <el-col :span="6" class="mb16">
  105. <el-form-item label="雄性" prop="maleNumber">
  106. <el-input-number style="width: 100%" :disabled="state.dialog.type === 'view'" placeholder="雄性数量"
  107. v-model="state.form.maleNumber" :min="0" />
  108. </el-form-item>
  109. </el-col>
  110. <el-col :span="6" class="mb16">
  111. <el-form-item label="雌性" prop="famaleNumber">
  112. <el-input-number style="width: 100%" :disabled="state.dialog.type === 'view'" placeholder="雌性数量"
  113. v-model="state.form.famaleNumber" :min="0" />
  114. </el-form-item>
  115. </el-col>
  116. <el-col :span="12" class="mb16">
  117. <el-form-item label="合计" prop="totalNumber">
  118. <el-input-number style="width: 100%" disabled placeholder="合计" v-model="state.form.totalNumber"
  119. :min="0" />
  120. </el-form-item>
  121. </el-col>
  122. </el-row>
  123. <LaText class="mb16 mt20" size="16" type="important" bold>采购渠道</LaText>
  124. <el-row class="mt10" :gutter="20">
  125. <el-col :span="12" class="mb16">
  126. <el-form-item label="采购渠道" prop="buyFrom">
  127. <el-radio-group :disabled="state.dialog.type === 'view'" v-model="state.form.buyFrom">
  128. <el-radio :label="ProcurementChannels.PURCHASED_BY_OTHERS" size="large">动物房代购</el-radio>
  129. <el-radio :label="ProcurementChannels.PURCHASED_BY_MYSELF" size="large">自行购买</el-radio>
  130. </el-radio-group>
  131. </el-form-item>
  132. </el-col>
  133. <el-col :span="12" class="mb16">
  134. <el-form-item label="动物到达时间" prop="comeTime"
  135. :required="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
  136. <el-date-picker :disabled="state.dialog.type === 'view'" v-model="state.form.comeTime" type="date"
  137. placeholder="请选择到达时间" clearable style="width: 100%" />
  138. </el-form-item>
  139. </el-col>
  140. </el-row>
  141. <el-row v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF" class="mt10" :gutter="20">
  142. <el-col :span="12" class="mb16">
  143. <el-form-item label="外购来源单位"
  144. :prop="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF ? 'comeFromUnit' : ''">
  145. <el-input :disabled="state.dialog.type === 'view'" v-model="state.form.comeFromUnit" />
  146. </el-form-item>
  147. </el-col>
  148. </el-row>
  149. <el-row class="mt10" :gutter="20" v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
  150. <el-col :span="12" class="mb16">
  151. <el-form-item label="生产许可证副本" prop="licenseNumberFile"
  152. :required="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
  153. <el-upload v-model:file-list="licenseNumberFileList" class="upload-demo" :action="uploadUrl" :limit="1"
  154. style="width: 100%" :before-upload="beforeAvatarFileUpload" :disabled="state.dialog.type === 'view'"
  155. :on-preview="handlePreview" :on-remove="() => handleRemove(UploadFileType.LICENSE_NUMBER)"
  156. :on-success="(res: any, file: UploadFile) => handleSuccess(res, UploadFileType.LICENSE_NUMBER, file)">
  157. <el-button :disabled="state.dialog.type === 'view'" type="primary">点击上传</el-button>
  158. <div class="el-upload__tip ml10">支持格式:jpg png pdf等,单个文件不超过20MB</div>
  159. </el-upload>
  160. </el-form-item>
  161. </el-col>
  162. </el-row>
  163. <el-row class="mt10" :gutter="20" v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
  164. <el-col :span="12" class="mb16">
  165. <el-form-item label="近三个月动物质量检测证明" prop="animalTestDateFile"
  166. :required="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
  167. <el-upload v-model:file-list="animalTestDateFileList" class="upload-demo" :action="uploadUrl" :limit="1"
  168. style="width: 100%" :before-upload="beforeAvatarFileUpload" :on-preview="handlePreview"
  169. :disabled="state.dialog.type === 'view'"
  170. :on-remove="() => handleRemove(UploadFileType.ANIMAL_TEST_DATE)"
  171. :on-success="(res: any, file: UploadFile) => handleSuccess(res, UploadFileType.ANIMAL_TEST_DATE, file)">
  172. <el-button :disabled="state.dialog.type === 'view'" type="primary">点击上传</el-button>
  173. <div class="el-upload__tip ml10">支持格式:jpg png pdf等,单个文件不超过20MB</div>
  174. </el-upload>
  175. </el-form-item>
  176. </el-col>
  177. </el-row>
  178. <el-row class="mt10" :gutter="20" v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
  179. <el-col :span="12" class="mb16">
  180. <el-form-item label="基因鉴定报告" prop="geneIdentificationFile">
  181. <el-upload v-model:file-list="geneIdentificationFileList" class="upload-demo" :action="uploadUrl"
  182. :limit="1" style="width: 100%" :before-upload="beforeAvatarFileUpload" :on-preview="handlePreview"
  183. :disabled="state.dialog.type === 'view'"
  184. :on-remove="() => handleRemove(UploadFileType.GENE_IDENTIFICATION_FILE)"
  185. :on-success="(res: any, file: UploadFile) => handleSuccess(res, UploadFileType.GENE_IDENTIFICATION_FILE, file)">
  186. <el-button :disabled="state.dialog.type === 'view'" type="primary">点击上传</el-button>
  187. <div class="el-upload__tip ml10">支持格式:jpg png pdf等,单个文件不超过20MB</div>
  188. </el-upload>
  189. </el-form-item>
  190. </el-col>
  191. </el-row>
  192. <LaText class="mb16 mt20" size="16" type="important" bold>特殊要求和附件</LaText>
  193. <el-row class="mt10" :gutter="20">
  194. <el-col :span="12" class="mb16">
  195. <el-form-item label="是否有特殊饲养要求" prop="hasFeedingSpecial">
  196. <el-radio-group :disabled="state.dialog.type === 'view'" v-model="state.form.hasFeedingSpecial">
  197. <el-radio :label="FeedingSpecial.HAVE_FEEDING_SPECIAL" size="large">有</el-radio>
  198. <el-radio :label="FeedingSpecial.NO_FEEDING_SPECIAL" size="large">无</el-radio>
  199. </el-radio-group>
  200. </el-form-item>
  201. </el-col>
  202. <el-col :span="12" class="mb16" v-if="state.form.hasFeedingSpecial === FeedingSpecial.HAVE_FEEDING_SPECIAL">
  203. <el-form-item label="特殊饲养要求" prop="feedingSpecialDesc" required>
  204. <el-input :disabled="state.dialog.type === 'view'" placeholder="输入特殊饲养要求,如每天更换垫料等"
  205. v-model="state.form.feedingSpecialDesc" />
  206. </el-form-item>
  207. </el-col>
  208. </el-row>
  209. <!-- <el-row class="mt10" :gutter="20">
  210. <el-col :span="12">
  211. <el-form-item label="笼位预约表" prop="cageAppointFile">
  212. <el-upload
  213. v-model:file-list="cageAppointFileList"
  214. class="upload-demo"
  215. :action="uploadUrl"
  216. :limit="1"
  217. style="width: 100%"
  218. :before-upload="beforeAvatarFileUpload"
  219. :on-preview="handlePreview"
  220. :disabled="state.dialog.type === 'view'"
  221. :on-remove="() => handleRemove(UploadFileType.CAGE_APPOINT_FILE)"
  222. :on-success="(res: any, file: UploadFile) => handleSuccess(res, UploadFileType.CAGE_APPOINT_FILE, file)"
  223. >
  224. <el-button :disabled="state.dialog.type === 'view'" type="primary">点击上传</el-button>
  225. <div class="el-upload__tip ml10">支持格式:jpg png pdf等,单个文件不超过20MB</div>
  226. </el-upload>
  227. </el-form-item>
  228. </el-col>
  229. </el-row> -->
  230. <el-row class="mt10" :gutter="20">
  231. <el-col :span="12" class="mb16">
  232. <el-form-item label="实验动物福利伦理审查申请表" prop="ethicsCheckFile" required>
  233. <el-upload v-model:file-list="ethicsCheckFileList" class="upload-demo" :action="uploadUrl" :limit="1"
  234. style="width: 100%" :before-upload="beforeAvatarFileUpload" :on-preview="handlePreview"
  235. :disabled="state.dialog.type === 'view'"
  236. :on-remove="() => handleRemove(UploadFileType.ETHICS_CHECK_FILE)"
  237. :on-success="(res: any, file: UploadFile) => handleSuccess(res, UploadFileType.ETHICS_CHECK_FILE, file)">
  238. <el-button :disabled="state.dialog.type === 'view'" type="primary">点击上传</el-button>
  239. <div class="el-upload__tip ml10">支持格式:jpg png pdf等,单个文件不超过20MB</div>
  240. </el-upload>
  241. </el-form-item>
  242. </el-col>
  243. </el-row>
  244. <el-row class="mt10" :gutter="20">
  245. <el-col :span="12" class="mb16">
  246. <el-form-item label="实验动物福利伦理审查意见表" prop="ethicsAdviceFile" required>
  247. <el-upload v-model:file-list="ethicsAdviceFileList" class="upload-demo" :action="uploadUrl" :limit="1"
  248. style="width: 100%" :before-upload="beforeAvatarFileUpload" :on-preview="handlePreview"
  249. :disabled="state.dialog.type === 'view'"
  250. :on-remove="() => handleRemove(UploadFileType.ETHICS_ADVICE_FILE)"
  251. :on-success="(res: any, file: UploadFile) => handleSuccess(res, UploadFileType.ETHICS_ADVICE_FILE, file)">
  252. <el-button :disabled="state.dialog.type === 'view'" type="primary">点击上传</el-button>
  253. <div class="el-upload__tip ml10">支持格式:jpg png pdf等,单个文件不超过20MB</div>
  254. </el-upload>
  255. </el-form-item>
  256. </el-col>
  257. </el-row>
  258. <el-row v-if="state.dialog.type === 'view'">
  259. <el-col :span="24" class="mb16">
  260. <LaText class="mb16 mt20" size="16" type="important" bold>审批流</LaText>
  261. <FlowTable :id="state.form.id" :businessCode="state.form.id + ''" defCode="plat_cage_applications" />
  262. </el-col>
  263. </el-row>
  264. <el-row class="mt10" :gutter="20">
  265. <el-col :span="24" class="mt30 mb16">
  266. <el-checkbox :disabled="state.dialog.type === 'view'" v-model="safePromiseStatus">
  267. {{ SafePromise }}
  268. </el-checkbox>
  269. </el-col>
  270. </el-row>
  271. </el-form>
  272. <template #footer>
  273. <span v-if="state.dialog.type === 'add'" class="dialog-footer">
  274. <el-button type="info" @click="onCancel" size="default">取 消</el-button>
  275. <el-button color="#2c78ff" @click="onSubmit()" size="default">提交</el-button>
  276. </span>
  277. </template>
  278. </el-dialog>
  279. </div>
  280. </template>
  281. <script setup lang="ts" name="systemProDialog">
  282. import { reactive, ref, computed, defineAsyncComponent, watch } from 'vue';
  283. import to from 'await-to-js';
  284. import { ElMessage } from 'element-plus';
  285. import { storeToRefs } from 'pinia';
  286. import dayjs from 'dayjs';
  287. import { UploadFile } from 'element-plus/es/components';
  288. import { usePlatAnimalCageApplicationApi } from 'labsop-api/src/api/platform/animal';
  289. import {
  290. LeavelList,
  291. ProcurementChannels,
  292. UploadFileType,
  293. FeedingSpecial,
  294. SafePromise,
  295. DateFormat,
  296. } from '/@/constants/pageConstants';
  297. import { deepClone } from '/@/utils/other';
  298. import { useUserInfo } from '/@/stores/userInfo';
  299. const uploadUrl = (import.meta as any).env.VITE_UPLOAD;
  300. const stores = useUserInfo();
  301. const { userInfos } = storeToRefs(stores);
  302. const FlowTable = defineAsyncComponent(() => import('/@/components/flow/flow-table.vue'));
  303. // 定义子组件向父组件传值/事件
  304. const emit = defineEmits(['refresh']);
  305. const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi();
  306. const expertDialogFormRef = ref();
  307. const projectGroupList = ref<any[]>([]);
  308. const projects = ref<any[]>([]);
  309. const animalNumber = computed(() => {
  310. const maleNumber = state.form.maleNumber || 0;
  311. const famaleNumber = state.form.famaleNumber || 0;
  312. return maleNumber + famaleNumber;
  313. });
  314. const animalTypeList = ref<any[]>([]);
  315. const licenseNumberFileList = ref<UploadFile[]>([]);
  316. const animalTestDateFileList = ref<UploadFile[]>([]);
  317. const geneIdentificationFileList = ref<UploadFile[]>([]);
  318. const cageAppointFileList = ref<UploadFile[]>([]);
  319. const ethicsCheckFileList = ref<UploadFile[]>([]);
  320. const ethicsAdviceFileList = ref<UploadFile[]>([]);
  321. const safePromiseStatus = ref<boolean>(false);
  322. const rules = {
  323. projectGroupId: { required: true, message: '不能为空', trigger: 'change' },
  324. categoryName: { required: true, message: '不能为空', trigger: 'change' },
  325. number: { required: true, message: '不能为空', trigger: 'change' },
  326. startDate: { required: true, message: '不能为空', trigger: 'change' },
  327. comeTime: {
  328. validator: (rule: any, value: any, callback: any) => {
  329. // 只有在自行购买时才验证动物到达时间
  330. if (state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF && (!value || value === '')) {
  331. callback(new Error('动物到达时间不能为空'));
  332. } else {
  333. callback();
  334. }
  335. },
  336. trigger: 'change',
  337. },
  338. // maleNumber: { required: true, message: '不能为空', trigger: 'change' },
  339. // weight: { required: true, message: '不能为空', trigger: 'change' },
  340. buyFrom: { required: true, message: '不能为空', trigger: 'change' },
  341. comeFromUnit: { required: true, message: '不能为空', trigger: 'change' },
  342. variety: { required: true, message: '不能为空', trigger: 'change' },
  343. categoryId: { required: true, message: '不能为空', trigger: 'change' },
  344. feedingDay: { required: true, message: '不能为空', trigger: 'change' },
  345. level: { required: true, message: '不能为空', trigger: 'change' },
  346. // feedingSpecialDesc: { required: true, message: '不能为空', trigger: 'change' },
  347. // ethicsCheckFile: { required: true, message: '不能为空', trigger: 'change' },
  348. // ethicsAdviceFile: { required: true, message: '不能为空', trigger: 'change' },
  349. licenseNumberFile: {
  350. validator: (rule: any, value: any, callback: any) => {
  351. // 只有在动物房代购时才验证这些字段
  352. if (state.form.buyFrom === ProcurementChannels.PURCHASED_BY_OTHERS && (!value || value.length === 0)) {
  353. callback(new Error('生产许可证副本不能为空'));
  354. } else {
  355. callback();
  356. }
  357. },
  358. trigger: 'change',
  359. },
  360. animalTestDateFile: {
  361. validator: (rule: any, value: any, callback: any) => {
  362. // 只有在动物房代购时才验证这些字段
  363. if (state.form.buyFrom === ProcurementChannels.PURCHASED_BY_OTHERS && (!value || value.length === 0)) {
  364. callback(new Error('动物质检证明不能为空'));
  365. } else {
  366. callback();
  367. }
  368. },
  369. trigger: 'change',
  370. },
  371. ethicsCheckFile: {
  372. validator: (rule: any, value: any, callback: any) => {
  373. if (!value || value.length === 0) {
  374. callback(new Error('实验动物福利伦理审查申请表不能为空'));
  375. } else {
  376. callback();
  377. }
  378. },
  379. trigger: 'change',
  380. },
  381. ethicsAdviceFile: {
  382. validator: (rule: any, value: any, callback: any) => {
  383. if (!value || value.length === 0) {
  384. callback(new Error('实验动物福利伦理审查意见表不能为空'));
  385. } else {
  386. callback();
  387. }
  388. },
  389. trigger: 'change',
  390. },
  391. feedingSpecialDesc: {
  392. validator: (rule: any, value: any, callback: any) => {
  393. // 只有在有特殊饲养要求时才验证特殊饲养要求描述
  394. if (state.form.hasFeedingSpecial === FeedingSpecial.HAVE_FEEDING_SPECIAL && (!value || value.trim() === '')) {
  395. callback(new Error('特殊饲养要求不能为空'));
  396. } else {
  397. callback();
  398. }
  399. },
  400. trigger: 'change',
  401. },
  402. age: {
  403. validator: (rule: any, value: any, callback: any) => {
  404. if (!value || value.min === null || value.max === null) {
  405. callback(new Error('周龄范围不能为空'));
  406. } else if (value.min > value.max) {
  407. callback(new Error('最小周龄不能大于最大周龄'));
  408. } else {
  409. callback();
  410. }
  411. },
  412. trigger: 'change',
  413. },
  414. weight: {
  415. validator: (rule: any, value: any, callback: any) => {
  416. if (!value || value.min === null || value.max === null) {
  417. callback(new Error('体重范围不能为空'));
  418. } else if (value.min > value.max) {
  419. callback(new Error('最小体重不能大于最大体重'));
  420. } else {
  421. callback();
  422. }
  423. },
  424. trigger: 'change',
  425. },
  426. };
  427. const defaultFormFields = {
  428. id: 0,
  429. projectGroupName: '',
  430. projectGroupId: null,
  431. categoryName: '',
  432. categoryId: null,
  433. level: null,
  434. number: 1,
  435. startDate: '',
  436. maleNumber: 0,
  437. famaleNumber: 0,
  438. totalNumber: 0,
  439. weight: { min: 0, max: 0 },
  440. age: { min: 0, max: 0 },
  441. feedingDay: 0,
  442. userName: '',
  443. deptId: null,
  444. deptName: '',
  445. phone: '',
  446. buyFrom: ProcurementChannels.PURCHASED_BY_OTHERS,
  447. comeTime: '',
  448. comeFromUnit: '',
  449. licenseNumberFile: [] as { name: string; url: string }[],
  450. animalTestDateFile: [] as { name: string; url: string }[],
  451. geneIdentificationFile: [] as { name: string; url: string }[],
  452. hasFeedingSpecial: FeedingSpecial.HAVE_FEEDING_SPECIAL,
  453. feedingSpecialDesc: '',
  454. cageAppointFile: [] as { name: string; url: string }[],
  455. ethicsCheckFile: [] as { name: string; url: string }[],
  456. ethicsAdviceFile: [] as { name: string; url: string }[],
  457. };
  458. const state = reactive({
  459. form: defaultFormFields,
  460. safePromise: false,
  461. safeRead: false,
  462. dialog: {
  463. isShowDialog: false,
  464. type: '',
  465. title: '',
  466. submitTxt: '',
  467. },
  468. });
  469. watch(
  470. () => [state.form.maleNumber, state.form.famaleNumber],
  471. ([maleNumber, famaleNumber]) => {
  472. state.form.totalNumber = (maleNumber || 0) + (famaleNumber || 0);
  473. },
  474. { immediate: true }
  475. );
  476. const getDicts = () => {
  477. Promise.all([
  478. platAnimalCageApplicationApi.getAnimalTypeList({}),
  479. platAnimalCageApplicationApi.getProjectGroup({}),
  480. ]).then(([animalType, projectGroup]) => {
  481. animalTypeList.value = animalType.data;
  482. if (projectGroup && projectGroup.data) {
  483. projectGroupList.value = projectGroup.data;
  484. const currentProject = projectGroup.data[0]?.projects;
  485. if (currentProject) {
  486. projects.value = currentProject;
  487. }
  488. }
  489. });
  490. };
  491. const isValidJsonArray = (str: string) => {
  492. try {
  493. const parsed = JSON.parse(str);
  494. return Array.isArray(parsed);
  495. } catch (e) {
  496. return false;
  497. }
  498. };
  499. const parseRangeField = (str: string) => {
  500. try {
  501. if (!str) return { min: 0, max: 0 };
  502. // 处理双重转义的情况
  503. const cleanStr = str.replace(/\\"/g, '"').replace(/^"|"$/g, '');
  504. const parsed = JSON.parse(cleanStr);
  505. return {
  506. min: parsed.min || 0,
  507. max: parsed.max || 0
  508. };
  509. } catch (e) {
  510. console.warn('解析范围字段失败:', str, e);
  511. return { min: 0, max: 0 };
  512. }
  513. };
  514. // 打开弹窗
  515. const openDialog = async (type: 'add' | 'view', row?: any) => {
  516. await getDicts();
  517. state.dialog.type = type;
  518. state.dialog.title = '笼位申请';
  519. if (type === 'view' && row) {
  520. // 处理范围字段的JSON字符串
  521. const processedRow = {
  522. ...row,
  523. age: parseRangeField(row.age),
  524. weight: parseRangeField(row.weight)
  525. };
  526. state.form = processedRow;
  527. licenseNumberFileList.value = isValidJsonArray(row.licenseNumberFile) ? JSON.parse(row.licenseNumberFile) : [];
  528. animalTestDateFileList.value = isValidJsonArray(row.animalTestDateFile) ? JSON.parse(row.animalTestDateFile) : [];
  529. geneIdentificationFileList.value = isValidJsonArray(row.geneIdentificationFile)
  530. ? JSON.parse(row.geneIdentificationFile)
  531. : [];
  532. cageAppointFileList.value = isValidJsonArray(row.cageAppointFile) ? JSON.parse(row.cageAppointFile) : [];
  533. ethicsCheckFileList.value = isValidJsonArray(row.ethicsCheckFile) ? JSON.parse(row.ethicsCheckFile) : [];
  534. ethicsAdviceFileList.value = isValidJsonArray(row.ethicsAdviceFile) ? JSON.parse(row.ethicsAdviceFile) : [];
  535. safePromiseStatus.value = true;
  536. } else if (type === 'add') {
  537. // 回显并写入当前人的部门与联系方式,用于提交
  538. state.form.deptId = userInfos.value?.deptId || null;
  539. state.form.deptName = userInfos.value?.deptName || '';
  540. state.form.phone = userInfos.value?.phone || '';
  541. }
  542. state.dialog.isShowDialog = true;
  543. };
  544. // 关闭弹窗
  545. const closeDialog = () => {
  546. state.form = defaultFormFields;
  547. licenseNumberFileList.value = [];
  548. animalTestDateFileList.value = [];
  549. geneIdentificationFileList.value = [];
  550. cageAppointFileList.value = [];
  551. ethicsCheckFileList.value = [];
  552. ethicsAdviceFileList.value = [];
  553. safePromiseStatus.value = false;
  554. state.dialog.isShowDialog = false;
  555. };
  556. // 取消
  557. const onCancel = () => {
  558. closeDialog();
  559. };
  560. const beforeAvatarFileUpload = (file: { size: number }) => {
  561. let isLt10m = file.size / 1024 / 1024 / 20 < 1;
  562. if (!isLt10m) {
  563. ElMessage.error('上传文件大小不能超过 20MB!');
  564. return false;
  565. }
  566. return true;
  567. };
  568. const handleRemove = (type: UploadFileType) => {
  569. if (type === UploadFileType.LICENSE_NUMBER) {
  570. licenseNumberFileList.value = [];
  571. state.form.licenseNumberFile = [];
  572. } else if (type === UploadFileType.ANIMAL_TEST_DATE) {
  573. animalTestDateFileList.value = [];
  574. state.form.animalTestDateFile = [];
  575. } else if (type === UploadFileType.GENE_IDENTIFICATION_FILE) {
  576. geneIdentificationFileList.value = [];
  577. state.form.geneIdentificationFile = [];
  578. } else if (type === UploadFileType.CAGE_APPOINT_FILE) {
  579. cageAppointFileList.value = [];
  580. state.form.cageAppointFile = [];
  581. } else if (type === UploadFileType.ETHICS_CHECK_FILE) {
  582. ethicsCheckFileList.value = [];
  583. state.form.ethicsCheckFile = [];
  584. } else if (type === UploadFileType.ETHICS_ADVICE_FILE) {
  585. ethicsAdviceFileList.value = [];
  586. state.form.ethicsAdviceFile = [];
  587. }
  588. };
  589. const handleSuccess = (res: { Data: string }, type: UploadFileType, file: UploadFile) => {
  590. console.log('ressss', res, file);
  591. if (type === UploadFileType.LICENSE_NUMBER) {
  592. state.form.licenseNumberFile = [{ name: file.name, url: res?.Data }];
  593. } else if (type === UploadFileType.ANIMAL_TEST_DATE) {
  594. state.form.animalTestDateFile = [{ name: file.name, url: res?.Data }];
  595. } else if (type === UploadFileType.GENE_IDENTIFICATION_FILE) {
  596. state.form.geneIdentificationFile = [{ name: file.name, url: res?.Data }];
  597. } else if (type === UploadFileType.CAGE_APPOINT_FILE) {
  598. state.form.cageAppointFile = [{ name: file.name, url: res?.Data }];
  599. } else if (type === UploadFileType.ETHICS_CHECK_FILE) {
  600. state.form.ethicsCheckFile = [{ name: file.name, url: res?.Data }];
  601. } else if (type === UploadFileType.ETHICS_ADVICE_FILE) {
  602. state.form.ethicsAdviceFile = [{ name: file.name, url: res?.Data }];
  603. }
  604. };
  605. const handlePreview = (file: UploadFile) => {
  606. if (file.url) {
  607. window.open(file.url, '_blank');
  608. }
  609. };
  610. // 提交
  611. const onSubmit = async () => {
  612. expertDialogFormRef.value.validate(async (valid: boolean) => {
  613. if (!valid) return;
  614. if (!safePromiseStatus.value) {
  615. ElMessage.error('请阅读并勾选安全承诺!');
  616. return;
  617. }
  618. if (!state.form.maleNumber && !state.form.famaleNumber) {
  619. ElMessage.error('请添加雄性或雌性数量!');
  620. return;
  621. }
  622. // json 字符串化
  623. state.form.age= JSON.stringify(state.form.age);
  624. state.form.weight = JSON.stringify(state.form.weight);
  625. const params = {
  626. ...deepClone(state.form),
  627. categoryId: state.form.categoryId != null ? String(state.form.categoryId) : null,
  628. categoryName: animalTypeList.value.find((item) => item.id == state.form.categoryId)?.name,
  629. projectGroupName: projects.value.find((item) => item.id == state.form.projectGroupId)?.projectName,
  630. startDate: dayjs(state.form.startDate).format(DateFormat),
  631. comeTime: state.form.comeTime ? dayjs(state.form.comeTime).format(DateFormat) : '',
  632. // 将范围对象转换为JSON字符串
  633. age: JSON.stringify(state.form.age),
  634. weight: JSON.stringify(state.form.weight),
  635. licenseNumberFile: JSON.stringify(state.form.licenseNumberFile),
  636. animalTestDateFile: JSON.stringify(state.form.animalTestDateFile),
  637. geneIdentificationFile: JSON.stringify(state.form.geneIdentificationFile),
  638. cageAppointFile: JSON.stringify(state.form.cageAppointFile),
  639. ethicsCheckFile: JSON.stringify(state.form.ethicsCheckFile),
  640. ethicsAdviceFile: JSON.stringify(state.form.ethicsAdviceFile),
  641. };
  642. Object.entries(params).forEach(([key, value]) => {
  643. if (value === '' || value === null) {
  644. delete params[key as keyof typeof params];
  645. }
  646. });
  647. const post = platAnimalCageApplicationApi.create;
  648. const [err]: ToResponse = await to(post(params));
  649. if (err) return;
  650. ElMessage.success('操作成功');
  651. closeDialog();
  652. emit('refresh');
  653. });
  654. };
  655. // 暴露变量
  656. defineExpose({
  657. openDialog,
  658. });
  659. </script>
  660. <style lang="scss" scoped>
  661. .application-dialog-container {
  662. .el-select {
  663. width: 100%;
  664. }
  665. }
  666. h4 {
  667. font-size: 18px;
  668. }
  669. ul {
  670. padding-left: 20px;
  671. }
  672. .text {
  673. p {
  674. text-indent: 2em;
  675. }
  676. }
  677. .el-upload+.el-button {
  678. vertical-align: top;
  679. }
  680. :deep(.el-checkbox) {
  681. white-space: pre-wrap;
  682. }
  683. </style>