Explorar el Código

添加笼位申请以及笼位退还

Rick hace 7 meses
padre
commit
37c9bbaade

+ 62 - 0
src/api/platform/animal/index.ts

@@ -0,0 +1,62 @@
+/*
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2023-07-19 13:42:40
+ * @LastEditors: wanglj
+ * @LastEditTime: 2025-01-20 10:43:14
+ * @Description: file content
+ * @FilePath: \labsop_backup2\frontend\components\labsop-api\src\api\platform\home\index.ts
+ */
+import request from '/@/utils/micro_request.js';
+const basePath = import.meta.env.VITE_PLATFORM_API;
+export function usePlatAnimalCageApplicationApi() {
+	return {
+		// 创建
+		create(params?: Object) {
+			return request.postRequest(basePath, 'PlatAnimalCageApplication', 'Create', params);
+		},
+		// 列表
+		getList(params?: Object) {
+			return request.postRequest(basePath, 'PlatAnimalCageApplication', 'GetApplicationList', params);
+		},
+		// 详情
+		getEntityById(params?: Object) {
+			return request.postRequest(basePath, 'PlatAnimalCageApplication', 'GetApplicationDetail', params);
+		},
+		// 更新
+		updateById(params?: Object) {
+			return request.postRequest(basePath, 'PlatAnimalCageApplication', 'UpdateApplication', params);
+		},
+		// 动物类型列表
+		getAnimalTypeList(params?: Object) {
+			return request.postRequest(basePath, 'PlatAnimals', 'GetVendorAnimalCategoires', params);
+		},
+		// 获取课题名称列表
+		getProjectGroup(params?: Object) {
+			return request.postRequest(basePath, 'PlatProjProjectGroup', 'GetProjectGroup', params);
+		},
+		// 笼位退还
+		releaseCage(params?: Object) {
+			return request.postRequest(basePath, 'PlatAnimalCageApplication', 'ReleaseCage', params);
+		},
+		// 笼位退还列表
+		getCageReleaseList(params?: Object) {
+			return request.postRequest(basePath, 'PlatAnimalCageApplication', 'GetCageReleaseApplications', params);
+		},
+		// 获取动物管理三方token
+		getVendorToken(params?: Object) {
+			return request.postRequest(basePath, 'PlatAnimals', 'GetVendorToken', params);
+		},
+		// 导出笼位退还列表
+		getCageReleaseApplicationsExport(params?: Object) {
+			return request.postRequest(basePath, 'PlatAnimalCageApplication', 'GetCageReleaseApplicationsExport', params);
+		},
+		// 导出列表
+		getApplicationListExport(params?: Object) {
+			return request.postRequest(basePath, 'PlatAnimalCageApplication', 'GetApplicationListExport', params);
+		},
+		//笼位退还申请详情
+		getApplicationReleaseDetail(params?: Object) {
+			return request.postRequest(basePath, 'PlatAnimalCageApplication', 'GetCageReleaseApplicationDetail', params);
+		},
+	};
+}

+ 46 - 0
src/constants/pageConstants.ts

@@ -0,0 +1,46 @@
+// 笼位申请审批状态
+export enum ApproveStatus {
+	WAIT_SUBMIT = 10, // 待提交
+	APPROVING = 20, // 审核中
+	PASS = 30, // 通过
+	REVOKE = 35, // 撤销
+	REFUSE = 40, // 拒绝
+}
+
+// 笼位退还审批状态
+export enum ReturnStatus {
+	WAIT = 10,
+	ON_THE_WAY = 20,
+	COMPLETE = 30,
+}
+
+// 动物级别
+export const LeavelList = [
+	{ name: '普通级', id: 1 },
+	{ name: 'SPF级', id: 3 },
+	{ name: '无菌级', id: 4 },
+];
+
+// 笼位申请审批状态列表
+export const ApproveStatusList = [
+	{
+		name: '待提交',
+		id: ApproveStatus.WAIT_SUBMIT,
+	},
+	{
+		name: '审批中',
+		id: ApproveStatus.APPROVING,
+	},
+	{
+		name: '通过',
+		id: ApproveStatus.PASS,
+	},
+	{
+		name: '撤回',
+		id: ApproveStatus.REVOKE,
+	},
+	{
+		name: '拒绝',
+		id: ApproveStatus.REFUSE,
+	},
+];

+ 89 - 0
src/layout/animal.vue

