Detail.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. <template>
  2. <div class="detail-drawer-container">
  3. <van-popup v-model:show="props.showDialog" position="right"
  4. :style="{ width: '100vw', maxWidth: '460px', height: '100vh' }" :closeable="true" @close="closeDialog"
  5. :close-on-click-overlay="false">
  6. <div class="drawer-wrapper">
  7. <header class="drawer-header">
  8. <h3>详情</h3>
  9. </header>
  10. <div class="drawer-body">
  11. <van-cell-group inset>
  12. <van-field v-model="state.form.projectGroupName" label="课题名称" readonly placeholder="请输入课题名称" />
  13. <van-field v-model="state.form.userName" label="姓名" readonly placeholder="请输入姓名" />
  14. <van-field v-model="state.form.deptName" label="部门" readonly />
  15. <van-field v-model="state.form.phone" label="联系方式" readonly />
  16. </van-cell-group>
  17. <h4 class="section-title">实验动物笼位预约信息</h4>
  18. <van-cell-group inset>
  19. <van-field v-model="state.form.number" label="笼位数量" readonly type="digit" />
  20. <van-field v-model="state.form.startDate" label="开始使用时间" readonly placeholder="请开始使用时间" />
  21. <van-field v-model="state.form.categoryName" label="动物类别" readonly placeholder="请选择" />
  22. <van-field v-model="state.form.variety" label="品种品系" readonly placeholder="请选择" />
  23. <van-field v-model="levelName" label="饲养区域" readonly placeholder="请选择" />
  24. <van-field :modelValue="state.form.age.min === state.form.age.max ? state.form.age.min : `${state.form.age.min} - ${state.form.age.max}`" label="周龄" readonly />
  25. <van-field :modelValue="state.form.age.min === state.form.age.max ? state.form.age.min : `${state.form.age.min} - ${state.form.age.max}`" label="体重" readonly />
  26. <van-field v-model="state.form.maleNumber" label="雄性" readonly type="digit" />
  27. <van-field v-model="state.form.famaleNumber" label="雌性" readonly type="digit" />
  28. <van-field v-model="state.form.totalNumber" label="合计" readonly type="digit" />
  29. <van-field v-model="state.form.feedingDay" label="饲养总天数" readonly type="digit" />
  30. </van-cell-group>
  31. <h4 class="section-title">采购渠道</h4>
  32. <van-cell-group inset>
  33. <van-cell title="采购渠道">
  34. <template #value>
  35. <van-radio-group v-model="state.form.buyFrom" direction="horizontal" disabled>
  36. <van-radio :name="ProcurementChannels.PURCHASED_BY_OTHERS">动物房代购</van-radio>
  37. <van-radio :name="ProcurementChannels.PURCHASED_BY_MYSELF">自行购买</van-radio>
  38. </van-radio-group>
  39. </template>
  40. </van-cell>
  41. <van-field v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF"
  42. v-model="state.form.comeFromUnit" label="外购来源单位" readonly />
  43. <van-field v-model="state.form.comeTime" label="动物到达时间" readonly placeholder="请选择到达时间" />
  44. </van-cell-group>
  45. <h4 class="section-title">特殊要求和附件</h4>
  46. <van-cell-group inset>
  47. <van-cell title="是否有特殊饲养要求">
  48. <template #value>
  49. <van-radio-group v-model="state.form.hasFeedingSpecial" direction="horizontal" disabled>
  50. <van-radio :name="FeedingSpecial.HAVE_FEEDING_SPECIAL">有</van-radio>
  51. <van-radio :name="FeedingSpecial.NO_FEEDING_SPECIAL">无</van-radio>
  52. </van-radio-group>
  53. </template>
  54. </van-cell>
  55. <van-field v-model="state.form.feedingSpecialDesc" label="特殊饲养要求" readonly type="textarea" rows="3"
  56. placeholder="请输入特殊饲养要求" />
  57. </van-cell-group>
  58. <!-- 附件列表 -->
  59. <van-cell-group v-if="state.form.licenseNumberFile && JSON.stringify(state.form.licenseNumberFile) !== '[]'"
  60. inset class="mt10">
  61. <van-cell title="生产许可证">
  62. <template #value>
  63. <div class="file-links">
  64. <a v-for="item in state.form.licenseNumberFile" :key="item.url" :href="item.url" target="_blank"
  65. class="file-link">
  66. {{ item.name }}
  67. </a>
  68. </div>
  69. </template>
  70. </van-cell>
  71. </van-cell-group>
  72. <van-cell-group v-if="state.form.animalTestDateFile && JSON.stringify(state.form.animalTestDateFile) !== '[]'"
  73. inset class="mt10">
  74. <van-cell title="近三个月动物质量检测证明">
  75. <template #value>
  76. <div class="file-links">
  77. <a v-for="item in state.form.animalTestDateFile" :key="item.url" :href="item.url" target="_blank"
  78. class="file-link">
  79. {{ item.name }}
  80. </a>
  81. </div>
  82. </template>
  83. </van-cell>
  84. </van-cell-group>
  85. <van-cell-group
  86. v-if="state.form.geneIdentificationFile && JSON.stringify(state.form.geneIdentificationFile) !== '[]'" inset
  87. class="mt10">
  88. <van-cell title="基因鉴定报告">
  89. <template #value>
  90. <div class="file-links">
  91. <a v-for="item in state.form.geneIdentificationFile" :key="item.url" :href="item.url" target="_blank"
  92. class="file-link">
  93. {{ item.name }}
  94. </a>
  95. </div>
  96. </template>
  97. </van-cell>
  98. </van-cell-group>
  99. <van-cell-group v-if="state.form.envTestDateFile && JSON.stringify(state.form.envTestDateFile) !== '[]'" inset
  100. class="mt10">
  101. <van-cell title="近三个月饲养环境检测证明">
  102. <template #value>
  103. <div class="file-links">
  104. <a v-for="item in state.form.envTestDateFile" :key="item.url" :href="item.url" target="_blank"
  105. class="file-link">
  106. {{ item.name }}
  107. </a>
  108. </div>
  109. </template>
  110. </van-cell>
  111. </van-cell-group>
  112. <van-cell-group v-if="state.form.cageAppointFile && JSON.stringify(state.form.cageAppointFile) !== '[]'" inset
  113. class="mt10">
  114. <van-cell title="笼位预约表">
  115. <template #value>
  116. <div class="file-links">
  117. <a v-for="item in state.form.cageAppointFile" :key="item.url" :href="item.url" target="_blank"
  118. class="file-link">
  119. {{ item.name }}
  120. </a>
  121. </div>
  122. </template>
  123. </van-cell>
  124. </van-cell-group>
  125. <van-cell-group v-if="state.form.ethicsCheckFile && JSON.stringify(state.form.ethicsCheckFile) !== '[]'" inset
  126. class="mt10">
  127. <van-cell title="实验动物福利伦理审查申请表">
  128. <template #value>
  129. <div class="file-links">
  130. <a v-for="item in state.form.ethicsCheckFile" :key="item.url" :href="item.url" target="_blank"
  131. class="file-link">
  132. {{ item.name }}
  133. </a>
  134. </div>
  135. </template>
  136. </van-cell>
  137. </van-cell-group>
  138. <van-cell-group v-if="state.form.ethicsAdviceFile && JSON.stringify(state.form.ethicsAdviceFile) !== '[]'"
  139. inset class="mt10">
  140. <van-cell title="实验动物福利伦理审查意见表">
  141. <template #value>
  142. <div class="file-links">
  143. <a v-for="item in state.form.ethicsAdviceFile" :key="item.url" :href="item.url" target="_blank"
  144. class="file-link">
  145. {{ item.name }}
  146. </a>
  147. </div>
  148. </template>
  149. </van-cell>
  150. </van-cell-group>
  151. <div class="card mt10">
  152. <h4 class="section-title">审批记录</h4>
  153. <FlowTable :id="state.form.id" :businessCode="`${state.form.id}`" defCode="plat_cage_applications" />
  154. </div>
  155. </div>
  156. </div>
  157. </van-popup>
  158. </div>
  159. </template>
  160. <script setup lang="ts" name="systemProDialog">
  161. import to from 'await-to-js'
  162. import { nextTick, reactive, ref, defineAsyncComponent, watch, computed } from 'vue'
  163. import dayjs from 'dayjs'
  164. import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
  165. import { ApproveStatusList, ProcurementChannels, FeedingSpecial, LeavelList } from '/@/constants/pageConstants'
  166. const FlowTable = defineAsyncComponent(() => import('/@/components/FlowTable.vue'))
  167. // 定义子组件向父组件传值/事件
  168. const props = defineProps({
  169. code: { type: String, default: '' },
  170. showDialog: { type: Boolean, default: false },
  171. isReturnCageList: { type: Boolean, default: false },
  172. })
  173. const emit = defineEmits(['close'])
  174. const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
  175. const expertDialogFormRef = ref()
  176. const animalTypeList = ref<any[]>([])
  177. // 解析范围字段
  178. const parseRangeField = (str: string) => {
  179. try {
  180. if (!str) return { min: 0, max: 0 };
  181. // 处理双重转义的情况
  182. const cleanStr = str.replace(/\\"/g, '"').replace(/^"|"$/g, '');
  183. const parsed = JSON.parse(cleanStr);
  184. return {
  185. min: parsed.min || 0,
  186. max: parsed.max || 0
  187. };
  188. } catch (e) {
  189. console.warn('解析范围字段失败:', str, e);
  190. return { min: 0, max: 0 };
  191. }
  192. };
  193. const categoryName = computed(() => {
  194. if (state.form.categoryId) {
  195. const category = animalTypeList.value.find((item) => item.id === state.form.categoryId)
  196. return category ? category.name : ''
  197. }
  198. return ''
  199. })
  200. const levelName = computed(() => {
  201. if (state.form.level) {
  202. const level = LeavelList.find((item) => item.id === state.form.level)
  203. return level ? level.name : ''
  204. }
  205. return ''
  206. })
  207. const state = reactive({
  208. form: {
  209. id: 0,
  210. userName: '',
  211. number: 0,
  212. approveStatus: '',
  213. categoryName: '',
  214. variety: '',
  215. projectGroupName: '',
  216. createdTime: '',
  217. returnNumber: 0,
  218. maleNumber: 0,
  219. famaleNumber: 0,
  220. weight: { min: null, max: null },
  221. age: { min: null, max: null },
  222. feedingDay: 0,
  223. buyFrom: ProcurementChannels.PURCHASED_BY_OTHERS,
  224. comeTime: '',
  225. comeFromUnit: '',
  226. licenseNumberFile: [],
  227. animalTestDateFile: [],
  228. envTestDateFile: [],
  229. hasFeedingSpecial: FeedingSpecial.HAVE_FEEDING_SPECIAL,
  230. feedingSpecialDesc: '',
  231. cageAppointFile: [],
  232. ethicsCheckFile: [],
  233. ethicsAdviceFile: [],
  234. geneIdentificationFile: [],
  235. deptName: '',
  236. phone: '',
  237. categoryId: null,
  238. level: null,
  239. startDate: '',
  240. totalNumber: 0,
  241. },
  242. disabled: false,
  243. })
  244. const getDicts = () => {
  245. Promise.all([platAnimalCageApplicationApi.getAnimalTypeList({})]).then(([animalType]) => {
  246. animalTypeList.value = animalType.data
  247. })
  248. }
  249. // 打开弹窗
  250. const initForm = async (code: string) => {
  251. await getDicts()
  252. const [err, res]: ToResponse = await to(platAnimalCageApplicationApi.getEntityById({ id: parseInt(code) }))
  253. if (err) return
  254. await nextTick()
  255. // 处理范围字段的JSON字符串
  256. const processedRow = {
  257. ...res?.data,
  258. age: parseRangeField(res?.data?.age),
  259. weight: parseRangeField(res?.data?.weight)
  260. };
  261. state.form = {
  262. ...processedRow,
  263. approveStatus: ApproveStatusList.find((item) => item.id == res?.data?.approveStatus)?.name,
  264. createdTime: dayjs(res?.data?.createdTime).format('YYYY-MM-DD'),
  265. licenseNumberFile: res?.data?.licenseNumberFile ? JSON.parse(res?.data?.licenseNumberFile) : [],
  266. animalTestDateFile: res?.data?.animalTestDateFile ? JSON.parse(res?.data?.animalTestDateFile) : [],
  267. envTestDateFile: res?.data?.envTestDateFile ? JSON.parse(res?.data?.envTestDateFile) : [],
  268. cageAppointFile: res?.data?.cageAppointFile ? JSON.parse(res?.data?.cageAppointFile) : [],
  269. ethicsCheckFile: res?.data?.ethicsCheckFile ? JSON.parse(res?.data?.ethicsCheckFile) : [],
  270. ethicsAdviceFile: res?.data?.ethicsAdviceFile ? JSON.parse(res?.data?.ethicsAdviceFile) : [],
  271. geneIdentificationFile: res?.data?.geneIdentificationFile ? JSON.parse(res?.data?.geneIdentificationFile) : [],
  272. }
  273. }
  274. const closeDialog = () => {
  275. emit('close')
  276. if (expertDialogFormRef.value && expertDialogFormRef.value.resetFields) {
  277. expertDialogFormRef.value.resetFields()
  278. }
  279. }
  280. // 暴露变量
  281. defineExpose({
  282. initForm,
  283. })
  284. watch(
  285. () => [state.form.maleNumber, state.form.famaleNumber],
  286. ([maleNumber, famaleNumber]) => {
  287. state.form.totalNumber = (maleNumber || 0) + (famaleNumber || 0)
  288. },
  289. { immediate: true },
  290. )
  291. </script>
  292. <style lang="scss" scoped>
  293. .detail-drawer-container {
  294. .drawer-wrapper {
  295. display: flex;
  296. flex-direction: column;
  297. height: 100vh;
  298. }
  299. .drawer-header {
  300. padding: 16px;
  301. border-bottom: 1px solid #f0f0f0;
  302. h3 {
  303. margin: 0;
  304. font-size: 18px;
  305. font-weight: 600;
  306. text-align: center;
  307. }
  308. }
  309. .drawer-body {
  310. flex: 1;
  311. overflow-y: auto;
  312. padding: 16px 12px 80px;
  313. -webkit-overflow-scrolling: touch;
  314. }
  315. .section-title {
  316. font-size: 16px;
  317. font-weight: 600;
  318. margin: 20px 0 8px;
  319. padding-left: 8px;
  320. position: relative;
  321. &::before {
  322. content: '';
  323. position: absolute;
  324. left: 0;
  325. top: 50%;
  326. transform: translateY(-50%);
  327. width: 3px;
  328. height: 16px;
  329. background-color: #1c9bfd;
  330. }
  331. }
  332. .file-links {
  333. display: flex;
  334. flex-direction: column;
  335. gap: 8px;
  336. }
  337. .file-link {
  338. color: #1989fa;
  339. text-decoration: none;
  340. word-break: break-all;
  341. &:hover {
  342. text-decoration: underline;
  343. }
  344. }
  345. .card {
  346. background-color: #fff;
  347. border-radius: 8px;
  348. margin-top: 16px;
  349. }
  350. .mt10 {
  351. margin-top: 10px;
  352. }
  353. }
  354. :deep(.van-field__label) {
  355. width: 120px;
  356. }
  357. :deep(.van-cell__title) {
  358. flex: 0 0 120px;
  359. }
  360. </style>