Ver código fonte

fix:笼位申请添加饭周龄和体重

张旭伟 4 dias atrás
pai
commit
b5bdc8a449

+ 1 - 1
src/constants/pageConstants.ts

@@ -18,7 +18,7 @@ export enum ReturnStatus {
 export const LeavelList = [
   { name: '普通级', id: 1 },
   { name: 'SPF级', id: 3 },
-  { name: '无菌级', id: 4 },
+  // { name: '无菌级', id: 4 },
 ]
 
 // 笼位申请审批状态列表

+ 735 - 0
src/view/Application.vue

@@ -0,0 +1,735 @@
+<template>
+  <div class="application-dialog-container">
+    <el-dialog :title="state.dialog.title" @close="onCancel" :close-on-click-modal="false" :destroy-on-close="true"
+      v-model="state.dialog.isShowDialog" width="800px">
+      <el-form ref="expertDialogFormRef" :model="state.form" :rules="rules" size="default" label-width="140px"
+        label-position="top">
+        <LaText size="16" type="important" bold class="mb16">基本信息</LaText>
+        <el-row :gutter="20">
+          <el-col :span="12" class="mb16">
+            <el-form-item label="课题名称" prop="projectGroupId">
+              <el-select :disabled="state.dialog.type === 'view'" 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" class="mb16">
+            <el-form-item label="姓名" prop="group">
+              <el-input v-if="state.dialog.type === 'add'" v-model="userInfos.nickName" disabled />
+              <el-input v-else v-model="state.form.userName" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12" class="mb16">
+            <el-form-item label="部门" prop="deptName">
+              <el-input v-if="state.dialog.type === 'add'" v-model="userInfos.deptName" disabled />
+              <el-input v-else v-model="state.form.deptName" disabled />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" class="mb16">
+            <el-form-item label="联系方式" prop="phone">
+              <el-input v-if="state.dialog.type === 'add'" v-model="userInfos.phone" disabled />
+              <el-input v-else v-model="state.form.phone" disabled />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <LaText class="mb16 mt20" size="16" type="important" bold>实验动物笼位预约信息</LaText>
+        <el-row class="mt10" :gutter="20">
+          <el-col :span="12" class="mb16">
+            <el-form-item label="笼位数量" prop="number">
+              <el-input-number :disabled="state.dialog.type === 'view'" v-model="state.form.number" style="width: 100%"
+                :min="1" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" class="mb16">
+            <el-form-item label="开始使用时间" prop="startDate">
+              <el-date-picker :disabled="state.dialog.type === 'view'" 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" class="mb16">
+            <el-form-item label="动物类别" prop="categoryId">
+              <el-select :disabled="state.dialog.type === 'view'" 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" class="mb16">
+            <el-form-item label="品种品系" prop="variety">
+              <el-input :disabled="state.dialog.type === 'view'" v-model="state.form.variety" placeholder="请输入品种品系" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row class="mt10" :gutter="20">
+          <el-col :span="12" class="mb16">
+            <el-form-item label="饲养区域" prop="level">
+              <el-select :disabled="state.dialog.type === 'view'" 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-col :span="12" class="mb16">
+            <el-form-item label="周龄" prop="age">
+              <div style="display: flex; align-items: center; gap: 8px;">
+                <el-input-number :disabled="state.dialog.type === 'view'" v-model="state.form.age.min" 
+                  placeholder="最小周龄" style="width: 45%" :min="0" />
+                <span style="color: #999;">至</span>
+                <el-input-number :disabled="state.dialog.type === 'view'" v-model="state.form.age.max" 
+                  placeholder="最大周龄" style="width: 45%" :min="0" />
+              </div>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" class="mb16">
+            <el-form-item label="体重" prop="weight">
+              <div style="display: flex; align-items: center; gap: 8px;">
+                <el-input-number :disabled="state.dialog.type === 'view'" v-model="state.form.weight.min" 
+                  placeholder="最小体重" style="width: 45%" :min="0" :precision="2" />
+                <span style="color: #999;">至</span>
+                <el-input-number :disabled="state.dialog.type === 'view'" v-model="state.form.weight.max" 
+                  placeholder="最大体重" style="width: 45%" :min="0" :precision="2" />
+              </div>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" class="mb16">
+            <el-form-item label="饲养总天数" prop="feedingDay">
+              <el-input-number :disabled="state.dialog.type === 'view'" v-model="state.form.feedingDay"
+                style="width: 100%" :min="1" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row class="mt10" :gutter="20">
+          <el-col :span="6" class="mb16">
+            <el-form-item label="雄性" prop="maleNumber">
+              <el-input-number style="width: 100%" :disabled="state.dialog.type === 'view'" placeholder="雄性数量"
+                v-model="state.form.maleNumber" :min="0" />
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="6" class="mb16">
+            <el-form-item label="雌性" prop="famaleNumber">
+              <el-input-number style="width: 100%" :disabled="state.dialog.type === 'view'" placeholder="雌性数量"
+                v-model="state.form.famaleNumber" :min="0" />
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="12" class="mb16">
+            <el-form-item label="合计" prop="totalNumber">
+              <el-input-number style="width: 100%" disabled placeholder="合计" v-model="state.form.totalNumber"
+                :min="0" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <LaText class="mb16 mt20" size="16" type="important" bold>采购渠道</LaText>
+        <el-row class="mt10" :gutter="20">
+          <el-col :span="12" class="mb16">
+            <el-form-item label="采购渠道" prop="buyFrom">
+              <el-radio-group :disabled="state.dialog.type === 'view'" v-model="state.form.buyFrom">
+                <el-radio :label="ProcurementChannels.PURCHASED_BY_OTHERS" size="large">动物房代购</el-radio>
+                <el-radio :label="ProcurementChannels.PURCHASED_BY_MYSELF" size="large">自行购买</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" class="mb16">
+            <el-form-item label="动物到达时间" prop="comeTime"
+              :required="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
+              <el-date-picker :disabled="state.dialog.type === 'view'" v-model="state.form.comeTime" type="date"
+                placeholder="请选择到达时间" clearable style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF" class="mt10" :gutter="20">
+          <el-col :span="12" class="mb16">
+            <el-form-item label="外购来源单位"
+              :prop="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF ? 'comeFromUnit' : ''">
+              <el-input :disabled="state.dialog.type === 'view'" v-model="state.form.comeFromUnit" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row class="mt10" :gutter="20" v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
+          <el-col :span="12" class="mb16">
+            <el-form-item label="生产许可证副本" prop="licenseNumberFile"
+              :required="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
+              <el-upload v-model:file-list="licenseNumberFileList" class="upload-demo" :action="uploadUrl" :limit="1"
+                style="width: 100%" :before-upload="beforeAvatarFileUpload" :disabled="state.dialog.type === 'view'"
+                :on-preview="handlePreview" :on-remove="() => handleRemove(UploadFileType.LICENSE_NUMBER)"
+                :on-success="(res: any, file: UploadFile) => handleSuccess(res, UploadFileType.LICENSE_NUMBER, file)">
+                <el-button :disabled="state.dialog.type === 'view'" type="primary">点击上传</el-button>
+                <div class="el-upload__tip ml10">支持格式:jpg png pdf等,单个文件不超过20MB</div>
+              </el-upload>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row class="mt10" :gutter="20" v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
+          <el-col :span="12" class="mb16">
+            <el-form-item label="近三个月动物质量检测证明" prop="animalTestDateFile"
+              :required="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
+              <el-upload v-model:file-list="animalTestDateFileList" class="upload-demo" :action="uploadUrl" :limit="1"
+                style="width: 100%" :before-upload="beforeAvatarFileUpload" :on-preview="handlePreview"
+                :disabled="state.dialog.type === 'view'"
+                :on-remove="() => handleRemove(UploadFileType.ANIMAL_TEST_DATE)"
+                :on-success="(res: any, file: UploadFile) => handleSuccess(res, UploadFileType.ANIMAL_TEST_DATE, file)">
+                <el-button :disabled="state.dialog.type === 'view'" type="primary">点击上传</el-button>
+                <div class="el-upload__tip ml10">支持格式:jpg png pdf等,单个文件不超过20MB</div>
+              </el-upload>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row class="mt10" :gutter="20" v-if="state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF">
+          <el-col :span="12" class="mb16">
+            <el-form-item label="基因鉴定报告" prop="geneIdentificationFile">
+              <el-upload v-model:file-list="geneIdentificationFileList" class="upload-demo" :action="uploadUrl"
+                :limit="1" style="width: 100%" :before-upload="beforeAvatarFileUpload" :on-preview="handlePreview"
+                :disabled="state.dialog.type === 'view'"
+                :on-remove="() => handleRemove(UploadFileType.GENE_IDENTIFICATION_FILE)"
+                :on-success="(res: any, file: UploadFile) => handleSuccess(res, UploadFileType.GENE_IDENTIFICATION_FILE, file)">
+                <el-button :disabled="state.dialog.type === 'view'" type="primary">点击上传</el-button>
+                <div class="el-upload__tip ml10">支持格式:jpg png pdf等,单个文件不超过20MB</div>
+              </el-upload>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <LaText class="mb16 mt20" size="16" type="important" bold>特殊要求和附件</LaText>
+        <el-row class="mt10" :gutter="20">
+          <el-col :span="12" class="mb16">
+            <el-form-item label="是否有特殊饲养要求" prop="hasFeedingSpecial">
+              <el-radio-group :disabled="state.dialog.type === 'view'" v-model="state.form.hasFeedingSpecial">
+                <el-radio :label="FeedingSpecial.HAVE_FEEDING_SPECIAL" size="large">有</el-radio>
+                <el-radio :label="FeedingSpecial.NO_FEEDING_SPECIAL" size="large">无</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" class="mb16" v-if="state.form.hasFeedingSpecial === FeedingSpecial.HAVE_FEEDING_SPECIAL">
+            <el-form-item label="特殊饲养要求" prop="feedingSpecialDesc" required>
+              <el-input :disabled="state.dialog.type === 'view'" placeholder="输入特殊饲养要求,如每天更换垫料等"
+                v-model="state.form.feedingSpecialDesc" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <!-- <el-row class="mt10" :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="笼位预约表" prop="cageAppointFile">
+              <el-upload
+                v-model:file-list="cageAppointFileList"
+                class="upload-demo"
+                :action="uploadUrl"
+                :limit="1"
+                style="width: 100%"
+                :before-upload="beforeAvatarFileUpload"
+                :on-preview="handlePreview"
+                :disabled="state.dialog.type === 'view'"
+                :on-remove="() => handleRemove(UploadFileType.CAGE_APPOINT_FILE)"
+                :on-success="(res: any, file: UploadFile) => handleSuccess(res, UploadFileType.CAGE_APPOINT_FILE, file)"
+              >
+                <el-button :disabled="state.dialog.type === 'view'" type="primary">点击上传</el-button>
+                <div class="el-upload__tip ml10">支持格式:jpg png pdf等,单个文件不超过20MB</div>
+              </el-upload>
+            </el-form-item>
+          </el-col>
+        </el-row> -->
+        <el-row class="mt10" :gutter="20">
+          <el-col :span="12" class="mb16">
+            <el-form-item label="实验动物福利伦理审查申请表" prop="ethicsCheckFile" required>
+              <el-upload v-model:file-list="ethicsCheckFileList" class="upload-demo" :action="uploadUrl" :limit="1"
+                style="width: 100%" :before-upload="beforeAvatarFileUpload" :on-preview="handlePreview"
+                :disabled="state.dialog.type === 'view'"
+                :on-remove="() => handleRemove(UploadFileType.ETHICS_CHECK_FILE)"
+                :on-success="(res: any, file: UploadFile) => handleSuccess(res, UploadFileType.ETHICS_CHECK_FILE, file)">
+                <el-button :disabled="state.dialog.type === 'view'" type="primary">点击上传</el-button>
+                <div class="el-upload__tip ml10">支持格式:jpg png pdf等,单个文件不超过20MB</div>
+              </el-upload>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row class="mt10" :gutter="20">
+          <el-col :span="12" class="mb16">
+            <el-form-item label="实验动物福利伦理审查意见表" prop="ethicsAdviceFile" required>
+              <el-upload v-model:file-list="ethicsAdviceFileList" class="upload-demo" :action="uploadUrl" :limit="1"
+                style="width: 100%" :before-upload="beforeAvatarFileUpload" :on-preview="handlePreview"
+                :disabled="state.dialog.type === 'view'"
+                :on-remove="() => handleRemove(UploadFileType.ETHICS_ADVICE_FILE)"
+                :on-success="(res: any, file: UploadFile) => handleSuccess(res, UploadFileType.ETHICS_ADVICE_FILE, file)">
+                <el-button :disabled="state.dialog.type === 'view'" type="primary">点击上传</el-button>
+                <div class="el-upload__tip ml10">支持格式:jpg png pdf等,单个文件不超过20MB</div>
+              </el-upload>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row v-if="state.dialog.type === 'view'">
+          <el-col :span="24" class="mb16">
+            <LaText class="mb16 mt20" size="16" type="important" bold>审批流</LaText>
+            <FlowTable :id="state.form.id" :businessCode="state.form.id + ''" defCode="plat_cage_applications" />
+          </el-col>
+        </el-row>
+
+        <el-row class="mt10" :gutter="20">
+          <el-col :span="24" class="mt30 mb16">
+            <el-checkbox :disabled="state.dialog.type === 'view'" v-model="safePromiseStatus">
+              {{ SafePromise }}
+            </el-checkbox>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <template #footer>
+        <span v-if="state.dialog.type === 'add'" 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, computed, defineAsyncComponent, watch } from 'vue';
+  import to from 'await-to-js';
+  import { ElMessage } from 'element-plus';
+  import { storeToRefs } from 'pinia';
+  import dayjs from 'dayjs';
+  import { UploadFile } from 'element-plus/es/components';
+
+  import { usePlatAnimalCageApplicationApi } from 'labsop-api/src/api/platform/animal';
+  import {
+    LeavelList,
+    ProcurementChannels,
+    UploadFileType,
+    FeedingSpecial,
+    SafePromise,
+    DateFormat,
+  } from '/@/constants/pageConstants';
+
+  import { deepClone } from '/@/utils/other';
+  import { useUserInfo } from '/@/stores/userInfo';
+
+  const uploadUrl = (import.meta as any).env.VITE_UPLOAD;
+
+  const stores = useUserInfo();
+  const { userInfos } = storeToRefs(stores);
+
+  const FlowTable = defineAsyncComponent(() => import('/@/components/flow/flow-table.vue'));
+
+  // 定义子组件向父组件传值/事件
+  const emit = defineEmits(['refresh']);
+
+  const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi();
+
+  const expertDialogFormRef = ref();
+  const projectGroupList = ref<any[]>([]);
+  const projects = ref<any[]>([]);
+
+  const animalNumber = computed(() => {
+    const maleNumber = state.form.maleNumber || 0;
+    const famaleNumber = state.form.famaleNumber || 0;
+    return maleNumber + famaleNumber;
+  });
+
+  const animalTypeList = ref<any[]>([]);
+
+  const licenseNumberFileList = ref<UploadFile[]>([]);
+  const animalTestDateFileList = ref<UploadFile[]>([]);
+  const geneIdentificationFileList = ref<UploadFile[]>([]);
+  const cageAppointFileList = ref<UploadFile[]>([]);
+  const ethicsCheckFileList = ref<UploadFile[]>([]);
+  const ethicsAdviceFileList = ref<UploadFile[]>([]);
+
+  const safePromiseStatus = ref<boolean>(false);
+
+  const rules = {
+    projectGroupId: { required: true, message: '不能为空', trigger: 'change' },
+    categoryName: { required: true, message: '不能为空', trigger: 'change' },
+    number: { required: true, message: '不能为空', trigger: 'change' },
+    startDate: { required: true, message: '不能为空', trigger: 'change' },
+    comeTime: {
+      validator: (rule: any, value: any, callback: any) => {
+        // 只有在自行购买时才验证动物到达时间
+        if (state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF && (!value || value === '')) {
+          callback(new Error('动物到达时间不能为空'));
+        } else {
+          callback();
+        }
+      },
+      trigger: 'change',
+    },
+    // maleNumber: { required: true, message: '不能为空', trigger: 'change' },
+    // weight: { required: true, message: '不能为空', trigger: 'change' },
+    buyFrom: { required: true, message: '不能为空', trigger: 'change' },
+    comeFromUnit: { required: true, message: '不能为空', trigger: 'change' },
+    variety: { required: true, message: '不能为空', trigger: 'change' },
+    categoryId: { required: true, message: '不能为空', trigger: 'change' },
+    feedingDay: { required: true, message: '不能为空', trigger: 'change' },
+
+    level: { required: true, message: '不能为空', trigger: 'change' },
+    // feedingSpecialDesc: { required: true, message: '不能为空', trigger: 'change' },
+    // ethicsCheckFile: { required: true, message: '不能为空', trigger: 'change' },
+    // ethicsAdviceFile: { required: true, message: '不能为空', trigger: 'change' },
+    licenseNumberFile: {
+      validator: (rule: any, value: any, callback: any) => {
+        // 只有在动物房代购时才验证这些字段
+        if (state.form.buyFrom === ProcurementChannels.PURCHASED_BY_OTHERS && (!value || value.length === 0)) {
+          callback(new Error('生产许可证副本不能为空'));
+        } else {
+          callback();
+        }
+      },
+      trigger: 'change',
+    },
+    animalTestDateFile: {
+      validator: (rule: any, value: any, callback: any) => {
+        // 只有在动物房代购时才验证这些字段
+        if (state.form.buyFrom === ProcurementChannels.PURCHASED_BY_OTHERS && (!value || value.length === 0)) {
+          callback(new Error('动物质检证明不能为空'));
+        } else {
+          callback();
+        }
+      },
+      trigger: 'change',
+    },
+    ethicsCheckFile: {
+      validator: (rule: any, value: any, callback: any) => {
+        if (!value || value.length === 0) {
+          callback(new Error('实验动物福利伦理审查申请表不能为空'));
+        } else {
+          callback();
+        }
+      },
+      trigger: 'change',
+    },
+    ethicsAdviceFile: {
+      validator: (rule: any, value: any, callback: any) => {
+        if (!value || value.length === 0) {
+          callback(new Error('实验动物福利伦理审查意见表不能为空'));
+        } else {
+          callback();
+        }
+      },
+      trigger: 'change',
+    },
+    feedingSpecialDesc: {
+      validator: (rule: any, value: any, callback: any) => {
+        // 只有在有特殊饲养要求时才验证特殊饲养要求描述
+        if (state.form.hasFeedingSpecial === FeedingSpecial.HAVE_FEEDING_SPECIAL && (!value || value.trim() === '')) {
+          callback(new Error('特殊饲养要求不能为空'));
+        } else {
+          callback();
+        }
+      },
+      trigger: 'change',
+    },
+    age: {
+      validator: (rule: any, value: any, callback: any) => {
+        if (!value || value.min === null || value.max === null) {
+          callback(new Error('周龄范围不能为空'));
+        } else if (value.min > value.max) {
+          callback(new Error('最小周龄不能大于最大周龄'));
+        } else {
+          callback();
+        }
+      },
+      trigger: 'change',
+    },
+    weight: {
+      validator: (rule: any, value: any, callback: any) => {
+        if (!value || value.min === null || value.max === null) {
+          callback(new Error('体重范围不能为空'));
+        } else if (value.min > value.max) {
+          callback(new Error('最小体重不能大于最大体重'));
+        } else {
+          callback();
+        }
+      },
+      trigger: 'change',
+    },
+  };
+
+  const defaultFormFields = {
+    id: 0,
+    projectGroupName: '',
+    projectGroupId: null,
+    categoryName: '',
+    categoryId: null,
+    level: null,
+    number: 1,
+    startDate: '',
+    maleNumber: 0,
+    famaleNumber: 0,
+    totalNumber: 0,
+    weight: { min: 0, max: 0 },
+    age: { min: 0, max: 0 },
+    feedingDay: 0,
+    userName: '',
+    deptId: null,
+    deptName: '',
+    phone: '',
+    buyFrom: ProcurementChannels.PURCHASED_BY_OTHERS,
+    comeTime: '',
+    comeFromUnit: '',
+    licenseNumberFile: [] as { name: string; url: string }[],
+    animalTestDateFile: [] as { name: string; url: string }[],
+    geneIdentificationFile: [] as { name: string; url: string }[],
+    hasFeedingSpecial: FeedingSpecial.HAVE_FEEDING_SPECIAL,
+    feedingSpecialDesc: '',
+    cageAppointFile: [] as { name: string; url: string }[],
+    ethicsCheckFile: [] as { name: string; url: string }[],
+    ethicsAdviceFile: [] as { name: string; url: string }[],
+  };
+
+  const state = reactive({
+    form: defaultFormFields,
+    safePromise: false,
+    safeRead: false,
+    dialog: {
+      isShowDialog: false,
+      type: '',
+      title: '',
+      submitTxt: '',
+    },
+  });
+
+  watch(
+    () => [state.form.maleNumber, state.form.famaleNumber],
+    ([maleNumber, famaleNumber]) => {
+      state.form.totalNumber = (maleNumber || 0) + (famaleNumber || 0);
+    },
+    { immediate: true }
+  );
+
+  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 isValidJsonArray = (str: string) => {
+    try {
+      const parsed = JSON.parse(str);
+      return Array.isArray(parsed);
+    } catch (e) {
+      return false;
+    }
+  };
+
+  const parseRangeField = (str: string) => {
+    try {
+      if (!str) return { min: 0, max: 0 };
+      // 处理双重转义的情况
+      const cleanStr = str.replace(/\\"/g, '"').replace(/^"|"$/g, '');
+      const parsed = JSON.parse(cleanStr);
+      return {
+        min: parsed.min || 0,
+        max: parsed.max || 0
+      };
+    } catch (e) {
+      console.warn('解析范围字段失败:', str, e);
+      return { min: 0, max: 0 };
+    }
+  };
+
+  // 打开弹窗
+  const openDialog = async (type: 'add' | 'view', row?: any) => {
+    await getDicts();
+    state.dialog.type = type;
+    state.dialog.title = '笼位申请';
+
+    if (type === 'view' && row) {
+      // 处理范围字段的JSON字符串
+      const processedRow = {
+        ...row,
+        age: parseRangeField(row.age),
+        weight: parseRangeField(row.weight)
+      };
+      state.form = processedRow;
+      licenseNumberFileList.value = isValidJsonArray(row.licenseNumberFile) ? JSON.parse(row.licenseNumberFile) : [];
+      animalTestDateFileList.value = isValidJsonArray(row.animalTestDateFile) ? JSON.parse(row.animalTestDateFile) : [];
+      geneIdentificationFileList.value = isValidJsonArray(row.geneIdentificationFile)
+        ? JSON.parse(row.geneIdentificationFile)
+        : [];
+      cageAppointFileList.value = isValidJsonArray(row.cageAppointFile) ? JSON.parse(row.cageAppointFile) : [];
+      ethicsCheckFileList.value = isValidJsonArray(row.ethicsCheckFile) ? JSON.parse(row.ethicsCheckFile) : [];
+      ethicsAdviceFileList.value = isValidJsonArray(row.ethicsAdviceFile) ? JSON.parse(row.ethicsAdviceFile) : [];
+      safePromiseStatus.value = true;
+    } else if (type === 'add') {
+      // 回显并写入当前人的部门与联系方式,用于提交
+      state.form.deptId = userInfos.value?.deptId || null;
+      state.form.deptName = userInfos.value?.deptName || '';
+      state.form.phone = userInfos.value?.phone || '';
+    }
+    state.dialog.isShowDialog = true;
+  };
+
+  // 关闭弹窗
+  const closeDialog = () => {
+    state.form = defaultFormFields;
+    licenseNumberFileList.value = [];
+    animalTestDateFileList.value = [];
+    geneIdentificationFileList.value = [];
+    cageAppointFileList.value = [];
+    ethicsCheckFileList.value = [];
+    ethicsAdviceFileList.value = [];
+    safePromiseStatus.value = false;
+    state.dialog.isShowDialog = false;
+  };
+  // 取消
+  const onCancel = () => {
+    closeDialog();
+  };
+
+  const beforeAvatarFileUpload = (file: { size: number }) => {
+    let isLt10m = file.size / 1024 / 1024 / 20 < 1;
+    if (!isLt10m) {
+      ElMessage.error('上传文件大小不能超过 20MB!');
+      return false;
+    }
+    return true;
+  };
+
+  const handleRemove = (type: UploadFileType) => {
+    if (type === UploadFileType.LICENSE_NUMBER) {
+      licenseNumberFileList.value = [];
+      state.form.licenseNumberFile = [];
+    } else if (type === UploadFileType.ANIMAL_TEST_DATE) {
+      animalTestDateFileList.value = [];
+      state.form.animalTestDateFile = [];
+    } else if (type === UploadFileType.GENE_IDENTIFICATION_FILE) {
+      geneIdentificationFileList.value = [];
+      state.form.geneIdentificationFile = [];
+    } else if (type === UploadFileType.CAGE_APPOINT_FILE) {
+      cageAppointFileList.value = [];
+      state.form.cageAppointFile = [];
+    } else if (type === UploadFileType.ETHICS_CHECK_FILE) {
+      ethicsCheckFileList.value = [];
+      state.form.ethicsCheckFile = [];
+    } else if (type === UploadFileType.ETHICS_ADVICE_FILE) {
+      ethicsAdviceFileList.value = [];
+      state.form.ethicsAdviceFile = [];
+    }
+  };
+
+  const handleSuccess = (res: { Data: string }, type: UploadFileType, file: UploadFile) => {
+    console.log('ressss', res, file);
+    if (type === UploadFileType.LICENSE_NUMBER) {
+      state.form.licenseNumberFile = [{ name: file.name, url: res?.Data }];
+    } else if (type === UploadFileType.ANIMAL_TEST_DATE) {
+      state.form.animalTestDateFile = [{ name: file.name, url: res?.Data }];
+    } else if (type === UploadFileType.GENE_IDENTIFICATION_FILE) {
+      state.form.geneIdentificationFile = [{ name: file.name, url: res?.Data }];
+    } else if (type === UploadFileType.CAGE_APPOINT_FILE) {
+      state.form.cageAppointFile = [{ name: file.name, url: res?.Data }];
+    } else if (type === UploadFileType.ETHICS_CHECK_FILE) {
+      state.form.ethicsCheckFile = [{ name: file.name, url: res?.Data }];
+    } else if (type === UploadFileType.ETHICS_ADVICE_FILE) {
+      state.form.ethicsAdviceFile = [{ name: file.name, url: res?.Data }];
+    }
+  };
+
+  const handlePreview = (file: UploadFile) => {
+    if (file.url) {
+      window.open(file.url, '_blank');
+    }
+  };
+
+  // 提交
+  const onSubmit = async () => {
+    expertDialogFormRef.value.validate(async (valid: boolean) => {
+      if (!valid) return;
+
+      if (!safePromiseStatus.value) {
+        ElMessage.error('请阅读并勾选安全承诺!');
+        return;
+      }
+
+      if (!state.form.maleNumber && !state.form.famaleNumber) {
+        ElMessage.error('请添加雄性或雌性数量!');
+        return;
+      }
+      // json 字符串化
+      state.form.age= JSON.stringify(state.form.age);
+      state.form.weight = JSON.stringify(state.form.weight);
+
+      const params = {
+        ...deepClone(state.form),
+        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(DateFormat),
+        comeTime: state.form.comeTime ? dayjs(state.form.comeTime).format(DateFormat) : '',
+        // 将范围对象转换为JSON字符串
+        age: JSON.stringify(state.form.age),
+        weight: JSON.stringify(state.form.weight),
+        licenseNumberFile: JSON.stringify(state.form.licenseNumberFile),
+        animalTestDateFile: JSON.stringify(state.form.animalTestDateFile),
+        geneIdentificationFile: JSON.stringify(state.form.geneIdentificationFile),
+        cageAppointFile: JSON.stringify(state.form.cageAppointFile),
+        ethicsCheckFile: JSON.stringify(state.form.ethicsCheckFile),
+        ethicsAdviceFile: JSON.stringify(state.form.ethicsAdviceFile),
+      };
+
+      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;
+}
+
+:deep(.el-checkbox) {
+  white-space: pre-wrap;
+}
+</style>

+ 72 - 12
src/view/animal/application/components/Application.vue

@@ -31,14 +31,22 @@
                 :rules="rules.variety" />
               <van-field v-model="state.form.levelName" label="饲养区域" placeholder="请选择" readonly is-link required :rules="rules.levelName"
                 @click="showLevelPicker = true" />
-              <van-field v-model="state.form.age" label="周龄" placeholder="请输入周龄" type="digit">
-                <template #button>
-                  <van-stepper v-model="state.form.age" :min="0" integer />
+              <van-field label="周龄" required :rules="rules.age">
+                <template #input>
+                  <div class="range-input-wrapper">
+                    <van-field v-model="state.form.age.min" placeholder="请输入" type="digit" class="range-input" />
+                    <span class="range-separator">至</span>
+                    <van-field v-model="state.form.age.max" placeholder="请输入" type="digit" class="range-input" />
+                  </div>
                 </template>
               </van-field>
-              <van-field v-model="state.form.weight" label="体重" placeholder="请输入体重" type="number">
-                <template #button>
-                  <van-stepper v-model="state.form.weight" :min="0" />
+              <van-field label="体重" required :rules="rules.weight">
+                <template #input>
+                  <div class="range-input-wrapper">
+                    <van-field v-model="state.form.weight.min" placeholder="请输入" type="number" class="range-input" />
+                    <span class="range-separator">至</span>
+                    <van-field v-model="state.form.weight.max" placeholder="请输入" type="number" class="range-input" />
+                  </div>
                 </template>
               </van-field>
               <van-field v-model="state.form.maleNumber" label="雄性" placeholder="雄性数量" type="digit">
@@ -69,7 +77,7 @@
               <van-field label="采购渠道" required :rules="rules.buyFrom">
                 <template #input>
                   <van-radio-group v-model="state.form.buyFrom" direction="horizontal">
-                    <van-radio :name="ProcurementChannels.PURCHASED_BY_OTHERS">动物房代购</van-radio>
+                    <van-radio style="margin-bottom: 10px" :name="ProcurementChannels.PURCHASED_BY_OTHERS">动物房代购</van-radio>
                     <van-radio :name="ProcurementChannels.PURCHASED_BY_MYSELF">自行购买</van-radio>
                   </van-radio-group>
                 </template>
@@ -236,6 +244,30 @@ const rules = {
   ethicsAdviceFile: [{ required: true, message: '实验动物福利伦理审查意见表不能为空' }],
   licenseNumberFile: [{ required: true, message: '生产许可证副本不能为空' }],
   animalTestDateFile: [{ required: true, message: '近三个月动物质量检测证明不能为空' }],
+  age: [{
+    validator: () => {
+      const value = state.form.age
+      console.log("周龄范围:", value)
+      if (!value || value.min === null || value.min === '' || value.max === null || value.max === '') {
+        return '周龄范围不能为空';
+      } else if (value.min > value.max) {
+        return '最小周龄不能大于最大周龄';
+      }
+      return true;
+    },
+  }],
+  weight: [{
+    validator: () => {
+      const value = state.form.weight
+      console.log("体重范围:", value)
+      if (!value || value.min === null || value.min === '' || value.max === null || value.max === '') {
+        return '体重范围不能为空';
+      } else if (value.min > value.max) {
+        return '最小体重不能大于最大体重';
+      }
+      return true;
+    },
+  }],
 }
 
 const licenseNumberFileList = ref<any[]>([])
@@ -261,8 +293,8 @@ const defaultFormData = {
   startDate: '',
   maleNumber: 0,
   famaleNumber: 0,
-  weight: 0,
-  age: 0,
+  weight: { min: null, max: null },
+  age: { min: null, max: null },
   feedingDay: 0,
   buyFrom: ProcurementChannels.PURCHASED_BY_OTHERS,
   comeTime: '',
@@ -524,6 +556,7 @@ const onSubmit = async () => {
     }
   }
 
+
   // 验证必填文件
   if (!state.form.ethicsCheckFile.length) {
     showNotify({
@@ -541,6 +574,10 @@ const onSubmit = async () => {
     submitting.value = false // 重置提交状态
     return
   }
+  // json 字符串化
+  state.form.age= JSON.stringify(state.form.age);
+  state.form.weight = JSON.stringify(state.form.weight);
+
 
   const params = {
     ...deepClone(state.form),
@@ -556,8 +593,6 @@ const onSubmit = async () => {
     maleNumber: Number(state.form.maleNumber) || 0,
     famaleNumber: Number(state.form.famaleNumber) || 0,
     number: Number(state.form.number) || 1,
-    age: Number(state.form.age) || 0,
-    weight: state.form.weight ? parseFloat(String(state.form.weight)) : 0,
     feedingDay: Number(state.form.feedingDay) || 0,
     totalNumber: Number(state.form.totalNumber) || 0,
   }
@@ -681,6 +716,31 @@ defineExpose({
   }
 }
 
+.range-input-wrapper {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  
+  .range-input {
+    flex: 1;
+    
+    :deep(.van-field__body) {
+      padding: 0;
+    }
+    
+    :deep(.van-field__control) {
+      text-align: center;
+    }
+  }
+  
+  .range-separator {
+    margin: 0 8px;
+    color: #969799;
+    font-size: 14px;
+    flex-shrink: 0;
+  }
+}
+
 :deep(.van-checkbox) {
   white-space: pre-wrap;
   line-height: 1.6;
@@ -695,4 +755,4 @@ defineExpose({
   top: 16px;
   right: 16px;
 }
-</style>
+</style>

+ 29 - 6
src/view/animal/application/components/Detail.vue

@@ -22,8 +22,8 @@
             <van-field v-model="state.form.categoryName" label="动物类别" readonly placeholder="请选择" />
             <van-field v-model="state.form.variety" label="品种品系" readonly placeholder="请选择" />
             <van-field v-model="levelName" label="饲养区域" readonly placeholder="请选择" />
-            <van-field v-model="state.form.age" label="周龄" readonly type="digit" />
-            <van-field v-model="state.form.weight" label="体重" readonly type="digit" />
+            <van-field :modelValue="`${state.form.age?.min || 0} - ${state.form.age?.max || 0}`" label="周龄" readonly />
+            <van-field :modelValue="`${state.form.weight?.min || 0} - ${state.form.weight?.max || 0}`" label="体重" readonly />
             <van-field v-model="state.form.maleNumber" label="雄性" readonly type="digit" />
             <van-field v-model="state.form.famaleNumber" label="雌性" readonly type="digit" />
             <van-field v-model="state.form.totalNumber" label="合计" readonly type="digit" />
@@ -192,6 +192,22 @@ import { nextTick, reactive, ref, defineAsyncComponent, watch, computed } from '
   const expertDialogFormRef = ref()
   const animalTypeList = ref<any[]>([])
 
+  // 解析范围字段
+  const parseRangeField = (str: string) => {
+    try {
+      if (!str) return { min: 0, max: 0 };
+      // 处理双重转义的情况
+      const cleanStr = str.replace(/\\"/g, '"').replace(/^"|"$/g, '');
+      const parsed = JSON.parse(cleanStr);
+      return {
+        min: parsed.min || 0,
+        max: parsed.max || 0
+      };
+    } catch (e) {
+      console.warn('解析范围字段失败:', str, e);
+      return { min: 0, max: 0 };
+    }
+  };
 const categoryName = computed(() => {
   if (state.form.categoryId) {
     const category = animalTypeList.value.find((item) => item.id === state.form.categoryId)
@@ -221,8 +237,8 @@ const levelName = computed(() => {
       returnNumber: 0,
       maleNumber: 0,
       famaleNumber: 0,
-      weight: 0,
-      age: 0,
+      weight: { min: null, max: null },
+      age: { min: null, max: null },
       feedingDay: 0,
       buyFrom: ProcurementChannels.PURCHASED_BY_OTHERS,
       comeTime: '',
@@ -258,8 +274,14 @@ const levelName = computed(() => {
     const [err, res]: ToResponse = await to(platAnimalCageApplicationApi.getEntityById({ id: parseInt(code) }))
     if (err) return
     await nextTick()
-    state.form = {
+    // 处理范围字段的JSON字符串
+    const processedRow = {
       ...res?.data,
+      age: parseRangeField(res?.data?.age),
+      weight: parseRangeField(res?.data?.weight)
+    };
+    state.form = {
+      ...processedRow,
       approveStatus: ApproveStatusList.find((item) => item.id == res?.data?.approveStatus)?.name,
       createdTime: dayjs(res?.data?.createdTime).format('YYYY-MM-DD'),
       licenseNumberFile: res?.data?.licenseNumberFile ? JSON.parse(res?.data?.licenseNumberFile) : [],
@@ -270,6 +292,7 @@ const levelName = computed(() => {
       ethicsAdviceFile: res?.data?.ethicsAdviceFile ? JSON.parse(res?.data?.ethicsAdviceFile) : [],
       geneIdentificationFile: res?.data?.geneIdentificationFile ? JSON.parse(res?.data?.geneIdentificationFile) : [],
     }
+
   }
 
   const closeDialog = () => {
@@ -372,4 +395,4 @@ const levelName = computed(() => {
 :deep(.van-cell__title) {
   flex: 0 0 120px;
   }
-</style>
+</style>

+ 27 - 6
src/view/todo/component/plat_cage_applications.vue

@@ -43,11 +43,11 @@
       />
       <van-cell
         title="动物体重"
-        :value="state.form.weight"
+        :value="state.form.weight.min === state.form.weight.max ? state.form.weight.max : `${state.form.weight.min} - ${state.form.weight.max}`"
       />
       <van-cell
         title="动物周龄"
-        :value="state.form.age"
+        :value="state.form.age.min === state.form.age.max ? state.form.age.max : `${state.form.age.min} - ${state.form.age.max}`"
       />
       <van-cell
         title="饲养总天数"
@@ -188,7 +188,22 @@
       id: 40,
     },
   ]
-
+  // 解析范围字段
+  const parseRangeField = (str: string) => {
+    try {
+      if (!str) return { min: 0, max: 0 };
+      // 处理双重转义的情况
+      const cleanStr = str.replace(/\\"/g, '"').replace(/^"|"$/g, '');
+      const parsed = JSON.parse(cleanStr);
+      return {
+        min: parsed.min || 0,
+        max: parsed.max || 0
+      };
+    } catch (e) {
+      console.warn('解析范围字段失败:', str, e);
+      return { min: 0, max: 0 };
+    }
+  };
   const platAnimalCageApplicationApi = usePlatAnimalCageApplicationApi()
 
   const state = reactive({
@@ -202,8 +217,8 @@
       createdTime: '',
       maleNumber: 0,
       famaleNumber: 0,
-      weight: 0,
-      age: 0,
+      weight: { min: null, max: null },
+      age: { min: null, max: null },
       feedingDay: 0,
       buyFrom: '',
       comeTime: '',
@@ -269,8 +284,14 @@
     const [err, res]: ToResponse = await to(platAnimalCageApplicationApi.getEntityById({ id: parseInt(code) }))
     if (err) return
     await nextTick()
-    state.form = {
+    // 处理范围字段的JSON字符串
+    const processedRow = {
       ...res?.data,
+      age: parseRangeField(res?.data?.age),
+      weight: parseRangeField(res?.data?.weight)
+    };
+    state.form = {
+      ...processedRow,
       approveStatus: approveStatusList.find((item) => item.id == res?.data?.approveStatus)?.name,
       createdTime: dayjs(res?.data?.createdTime).format('YYYY-MM-DD'),
     }