@@ -0,0 +1,89 @@
+<!--
+ * @Author: wanglj wanglijie@dashoo.cn
+ * @Date: 2025-03-13 09:07:55
+ * @LastEditors: wanglj wanglijie@dashoo.cn
+ * @LastEditTime: 2025-03-20 16:38:29
+ * @FilePath: \labsop-h5\src\layout\index.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+  <router-view></router-view>
+  <van-tabbar route :placeholder="true" v-if="route.path !== '/entry/add'">
+    <van-tabbar-item replace to="/animal-application" icon="send-gift-o">笼位申请</van-tabbar-item>
+    <van-tabbar-item replace to="/animal-return" icon="send-gift-o">笼位退还</van-tabbar-item>
+  </van-tabbar>
+</template>
+
+<script lang="ts" setup>
+  import to from 'await-to-js'
+  import { showDialog } from 'vant'
+  import { ref } from 'vue'
+  import { useRouter, useRoute } from 'vue-router'
+  import { useUserInfo } from '/@/stores/userInfo'
+  import { Local } from '/@/utils/storage'
+
+  const active = ref(0)
+  const router = useRouter()
+  const route = useRoute()
+
+  const onRouterPush = (val: string) => {
+    router.push(val)
+  }
+  const scan = async () => {
+    const res = await useUserInfo().scanCode()
+    if (res) {
+      window.location.href = res as string
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .footer-nav {
+    height: 60px;
+    font-weight: bold;
+    ul {
+      height: 60px;
+      display: flex;
+      li {
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        font-size: 12px;
+        &.center-icon {
+          height: 60px;
+          flex: 0 0 60px;
+          padding: 10px;
+          align-self: flex-end;
+          i {
+            padding: 10px;
+            background: #1d66dc;
+            border-radius: 50%;
+          }
+        }
+        img {
+          height: 24px;
+          width: 24px;
+        }
+        .img-container {
+          padding: 6px;
+          height: 24px;
+          width: 24px;
+          background-color: #419fe5;
+          border-radius: 50%;
+          overflow: visible;
+        }
+        &.service .img-container {
+          background-color: #f59969;
+        }
+        &.todo .img-container {
+          background-color: #20b5ae;
+        }
+        &.user .img-container {
+          background-color: #595cac;
+        }
+      }
+    }
+  }
+</style>

+ 23 - 0
src/router.ts

@@ -233,6 +233,29 @@ const routes = [
       }
     ]
   },
+  {
+    path: '/',
+    redirect: '/login',
+    component: () => import('/@/layout/animal.vue'),
+    children: [
+      {
+        name: 'animalApplication',
+        path: '/animal-application',
+        component: () => import('/@/view/animal/application/index.vue'),
+        meta: {
+          title: '笼位申请'
+        }
+      },
+      {
+        name: 'animalReturn',
+        path: '/animal-return',
+        component: () => import('/@/view/animal/return/index.vue'),
+        meta: {
+          title: '笼位退还'
+        }
+      }
+    ]
+  },
   {
     path: '/',
     redirect: '/login',

+ 198 - 0
src/view/animal/application/components/Application.vue

@@ -0,0 +1,198 @@
+<template>
+	<div class="application-dialog-container">
+		<el-dialog :title="state.dialog.title" @close="onCancel" :close-on-click-modal="false" v-model="state.dialog.isShowDialog" width="100%">
+			<el-form ref="expertDialogFormRef" :model="state.form" :rules="rules" size="default" label-width="140px" label-position="top">
+				<h4 class="mb8 mt8">基本信息</h4>
+				<el-row :gutter="20">
+					<el-col :span="12">
+						<el-form-item label="课题名称" prop="projectGroupId">
+							<el-select v-model="state.form.projectGroupId" placeholder="请选择">
+								<el-option v-for="item in projects" :key="item.id" :label="item.projectName" :value="item.id" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="姓名" prop="group">
+							<el-input v-model="userInfos.userName" disabled />
+						</el-form-item>
+					</el-col>
+				</el-row>
+
+				<h4 class="mb8 mt10">实验动物笼位预约信息</h4>
+				<el-row class="mt10" :gutter="20">
+					<el-col :span="12">
+						<el-form-item label="笼位数量" prop="number">
+							<el-input-number v-model="state.form.number" style="width: 100%" :min="1" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="选择时间" prop="startDate">
+							<el-date-picker v-model="state.form.startDate" type="date" placeholder="请选择时间" clearable style="width: 100%" />
+						</el-form-item>
+					</el-col>
+				</el-row>
+
+				<el-row class="mt10" :gutter="20">
+					<el-col :span="12">
+						<el-form-item label="品种品系" prop="categoryId">
+							<el-select v-model="state.form.categoryId" placeholder="请选择">
+								<el-option v-for="item in animalTypeList" :key="item.id" :label="item.name" :value="item.id" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="级别" prop="level">
+							<el-select v-model="state.form.level" placeholder="请选择">
+								<el-option v-for="item in LeavelList" :key="item.id" :label="item.name" :value="item.id" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button type="info" @click="onCancel" size="default">取 消</el-button>
+					<el-button color="#2c78ff" @click="onSubmit()" size="default">提交</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script setup lang="ts" name="systemProDialog">
+import { reactive, ref } from 'vue';
+import to from 'await-to-js';
+import { ElMessage } from 'element-plus';
+
+import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+import { LeavelList } from '/@/constants/pageConstants';
+import { deepClone } from '/@/utils/other';
+import { useUserInfo } from '/@/stores/userInfo';
+import { storeToRefs } from 'pinia';
+import dayjs from 'dayjs';
+
+const stores = useUserInfo();
+const { userInfos } = storeToRefs(stores);
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['refresh']);
+
+const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi();
+
+const expertDialogFormRef = ref();
+const projectGroupList = ref<any[]>([]);
+const projects = ref<any[]>([]);
+
+const rules = {
+	projectGroupId: { required: true, message: '不能为空', trigger: 'change' },
+	categoryId: { required: true, message: '不能为空', trigger: 'change' },
+	number: { required: true, message: '不能为空', trigger: 'change' },
+	startDate: { required: true, message: '不能为空', trigger: 'change' },
+};
+
+const animalTypeList = ref<any[]>([]);
+const state = reactive({
+	form: {
+		projectGroupName: '',
+		projectGroupId: null,
+		categoryName: '',
+		categoryId: null,
+		level: null,
+		number: 1,
+		startDate: '',
+	},
+	safePromise: false,
+	safeRead: false,
+	dialog: {
+		isShowDialog: false,
+		type: '',
+		title: '',
+		submitTxt: '',
+	},
+});
+
+const getDicts = () => {
+	Promise.all([platAnimalCageApplicationApi.getAnimalTypeList({}), platAnimalCageApplicationApi.getProjectGroup({})]).then(
+		([animalType, projectGroup]) => {
+			animalTypeList.value = animalType.data;
+			if (projectGroup && projectGroup.data) {
+				projectGroupList.value = projectGroup.data;
+				const currentProject = projectGroup.data[0]?.projects;
+				if (currentProject) {
+					projects.value = currentProject;
+				}
+			}
+		}
+	);
+};
+// 打开弹窗
+const openDialog = async (type: 'add' | 'edit') => {
+	getDicts();
+	state.dialog.type = type;
+	state.dialog.isShowDialog = true;
+};
+
+// 关闭弹窗
+const closeDialog = () => {
+	expertDialogFormRef.value.resetFields();
+	state.dialog.isShowDialog = false;
+};
+// 取消
+const onCancel = () => {
+	closeDialog();
+};
+
+// 提交
+const onSubmit = async () => {
+	expertDialogFormRef.value.validate(async (valid: boolean) => {
+		if (!valid) return;
+		const params = {
+			...deepClone(state.form),
+			categoryId: state.form.categoryId ? Number(state.form.categoryId) : null,
+			categoryName: animalTypeList.value.find((item) => item.id == state.form.categoryId)?.name,
+			projectGroupName: projects.value.find((item) => item.id == state.form.projectGroupId)?.projectName,
+			startDate: dayjs(state.form.startDate).format('YYYY-MM-DD'),
+		};
+
+		Object.entries(params).forEach(([key, value]) => {
+			if (value === '' || value === null) {
+				delete params[key as keyof typeof params];
+			}
+		});
+
+		const post = platAnimalCageApplicationApi.create;
+		const [err]: ToResponse = await to(post(params));
+		if (err) return;
+		ElMessage.success('操作成功');
+		closeDialog();
+		emit('refresh');
+	});
+};
+
+// 暴露变量
+defineExpose({
+	openDialog,
+});
+</script>
+<style lang="scss" scoped>
+.application-dialog-container {
+	.el-select {
+		width: 100%;
+	}
+}
+h4 {
+	font-size: 18px;
+}
+ul {
+	padding-left: 20px;
+}
+.text {
+	p {
+		text-indent: 2em;
+	}
+}
+.el-upload + .el-button {
+	vertical-align: top;
+}
+</style>

