index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. <template>
  2. <div class="entry-container">
  3. <div
  4. class="search-wrap"
  5. ref="searchWrapRef"
  6. >
  7. <el-form
  8. :model="state.queryParams"
  9. ref="queryRef"
  10. >
  11. <el-form-item prop="serialNo">
  12. <el-select
  13. v-model="state.queryParams.approveStatus"
  14. style="width: 100%"
  15. placeholder="审批状态"
  16. clearable
  17. @change="search"
  18. >
  19. <el-option
  20. v-for="item in ApproveStatusList"
  21. :key="item.id"
  22. :label="item.name"
  23. :value="item.id"
  24. ></el-option>
  25. </el-select>
  26. </el-form-item>
  27. <el-form-item prop="dateRange">
  28. <el-input
  29. v-model="dateRangeText"
  30. placeholder="请选择申请时间范围"
  31. readonly
  32. style="width: 100%"
  33. @click="showCalendar = true"
  34. >
  35. <template #suffix>
  36. <el-icon
  37. v-if="dateRangeText"
  38. style="cursor: pointer; margin-right: 8px"
  39. @click.stop="clearDateRange"
  40. >
  41. <Close />
  42. </el-icon>
  43. <el-icon
  44. style="cursor: pointer"
  45. @click.stop="showCalendar = true"
  46. >
  47. <Calendar />
  48. </el-icon>
  49. </template>
  50. </el-input>
  51. </el-form-item>
  52. <!-- <el-form-item prop="serialNo">
  53. <el-select
  54. v-model="state.queryParams.isMyself"
  55. placeholder="申请人"
  56. clearable
  57. @change="search"
  58. >
  59. <el-option
  60. label="我申请的"
  61. :value="1"
  62. ></el-option>
  63. <el-option
  64. label="全部"
  65. :value="0"
  66. ></el-option>
  67. </el-select>
  68. </el-form-item> -->
  69. </el-form>
  70. <!-- <div style="text-align: right">
  71. <el-button
  72. @click="handleExport"
  73. style="height: 25px"
  74. type="primary"
  75. >
  76. 导出
  77. </el-button>
  78. </div> -->
  79. </div>
  80. <div class="list-container">
  81. <van-list
  82. v-model:loading="state.loading"
  83. :finished="state.finished"
  84. finished-text="没有更多了"
  85. @load="onLoad"
  86. >
  87. <van-cell
  88. v-for="item in state.list"
  89. :key="item"
  90. @click="handleCheckDetail(item)"
  91. >
  92. <template #default>
  93. <div class="list">
  94. <header class="flex justify-between">
  95. <strong class="title">{{ `${item.userName}的笼位申请` }}</strong>
  96. <van-tag
  97. v-if="item.approveStatus == ApproveStatus.WAIT_SUBMIT"
  98. type="warning"
  99. >
  100. 待提交
  101. </van-tag>
  102. <van-tag
  103. v-else-if="item.approveStatus == ApproveStatus.APPROVING"
  104. type="primary"
  105. >
  106. 审核中
  107. </van-tag>
  108. <van-tag
  109. v-else-if="item.approveStatus == ApproveStatus.PASS"
  110. type="success"
  111. >
  112. 通过
  113. </van-tag>
  114. <van-tag
  115. v-else-if="item.approveStatus == ApproveStatus.REVOKE"
  116. type="success"
  117. >
  118. 撤销
  119. </van-tag>
  120. <van-tag
  121. v-else-if="item.approveStatus == ApproveStatus.REFUSE"
  122. type="danger"
  123. >
  124. 拒绝
  125. </van-tag>
  126. </header>
  127. <p class="inst-title">
  128. <span>课题名称</span>
  129. <span class="title ml8">{{ item.projectGroupName }}</span>
  130. </p>
  131. <p class="inst-title">
  132. <span>申请人</span>
  133. <span class="title ml8">
  134. {{ item.userName }}
  135. </span>
  136. </p>
  137. <p class="inst-title">
  138. <span>申请时间</span>
  139. <span class="title ml8">
  140. {{ formatToChineseDate(item.createdTime) }}
  141. </span>
  142. </p>
  143. <p class="inst-title">
  144. <span>开始时间</span>
  145. <span class="title ml8">
  146. {{ formatToChineseDate(item.startDate) }}
  147. </span>
  148. </p>
  149. <p class="inst-title">
  150. <span>申请笼位(个)</span>
  151. <span class="title ml8">
  152. {{ item.number }}
  153. </span>
  154. </p>
  155. <p class="inst-title">
  156. <span>退还笼位(个)</span>
  157. <span class="title ml8">
  158. {{ item.returnNumber }}
  159. </span>
  160. </p>
  161. <p class="inst-title">
  162. <span>申请状态</span>
  163. <span class="title ml8">
  164. {{ formatApproveStatus(Number(item.approveStatus)) }}
  165. </span>
  166. </p>
  167. <p class="inst-title">
  168. <span>动物类别</span>
  169. <span class="title ml8">
  170. {{ item.categoryName }}
  171. </span>
  172. </p>
  173. <p class="inst-title">
  174. <span>品种品系</span>
  175. <span class="title ml8">
  176. {{ item.variety }}
  177. </span>
  178. </p>
  179. <p class="inst-title">
  180. <span>级别</span>
  181. <span class="title ml8">
  182. {{ LeavelList.find((leaveItem) => leaveItem.id === item.level)?.name || '' }}
  183. </span>
  184. </p>
  185. <footer class="flex justify-between mt16">
  186. <span class="title">
  187. <el-button
  188. v-if="
  189. item.approveStatus === ApproveStatus.PASS.toString() &&
  190. item.returnStatus !== ReturnStatus.COMPLETE.toString() &&
  191. userInfos.id === item.userId
  192. "
  193. style="height: 25px"
  194. type="primary"
  195. @click.stop="handleRefundable(item)"
  196. >
  197. 退还
  198. </el-button>
  199. </span>
  200. <span class="time">{{ formatDate(new Date(item.createdTime), 'YYYY-mm-dd') }}</span>
  201. </footer>
  202. </div>
  203. </template>
  204. </van-cell>
  205. </van-list>
  206. </div>
  207. <ApplicationModal
  208. ref="cageApplicationModalRef"
  209. @refresh="handleRefresh"
  210. />
  211. <DetailModal
  212. :showDialog="showDetailDialog"
  213. :isReturnCageList="false"
  214. ref="detailModalRef"
  215. @close="() => (showDetailDialog = false)"
  216. />
  217. <ReturnCageDialog
  218. ref="returnCageDialogRef"
  219. :currentRefundableItemNumber="currentRefundableItemNumber"
  220. :getTableData="handleRefresh"
  221. />
  222. <van-floating-bubble
  223. v-model:offset="offset"
  224. icon="plus"
  225. @click="handleApplication"
  226. axis="y"
  227. />
  228. <!-- 日期范围选择日历 -->
  229. <van-popup
  230. v-model:show="showCalendar"
  231. position="bottom"
  232. :style="{ height: '80vh' }"
  233. round
  234. >
  235. <van-calendar
  236. v-model:show="showCalendar"
  237. type="range"
  238. :min-date="new Date(1900, 0, 1)"
  239. @confirm="onDateRangeConfirm"
  240. />
  241. </van-popup>
  242. </div>
  243. </template>
  244. <script setup lang="ts">
  245. import { ref, reactive, onMounted, defineAsyncComponent, computed } from 'vue'
  246. import to from 'await-to-js'
  247. import dayjs from 'dayjs'
  248. import { useRouter, useRoute } from 'vue-router'
  249. import { Calendar, Close } from '@element-plus/icons-vue'
  250. import { formatDate } from '/@/utils/formatTime'
  251. import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
  252. import { ApproveStatus, ReturnStatus, LeavelList, ApproveStatusList } from '/@/constants/pageConstants'
  253. import { useUserInfos } from '/@/hooks/useUserInfos'
  254. const ApplicationModal = defineAsyncComponent(() => import('/@/view/animal/application/components/Application.vue'))
  255. const DetailModal = defineAsyncComponent(() => import('/@/view/animal/application/components/Detail.vue'))
  256. const ReturnCageDialog = defineAsyncComponent(
  257. () => import('/@/view/animal/application/components/ReturnCageDialog.vue'),
  258. )
  259. const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
  260. const { userInfos } = useUserInfos()
  261. const router = useRouter()
  262. const route = useRoute()
  263. const cageApplicationModalRef = ref()
  264. const detailModalRef = ref()
  265. const returnCageDialogRef = ref()
  266. const showDetailDialog = ref<boolean>(false)
  267. const dateTime = ref<any>([])
  268. const showCalendar = ref(false)
  269. const selectedDateRange = ref<[Date, Date] | null>(null)
  270. const offset = ref({ x: -80, y: 450 })
  271. const dateRangeText = computed(() => {
  272. if (selectedDateRange.value && selectedDateRange.value.length === 2) {
  273. const start = dayjs(selectedDateRange.value[0]).format('YYYY-MM-DD')
  274. const end = dayjs(selectedDateRange.value[1]).format('YYYY-MM-DD')
  275. return `${start} 至 ${end}`
  276. }
  277. return ''
  278. })
  279. const state = reactive({
  280. queryParams: {
  281. categoryId: null,
  282. categoryName: '',
  283. level: null,
  284. projectGroupName: '',
  285. pageNum: 1,
  286. pageSize: 20,
  287. userName: '',
  288. beginTime: '',
  289. endTime: '',
  290. approveStatus: '',
  291. isMyself: userInfos.value.userRoles === 'project_group_member' ? 1 : 0,
  292. },
  293. finished: false,
  294. loading: true,
  295. list: [] as any[],
  296. })
  297. const currentRefundableItemNumber = ref<number>(0)
  298. const resetQueryParams = () => {
  299. state.queryParams = {
  300. categoryId: null,
  301. categoryName: '',
  302. level: null,
  303. projectGroupName: '',
  304. pageNum: 1,
  305. pageSize: 20,
  306. userName: '',
  307. beginTime: '',
  308. endTime: '',
  309. approveStatus: '',
  310. isMyself: userInfos.value.userRoles === 'project_group_member' ? 1 : 0,
  311. }
  312. ;(state.finished = false), (state.loading = true), (state.list = [] as any[])
  313. }
  314. const setListPayload = (isExport?: boolean) => {
  315. const payload = {
  316. ...state.queryParams,
  317. pageSize: isExport ? 99999 : state.queryParams.pageSize,
  318. }
  319. if (dateTime.value && dateTime.value.length === 2 && dateTime.value[0]) {
  320. payload.beginTime = dayjs(dateTime.value[0]).format('YYYY-MM-DD') +' 00:00:00'
  321. }
  322. if (dateTime.value && dateTime.value.length === 2 && dateTime.value[1]) {
  323. payload.endTime = dayjs(dateTime.value[1]).format('YYYY-MM-DD') +' 23:59:59'
  324. }
  325. Object.entries(payload).forEach(([key, value]) => {
  326. if (value === '' || value === null) {
  327. delete payload[key as keyof typeof payload]
  328. }
  329. })
  330. return payload
  331. }
  332. const onDateRangeConfirm = (values: Date[]) => {
  333. if (values && values.length === 2) {
  334. selectedDateRange.value = [values[0], values[1]]
  335. dateTime.value = [
  336. dayjs(values[0]).format('YYYY-MM-DD'),
  337. dayjs(values[1]).format('YYYY-MM-DD')
  338. ]
  339. showCalendar.value = false
  340. search()
  341. }
  342. }
  343. const clearDateRange = () => {
  344. selectedDateRange.value = null
  345. dateTime.value = []
  346. search()
  347. }
  348. const formatToChineseDate = (dateStr: string) => {
  349. const date = new Date(dateStr)
  350. const year = date.getFullYear()
  351. const month = String(date.getMonth() + 1).padStart(2, '0')
  352. const day = String(date.getDate()).padStart(2, '0')
  353. return `${year}年${month}月${day}日`
  354. }
  355. const formatApproveStatus = (status: number) => {
  356. return ApproveStatusList.find((item) => item.id === status)?.name || ''
  357. }
  358. const onLoad = async (isSearch?: boolean) => {
  359. const [err, res]: ToResponse = await to(
  360. platAnimalCageApplicationApi.getList({
  361. ...setListPayload(),
  362. pageNum: isSearch ? 1 : state.queryParams.pageNum,
  363. }),
  364. )
  365. if (err) return
  366. if (res && res.data && res.data.list) {
  367. const list = res.data.list || []
  368. state.loading = false
  369. if (!isSearch) {
  370. for (const item of list) {
  371. state.list.push(item)
  372. }
  373. state.queryParams.pageNum++
  374. if (list.length < state.queryParams.pageSize) {
  375. state.finished = true
  376. }
  377. } else {
  378. state.list = list
  379. }
  380. }
  381. }
  382. const handleCheckDetail = (row: any) => {
  383. detailModalRef.value.initForm(row.id)
  384. showDetailDialog.value = true
  385. }
  386. const handleRefundable = (row: any) => {
  387. currentRefundableItemNumber.value = row.number
  388. returnCageDialogRef.value.handleOpenRefundableDialog(row.id)
  389. }
  390. const handleApplication = () => {
  391. cageApplicationModalRef.value.openDialog()
  392. }
  393. const handleRefresh = () => {
  394. resetQueryParams()
  395. onLoad()
  396. }
  397. const search = () => {
  398. onLoad(true)
  399. }
  400. const handleExport = async () => {
  401. const [err, res]: ToResponse = await to(
  402. platAnimalCageApplicationApi.getApplicationListExport({ ...setListPayload(true), base64Enable: 1 }),
  403. )
  404. if (err) return
  405. if (res && res.data && typeof res.data === 'string') {
  406. const link = document.createElement('a')
  407. link.href = `data:application/octet-stream;base64,${res.data}`
  408. link.download = `笼位申请_${dayjs(new Date()).format('YYYY-MM-DD')}.xlsx`
  409. link.style.display = 'none'
  410. document.body.appendChild(link)
  411. link.click()
  412. document.body.removeChild(link)
  413. }
  414. }
  415. onMounted(() => {
  416. const type = route.query.type
  417. if (type) {
  418. state.queryParams.approveStatus = type as string
  419. }
  420. onLoad()
  421. })
  422. </script>
  423. <style lang="scss" scoped>
  424. .entry-container {
  425. position: relative;
  426. display: flex;
  427. flex-direction: column;
  428. .search-wrap {
  429. background: #fff;
  430. margin-bottom: 10px;
  431. padding: 15px;
  432. }
  433. .list-container {
  434. overflow-y: auto;
  435. padding: 10px;
  436. border-radius: 4px;
  437. flex: 1;
  438. }
  439. .van-list {
  440. .van-cell {
  441. background-color: #fff;
  442. + .van-cell {
  443. margin-top: 10px;
  444. }
  445. header,
  446. footer {
  447. color: #333;
  448. }
  449. .title {
  450. flex: 1;
  451. white-space: nowrap;
  452. overflow: hidden;
  453. text-overflow: ellipsis;
  454. text-align: left;
  455. }
  456. .inst-title {
  457. color: #333;
  458. text-align: left;
  459. flex: 1;
  460. overflow: hidden;
  461. white-space: nowrap;
  462. text-overflow: ellipsis;
  463. margin-top: 4px;
  464. span:first-child {
  465. color: rgb(120, 120, 120);
  466. }
  467. }
  468. .time {
  469. color: #f69a4d;
  470. }
  471. }
  472. }
  473. }
  474. </style>