+ 125 - 0
src/view/animal/application/components/Detail.vue

@@ -0,0 +1,125 @@
+<template>
+	<div class="facilities-dialog-container">
+		<el-dialog title="详情" @close="closeDialog" :close-on-click-modal="false" v-model="props.showDialog" width="100%">
+			<el-form ref="expertDialogFormRef" disabled :model="state.form" size="default" label-width="140px" label-position="top">
+				<el-row :gutter="35">
+					<el-col :span="12" class="mb20">
+						<el-form-item label="申请人姓名" prop="memberName">
+							<el-input v-model="state.form.userName" disabled placeholder="请输入申请人姓名"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12" class="mb20">
+						<el-form-item label="课题名称" prop="memberType">
+							<el-input v-model="state.form.projectGroupName" disabled placeholder="请输入课题名称"></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+
+				<el-row :gutter="35">
+					<el-col :span="12" class="mb20">
+						<el-form-item label="申请时间" prop="memberPhone">
+							<el-input v-model="state.form.createdTime" disabled placeholder="请输入申请时间"></el-input>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12" class="mb20">
+						<el-form-item label="申请状态" prop="mentorName">
+							<el-input v-model="state.form.approveStatus" disabled placeholder="请输入申请状态"></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+
+				<el-row :gutter="35">
+					<el-col :span="12" class="mb20">
+						<el-form-item :label="props.isReturnCageList ? '退还笼位(个)' : '申请笼位(个)'" prop="mentorObj">
+							<el-input v-if="!props.isReturnCageList" v-model="state.form.number" disabled placeholder="请输入申请笼位"></el-input>
+							<el-input v-else v-model="state.form.returnNumber" disabled placeholder="请输入申请笼位"></el-input>
+						</el-form-item>
+					</el-col>
+
+					<el-col :span="12" class="mb20">
+						<el-form-item label="动物类型" prop="mentorDeptName">
+							<el-input v-model="state.form.categoryName" disabled placeholder="请输入动物类型"></el-input>
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+		</el-dialog>
+	</div>
+</template>
+
+<script setup lang="ts" name="systemProDialog">
+import to from 'await-to-js';
+import { nextTick, reactive, ref } from 'vue';
+import dayjs from 'dayjs';
+
+import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+import { ApproveStatusList } from '/@/constants/pageConstants';
+
+// 定义子组件向父组件传值/事件
+const props = defineProps({
+	code: { type: String, default: '' },
+	showDialog: { type: Boolean, default: false },
+	isReturnCageList: { type: Boolean, default: false },
+});
+const emit = defineEmits(['close']);
+
+const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi();
+
+const expertDialogFormRef = ref();
+
+const state = reactive({
+	form: {
+		userName: '',
+		number: 0,
+		approveStatus: '',
+		categoryName: '',
+		projectGroupName: '',
+		createdTime: '',
+		returnNumber: 0,
+	},
+	disabled: false,
+});
+
+// 打开弹窗
+const initForm = async (code: string) => {
+	const [err, res]: ToResponse = await to(platAnimalCageApplicationApi.getEntityById({ id: parseInt(code) }));
+	if (err) return;
+	await nextTick();
+	state.form = {
+		...res?.data,
+		approveStatus: ApproveStatusList.find((item) => item.id == res?.data?.approveStatus)?.name,
+		createdTime: dayjs(res?.data?.createdTime).format('YYYY-MM-DD'),
+	};
+};
+
+const closeDialog = () => {
+	emit('close');
+	expertDialogFormRef.value.resetFields();
+};
+
+// 暴露变量
+defineExpose({
+	initForm,
+});
+</script>
+<style lang="scss" scoped>
+:deep(.disUoloadSty .el-upload--picture-card) {
+	display: none; /* 上传按钮隐藏 */
+}
+:deep(.el-descriptions__label.el-descriptions__cell.is-bordered-label) {
+	width: 120px;
+}
+.el-upload-list--picture-card .el-upload-list__item-thumbnail {
+	height: 120px;
+}
+.fileName {
+	position: absolute;
+	width: 100%;
+	left: 0;
+	bottom: 0;
+	text-align: center;
+	font-size: 11px;
+	line-height: 1;
+	margin: 0;
+}
+</style>

+ 93 - 0
src/view/animal/application/components/ReturnCageDialog.vue

@@ -0,0 +1,93 @@
+<template>
+	<el-dialog title="退还" @close="() => (showRefundableDialog = false)" :close-on-click-modal="false" v-model="showRefundableDialog" width="400px">
+		<el-form ref="uploadRef" :model="refundableForm" :rules="refundableRules" size="default" label-width="140px" label-position="top">
+			<el-row style="margin-bottom: 20px">
+				<el-col :span="24">
+					<el-form-item label="笼位数量" prop="number">
+						<el-input-number
+							v-model="refundableForm.number"
+							style="width: 100%"
+							:min="1"
+							:max="props.currentRefundableItemNumber"
+							placeholder="请输入笼位减少数量"
+						/>
+					</el-form-item>
+				</el-col>
+			</el-row>
+			<el-row style="margin-bottom: 20px">
+				<el-col :span="24">
+					<el-form-item label="结束时间" prop="endDate">
+						<el-date-picker v-model="refundableForm.endDate" type="date" style="width: 100%" placeholder="请选择结束时间" clearable />
+					</el-form-item>
+				</el-col>
+			</el-row>
+		</el-form>
+		<template #footer>
+			<span class="dialog-footer">
+				<el-button type="info" @click="() => ((showRefundableDialog = false), (refundableId = 0))" size="default">取 消</el-button>
+				<el-button color="#2c78ff" @click="onRefundableSubmit()" size="default">提交</el-button>
+			</span>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, watch } from 'vue';
+import dayjs from 'dayjs';
+import to from 'await-to-js';
+import { ElMessage } from 'element-plus';
+
+import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+
+const emit = defineEmits(['getTableData']);
+
+const props = defineProps({
+	currentRefundableItemNumber: { type: Number, default: 0 },
+});
+
+const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi();
+
+const uploadRef = ref<any>(null);
+const showRefundableDialog = ref<boolean>(false);
+const refundableId = ref<number>(0);
+const refundableForm = reactive<{ number: number; endDate: string }>({
+	number: 0,
+	endDate: '',
+});
+
+const refundableRules = {
+	number: { required: true, message: '请输入笼位减少数量', trigger: 'blur' },
+	endDate: { required: true, message: '请选择结束时间', trigger: 'change' },
+};
+
+const handleOpenRefundableDialog = (id: number) => {
+	refundableId.value = id;
+	showRefundableDialog.value = true;
+};
+
+const onRefundableSubmit = () => {
+	uploadRef.value?.validate(async (valid: boolean) => {
+		if (!valid) return;
+		const params = {
+			cageApplicationId: refundableId.value,
+			endDate: dayjs(refundableForm.endDate).format('YYYY-MM-DD'),
+			number: refundableForm.number,
+		};
+		const [err]: ToResponse = await to(platAnimalCageApplicationApi.releaseCage(params));
+		if (err) return;
+		ElMessage.success('操作成功');
+		showRefundableDialog.value = false;
+		emit('getTableData');
+	});
+};
+
+watch(showRefundableDialog, (val) => {
+	if (!val) {
+		uploadRef.value?.resetFields();
+	}
+});
+
+defineExpose({
+	handleOpenRefundableDialog,
+});
+</script>

+ 348 - 0
src/view/animal/application/index.vue

@@ -0,0 +1,348 @@
+<template>
+  <div class="entry-container">
+    <div class="search-wrap" ref="searchWrapRef">
+      <el-form :model="state.queryParams" ref="queryRef">
+        <el-form-item prop="serialNo">
+          <el-select v-model="state.queryParams.approveStatus" style="width: 100%" placeholder="审批状态" clearable @change="search">
+            <el-option v-for="item in ApproveStatusList" :key="item.id" :label="item.name" :value="item.id"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item prop="serialNo">
+          <el-date-picker
+            v-model="dateTime"
+            type="daterange"
+            style="width: 100%"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            clearable
+            @change="search"
+          />
+        </el-form-item>
+        <el-form-item prop="serialNo">
+          <el-input v-model="state.queryParams.userName" style="width: 100%" placeholder="申请人" clearable @change="search" />
+        </el-form-item>
+      </el-form>
+      <div style="text-align: right">
+        <el-button @click="handleExport" style="height: 25px" type="primary">导出</el-button>
+      </div>
+    </div>
+
+    <div class="list-container">
+      <van-list v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad">
+        <van-cell v-for="item in state.list" :key="item" @click="handleCheckDetail(item)">
+          <template #default>
+            <div class="list">
+              <header class="flex justify-between">
+                <strong class="title">{{ `${item.userName}的笼位申请` }}</strong>
+                <van-tag v-if="item.approveStatus == ApproveStatus.WAIT_SUBMIT" type="warning">待提交</van-tag>
+                <van-tag v-else-if="item.approveStatus == ApproveStatus.APPROVING" type="primary">审核中</van-tag>
+                <van-tag v-else-if="item.approveStatus == ApproveStatus.PASS" type="success">通过</van-tag>
+                <van-tag v-else-if="item.approveStatus == ApproveStatus.REVOKE" type="success">撤销</van-tag>
+                <van-tag v-else-if="item.approveStatus == ApproveStatus.REFUSE" type="danger">拒绝</van-tag>
+              </header>
+              <p class="inst-title">
+                <span>课题名称</span>
+                <span class="title ml8">{{ item.projectGroupName }}</span>
+              </p>
+              <p class="inst-title">
+                <span>申请人</span>
+                <span class="title ml8">
+                  {{ item.userName }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>申请时间</span>
+                <span class="title ml8">
+                  {{ formatToChineseDate(item.createdTime) }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>开始时间</span>
+                <span class="title ml8">
+                  {{ formatToChineseDate(item.startDate) }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>申请笼位(个)</span>
+                <span class="title ml8">
+                  {{ item.number }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>退还笼位(个)</span>
+                <span class="title ml8">
+                  {{ item.returnNumber }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>申请状态</span>
+                <span class="title ml8">
+                  {{ formatApproveStatus(Number(item.approveStatus)) }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>动物类型</span>
+                <span class="title ml8">
+                  {{ item.categoryName }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>级别</span>
+                <span class="title ml8">
+                  {{ LeavelList.find((leaveItem) => leaveItem.id === item.level)?.name || '' }}
+                </span>
+              </p>
+              <footer class="flex justify-between mt16">
+                <span class="title">
+                  <el-button
+                    v-if="
+                      item.approveStatus === ApproveStatus.PASS.toString() &&
+                      item.returnStatus !== ReturnStatus.COMPLETE.toString() &&
+                      userInfos.id === item.userId
+                    "
+                    style="height: 25px"
+                    type="primary"
+                    @click="handleRefundable(item)"
+                    >退还</el-button
+                  >
+                </span>
+                <span class="time">{{ formatDate(new Date(item.createdTime), 'YYYY-mm-dd') }}</span>
+              </footer>
+            </div>
+          </template>
+        </van-cell>
+      </van-list>
+    </div>
+
+    <ApplicationModal ref="cageApplicationModalRef" @refresh="handleRefresh" />
+    <DetailModal :showDialog="showDetailDialog" :isReturnCageList="false" ref="detailModalRef" @close="() => (showDetailDialog = false)" />
+    <ReturnCageDialog ref="returnCageDialogRef" :currentRefundableItemNumber="currentRefundableItemNumber" :getTableData="handleRefresh" />
+
+    <van-floating-bubble v-model:offset="offset" icon="plus" @click="handleApplication" axis="y" />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, onMounted, defineAsyncComponent } from 'vue'
+  import to from 'await-to-js'
+  import dayjs from 'dayjs'
+  import { storeToRefs } from 'pinia'
+  import { useRouter, useRoute } from 'vue-router'
+
+  import { formatDate } from '/@/utils/formatTime'
+  import { useUserInfo } from '/@/stores/userInfo'
+  import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+  import { ApproveStatus, ReturnStatus, LeavelList, ApproveStatusList } from '/@/constants/pageConstants'
+
+  const ApplicationModal = defineAsyncComponent(() => import('/@/view/animal/application/components/Application.vue'))
+  const DetailModal = defineAsyncComponent(() => import('/@/view/animal/application/components/Detail.vue'))
+  const ReturnCageDialog = defineAsyncComponent(() => import('/@/view/animal/application/components/ReturnCageDialog.vue'))
+
+  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+  const stores = useUserInfo()
+  const { userInfos } = storeToRefs(stores)
+
+  const router = useRouter()
+  const route = useRoute()
+
+  const cageApplicationModalRef = ref()
+  const detailModalRef = ref()
+  const returnCageDialogRef = ref()
+  const showDetailDialog = ref<boolean>(false)
+  const dateTime = ref<any>([])
+  const offset = ref({ x: -80, y: 450 })
+  const state = reactive({
+    queryParams: {
+      categoryId: null,
+      categoryName: '',
+      level: null,
+      projectGroupName: '',
+      pageNum: 1,
+      pageSize: 20,
+      userName: '',
+      startDate: '',
+      endDate: '',
+      approveStatus: ''
+    },
+    finished: false,
+    loading: true,
+    list: [] as any[]
+  })
+  const currentRefundableItemNumber = ref<number>(0)
+
+  const resetQueryParams = () => {
+    state.queryParams = {
+      categoryId: null,
+      categoryName: '',
+      level: null,
+      projectGroupName: '',
+      pageNum: 1,
+      pageSize: 20,
+      userName: '',
+      startDate: '',
+      endDate: '',
+      approveStatus: ''
+    }
+    ;(state.finished = false), (state.loading = true), (state.list = [] as any[])
+  }
+
+  const setListPayload = (isExport?: boolean) => {
+    const payload = {
+      ...state.queryParams,
+      pageSize: isExport ? 99999 : state.queryParams.pageSize
+    }
+
+    if (dateTime.value && dateTime.value[0]) {
+      payload.startDate = dayjs(dateTime.value[0]).format('YYYY-MM-DD')
+    }
+
+    if (dateTime.value && dateTime.value[1]) {
+      payload.endDate = dayjs(dateTime.value[1]).format('YYYY-MM-DD')
+    }
+
+    Object.entries(payload).forEach(([key, value]) => {
+      if (value === '' || value === null) {
+        delete payload[key as keyof typeof payload]
+      }
+    })
+
+    return payload
+  }
+
+  const formatToChineseDate = (dateStr: string) => {
+    const date = new Date(dateStr)
+    const year = date.getFullYear()
+    const month = String(date.getMonth() + 1).padStart(2, '0')
+    const day = String(date.getDate()).padStart(2, '0')
+
+    return `${year}年${month}月${day}日`
+  }
+
+  const formatApproveStatus = (status: number) => {
+    return ApproveStatusList.find((item) => item.id === status)?.name || ''
+  }
+
+  const onLoad = async (isSearch?: boolean) => {
+    const [err, res]: ToResponse = await to(
+      platAnimalCageApplicationApi.getList({
+        ...setListPayload(),
+        pageNum: isSearch ? 1 : state.queryParams.pageNum
+      })
+    )
+    if (err) return
+    const list = res?.data?.list || []
+    state.loading = false
+
+    if (!isSearch) {
+      for (const item of list) {
+        state.list.push(item)
+      }
+      state.queryParams.pageNum++
+      if (list.length < state.queryParams.pageSize) {
+        state.finished = true
+      }
+    } else {
+      state.list = list
+    }
+  }
+
+  const handleCheckDetail = (row: any) => {
+    detailModalRef.value.initForm(row.id)
+    showDetailDialog.value = true
+  }
+
+  const handleRefundable = (row: any) => {
+    currentRefundableItemNumber.value = row.number
+    returnCageDialogRef.value.handleOpenRefundableDialog(row.id)
+  }
+
+  const handleApplication = () => {
+    cageApplicationModalRef.value.openDialog()
+  }
+
+  const handleRefresh = () => {
+    resetQueryParams()
+    onLoad()
+  }
+
+  const search = () => {
+    onLoad(true)
+  }
+
+  const handleExport = async () => {
+    const [err, res]: ToResponse = await to(platAnimalCageApplicationApi.getApplicationListExport({ ...setListPayload(true), base64Enable: 1 }))
+
+    if (err) return
+
+    if (res && res.data && typeof res.data === 'string') {
+      const link = document.createElement('a')
+      link.href = `data:application/octet-stream;base64,${res.data}`
+      link.download = `笼位申请_${dayjs(new Date()).format('YYYY-MM-DD')}.xlsx`
+      link.style.display = 'none'
+      document.body.appendChild(link)
+      link.click()
+      document.body.removeChild(link)
+    }
+  }
+
+  onMounted(() => {
+    const type = route.query.type
+    if (type) {
+      state.queryParams.approveStatus = type as string
+    }
+    onLoad()
+  })
+</script>
+
+<style lang="scss" scoped>
+  .entry-container {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    .search-wrap {
+      background: #fff;
+      margin-bottom: 10px;
+      padding: 15px;
+    }
+    .list-container {
+      overflow-y: auto;
+      padding: 10px;
+      border-radius: 4px;
+      flex: 1;
+    }
+    .van-list {
+      .van-cell {
+        background-color: #fff;
+        + .van-cell {
+          margin-top: 10px;
+        }
+        header,
+        footer {
+          color: #333;
+        }
+        .title {
+          flex: 1;
+          white-space: nowrap;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          text-align: left;
+        }
+        .inst-title {
+          color: #333;
+          text-align: left;
+          flex: 1;
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+          margin-top: 4px;
+          span:first-child {
+            color: rgb(120, 120, 120);
+          }
+        }
+        .time {
+          color: #f69a4d;
+        }
+      }
+    }
+  }
+</style>

+ 293 - 0
src/view/animal/return/index.vue

@@ -0,0 +1,293 @@
+<template>
+  <div class="entry-container">
+    <div class="search-wrap" ref="searchWrapRef">
+      <el-form :model="state.queryParams" ref="queryRef">
+        <el-form-item prop="serialNo">
+          <el-select v-model="state.queryParams.approveStatus" style="width: 100%" placeholder="审批状态" clearable @change="search">
+            <el-option v-for="item in ApproveStatusList" :key="item.id" :label="item.name" :value="item.id"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item prop="serialNo">
+          <el-date-picker
+            v-model="dateTime"
+            type="daterange"
+            style="width: 100%"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            clearable
+            @change="search"
+          />
+        </el-form-item>
+        <el-form-item prop="serialNo">
+          <el-input v-model="state.queryParams.userName" style="width: 100%" placeholder="申请人" clearable @change="search" />
+        </el-form-item>
+      </el-form>
+      <div style="text-align: right">
+        <el-button @click="handleExport" style="height: 25px" type="primary">导出</el-button>
+      </div>
+    </div>
+
+    <div class="list-container">
+      <van-list v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad">
+        <van-cell v-for="item in state.list" :key="item" @click="handleCheckDetail(item)">
+          <template #default>
+            <div class="list">
+              <header class="flex justify-between">
+                <strong class="title">{{ `${item.userName}的笼位退还申请` }}</strong>
+                <van-tag v-if="item.approveStatus == ApproveStatus.WAIT_SUBMIT" type="warning">待提交</van-tag>
+                <van-tag v-else-if="item.approveStatus == ApproveStatus.APPROVING" type="primary">审核中</van-tag>
+                <van-tag v-else-if="item.approveStatus == ApproveStatus.PASS" type="success">通过</van-tag>
+                <van-tag v-else-if="item.approveStatus == ApproveStatus.REVOKE" type="success">撤销</van-tag>
+                <van-tag v-else-if="item.approveStatus == ApproveStatus.REFUSE" type="danger">拒绝</van-tag>
+              </header>
+              <p class="inst-title">
+                <span>课题名称</span>
+                <span class="title ml8">{{ item.projectGroupName }}</span>
+              </p>
+              <p class="inst-title">
+                <span>申请人</span>
+                <span class="title ml8">
+                  {{ item.userName }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>申请时间</span>
+                <span class="title ml8">
+                  {{ formatToChineseDate(item.createdTime) }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>退还笼位(个)</span>
+                <span class="title ml8">
+                  {{ item.number }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>申请状态</span>
+                <span class="title ml8">
+                  {{ formatApproveStatus(Number(item.approveStatus)) }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>动物类型</span>
+                <span class="title ml8">
+                  {{ item.categoryName }}
+                </span>
+              </p>
+              <p class="inst-title">
+                <span>级别</span>
+                <span class="title ml8">
+                  {{ LeavelList.find((leaveItem) => leaveItem.id === item.level)?.name || '' }}
+                </span>
+              </p>
+              <footer class="flex justify-between mt16">
+                <span class="time">{{ formatDate(new Date(item.createdTime), 'YYYY-mm-dd') }}</span>
+              </footer>
+            </div>
+          </template>
+        </van-cell>
+      </van-list>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, onMounted } from 'vue'
+  import to from 'await-to-js'
+  import dayjs from 'dayjs'
+  import { useRoute } from 'vue-router'
+
+  import { formatDate } from '/@/utils/formatTime'
+  import { usePlatAnimalCageApplicationApi } from '/@/api/platform/animal'
+  import { ApproveStatus, LeavelList, ApproveStatusList } from '/@/constants/pageConstants'
+
+  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
+
+  const route = useRoute()
+
+  const cageApplicationModalRef = ref()
+  const detailModalRef = ref()
+  const returnCageDialogRef = ref()
+  const showDetailDialog = ref<boolean>(false)
+  const dateTime = ref<any>([])
+  const state = reactive({
+    queryParams: {
+      categoryId: null,
+      categoryName: '',
+      level: null,
+      projectGroupName: '',
+      pageNum: 1,
+      pageSize: 20,
+      userName: '',
+      startDate: '',
+      endDate: '',
+      approveStatus: ''
+    },
+    finished: false,
+    loading: true,
+    list: [] as any[]
+  })
+  const currentRefundableItemNumber = ref<number>(0)
+
+  const resetQueryParams = () => {
+    state.queryParams = {
+      categoryId: null,
+      categoryName: '',
+      level: null,
+      projectGroupName: '',
+      pageNum: 1,
+      pageSize: 20,
+      userName: '',
+      startDate: '',
+      endDate: '',
+      approveStatus: ''
+    }
+    ;(state.finished = false), (state.loading = true), (state.list = [] as any[])
+  }
+
+  const setListPayload = (isExport?: boolean) => {
+    const payload = {
+      ...state.queryParams,
+      pageSize: isExport ? 99999 : state.queryParams.pageSize
+    }
+
+    if (dateTime.value && dateTime.value[0]) {
+      payload.startDate = dayjs(dateTime.value[0]).format('YYYY-MM-DD')
+    }
+
+    if (dateTime.value && dateTime.value[1]) {
+      payload.endDate = dayjs(dateTime.value[1]).format('YYYY-MM-DD')
+    }
+
+    Object.entries(payload).forEach(([key, value]) => {
+      if (value === '' || value === null) {
+        delete payload[key as keyof typeof payload]
+      }
+    })
+
+    return payload
+  }
+
+  const formatToChineseDate = (dateStr: string) => {
+    const date = new Date(dateStr)
+    const year = date.getFullYear()
+    const month = String(date.getMonth() + 1).padStart(2, '0')
+    const day = String(date.getDate()).padStart(2, '0')
+
+    return `${year}年${month}月${day}日`
+  }
+
+  const formatApproveStatus = (status: number) => {
+    return ApproveStatusList.find((item) => item.id === status)?.name || ''
+  }
+
+  const onLoad = async (isSearch?: boolean) => {
+    const [err, res]: ToResponse = await to(
+      platAnimalCageApplicationApi.getCageReleaseList({
+        ...setListPayload(),
+        pageNum: isSearch ? 1 : state.queryParams.pageNum
+      })
+    )
+    if (err) return
+    const list = res?.data?.list || []
+    state.loading = false
+
+    if (!isSearch) {
+      for (const item of list) {
+        state.list.push(item)
+      }
+      state.queryParams.pageNum++
+      if (list.length < state.queryParams.pageSize) {
+        state.finished = true
+      }
+    } else {
+      state.list = list
+    }
+  }
+
+  const handleCheckDetail = (row: any) => {
+    detailModalRef.value.initForm(row.id)
+    showDetailDialog.value = true
+  }
+
+  const search = () => {
+    onLoad(true)
+  }
+
+  const handleExport = async () => {
+    const [err, res]: ToResponse = await to(platAnimalCageApplicationApi.getCageReleaseApplicationsExport({ ...setListPayload(true), base64Enable: 1 }))
+
+    if (err) return
+
+    if (res && res.data && typeof res.data === 'string') {
+      const link = document.createElement('a')
+      link.href = `data:application/octet-stream;base64,${res.data}`
+      link.download = `笼位申请_${dayjs(new Date()).format('YYYY-MM-DD')}.xlsx`
+      link.style.display = 'none'
+      document.body.appendChild(link)
+      link.click()
+      document.body.removeChild(link)
+    }
+  }
+
+  onMounted(() => {
+    const type = route.query.type
+    if (type) {
+      state.queryParams.approveStatus = type as string
+    }
+    onLoad()
+  })
+</script>
+
+<style lang="scss" scoped>
+  .entry-container {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    .search-wrap {
+      background: #fff;
+      margin-bottom: 10px;
+      padding: 15px;
+    }
+    .list-container {
+      overflow-y: auto;
+      padding: 10px;
+      border-radius: 4px;
+      flex: 1;
+    }
+    .van-list {
+      .van-cell {
+        background-color: #fff;
+        + .van-cell {
+          margin-top: 10px;
+        }
+        header,
+        footer {
+          color: #333;
+        }
+        .title {
+          flex: 1;
+          white-space: nowrap;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          text-align: left;
+        }
+        .inst-title {
+          color: #333;
+          text-align: left;
+          flex: 1;
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+          margin-top: 4px;
+          span:first-child {
+            color: rgb(120, 120, 120);
+          }
+        }
+        .time {
+          color: #f69a4d;
+        }
+      }
+    }
+  }
+</style>

+ 1 - 1
src/view/home/index.vue

@@ -43,7 +43,7 @@
           <img src="../../assets/img/培训考试.png" alt="" />
           <p>培训考试</p>
         </li>
-        <li v-auth="'h5-home-animal'">
+        <li @click="onRouterPush('/animal-application')" v-auth="'h5-home-animal'">
           <img src="../../assets/img/动物笼位.png" alt="" />
           <p>动物笼位</p>
         </li>