Browse Source

Merge branch 'master' into release/昆明医科大学第一附属医院

LYK 2 months ago
parent
commit
e08863a9d5

+ 10 - 10
.env.development

@@ -10,21 +10,21 @@
 # 本地环境
 ENV = development
 # 本地环境接口地址
-VITE_API_URL = http://192.168.0.216:9954/
+VITE_API_URL = http://192.168.0.216:9957/
 VITE_API_WECHAT = /wechat/
 VITE_TENANT = default
 
 # 微服务地址
-VITE_ADMIN = dashoo.labsop.admin-34000
-VITE_WORKFLOW = dashoo.labsop.workflow-34000
-VITE_FINANCE = dashoo.labsop.finance-34000
-VITE_SCI = dashoo.labsop.scientific-34000
-VITE_INSTR_ADMIN = dashoo.labsop.apparatus-34000
-VITE_LEARNING = dashoo.labsop.learning-34000
-VITE_PLATFORM_API = dashoo.labsop.platform-34000
-VITE_LABORATORY: dashoo.labsop.laboratory-34000
+VITE_ADMIN = dashoo.labsop.admin-50000
+VITE_WORKFLOW = dashoo.labsop.workflow-50000
+VITE_FINANCE = dashoo.labsop.finance-50000
+VITE_SCI = dashoo.labsop.scientific-50000
+VITE_INSTR_ADMIN = dashoo.labsop.apparatus-50000
+VITE_LEARNING = dashoo.labsop.learning-50000
+VITE_PLATFORM_API = dashoo.labsop.platform-50000
+VITE_LABORATORY: dashoo.labsop.laboratory-50000
 
 #公共配置
 VITE_UPLOAD = http://192.168.0.218:9933/weedfs/upload
 #图片回显域名
-VITE_IMAGE_BASE_URL = http://lims-demo.labsop.cn:34000
+VITE_IMAGE_BASE_URL = http://lims-demo.labsop.cn:50000

+ 4 - 1
src/components/FlowTable.vue

@@ -57,7 +57,10 @@
   }
   // 详情工作流列表
   const getFlowInstance = async () => {
-    const [err, res]: ToResponse = await to(flowApi.getFlowInstance({ id: props.id, businessCode: props.businessCode, defCode: props.defCode }))
+    const [err, res]: ToResponse = await to(flowApi.getFlowInstance({ 
+      id: props.id, 
+      businessCode: props.businessCode, 
+      defCode: props.defCode }))
     if (err) return
     const arr = res?.data?.nodes || []
     apprList.value = arr

+ 2 - 2
src/view/Application.vue

@@ -74,7 +74,7 @@
             </el-form-item>
           </el-col>
           <el-col :span="12" class="mb16">
-            <el-form-item label="周龄" prop="age">
+            <el-form-item label="周龄(w)" 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" />
@@ -85,7 +85,7 @@
             </el-form-item>
           </el-col>
           <el-col :span="12" class="mb16">
-            <el-form-item label="体重" prop="weight">
+            <el-form-item label="体重(g)" 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" />

+ 100 - 36
src/view/animal/application/components/Application.vue

@@ -31,7 +31,7 @@
                 :rules="rules.variety" />
               <van-field v-model="state.form.levelName" label="饲养区域" placeholder="请选择" readonly is-link required :rules="rules.levelName"
                 @click="showLevelPicker = true" />
-              <van-field label="周龄" required :rules="rules.age">
+              <van-field label="周龄(w)" :rules="rules.age">
                 <template #input>
                   <div class="range-input-wrapper">
                     <van-field v-model="state.form.age.min" placeholder="请输入" type="digit" class="range-input" />
@@ -40,7 +40,7 @@
                   </div>
                 </template>
               </van-field>
-              <van-field label="体重" required :rules="rules.weight">
+              <van-field label="体重(g)" :rules="rules.weight">
                 <template #input>
                   <div class="range-input-wrapper">
                     <van-field v-model="state.form.weight.min" placeholder="请输入" type="number" class="range-input" />
@@ -49,7 +49,14 @@
                   </div>
                 </template>
               </van-field>
-              <van-field v-model="state.form.maleNumber" label="雄性" placeholder="雄性数量" type="digit">
+              <van-field
+                v-model="state.form.maleNumber"
+                label="雄性"
+                placeholder="雄性数量"
+                type="digit"
+                
+                :rules="rules.sexNumber"
+              >
                 <template #button>
                   <van-stepper v-model="state.form.maleNumber" :min="0" integer />
                 </template>
@@ -244,32 +251,98 @@ 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;
+  age: [
+    {
+      validator: () => {
+        const age = state.form.age
+        const weight = state.form.weight
+        const ageMin = age?.min
+        const ageMax = age?.max
+        const weightMin = weight?.min
+        const weightMax = weight?.max
+        const hasAgeMin = ageMin !== null && ageMin !== ''
+        const hasAgeMax = ageMax !== null && ageMax !== ''
+        const hasWeightMin = weightMin !== null && weightMin !== ''
+        const hasWeightMax = weightMax !== null && weightMax !== ''
+        const isAgeFilled = hasAgeMin && hasAgeMax
+        const isWeightFilled = hasWeightMin && hasWeightMax
+        const hasAgeAny = hasAgeMin || hasAgeMax
+        if (!hasAgeAny && !isWeightFilled) {
+          return '周龄或体重至少填写一个'
+        }
+        if (hasAgeAny && !isAgeFilled) {
+          return '周龄范围不能为空'
+        }
+        if (!isAgeFilled) {
+          return true
+        }
+        const min = Number(ageMin)
+        const max = Number(ageMax)
+        if (isNaN(min) || isNaN(max)) {
+          return '周龄范围不能为空'
+        }
+        if (max === 0) {
+          return '最大周龄不能为0'
+        }
+        if (min > max) {
+          return '最小周龄不能大于最大周龄'
+        }
+        return true
+      },
     },
-  }],
-  weight: [{
-    validator: () => {
-      const value = state.form.weight
-      const min = Number(value.min)
-      const max = Number(value.max)
-      console.log("体重范围:", value)
-      if (!value || isNaN(min) || isNaN(max)) {
-        return '体重范围不能为空'
-      } else if (min > max) {
-        return '最小体重不能大于最大体重'
-      }
-      return true;
+  ],
+  weight: [
+    {
+      validator: () => {
+        const age = state.form.age
+        const weight = state.form.weight
+        const ageMin = age?.min
+        const ageMax = age?.max
+        const weightMin = weight?.min
+        const weightMax = weight?.max
+        const hasAgeMin = ageMin !== null && ageMin !== ''
+        const hasAgeMax = ageMax !== null && ageMax !== ''
+        const hasWeightMin = weightMin !== null && weightMin !== ''
+        const hasWeightMax = weightMax !== null && weightMax !== ''
+        const isAgeFilled = hasAgeMin && hasAgeMax
+        const isWeightFilled = hasWeightMin && hasWeightMax
+        const hasWeightAny = hasWeightMin || hasWeightMax
+        if (!hasWeightAny && !isAgeFilled) {
+          return '周龄或体重至少填写一个'
+        }
+        if (hasWeightAny && !isWeightFilled) {
+          return '体重范围不能为空'
+        }
+        if (!isWeightFilled) {
+          return true
+        }
+        const min = Number(weightMin)
+        const max = Number(weightMax)
+        if (isNaN(min) || isNaN(max)) {
+          return '体重范围不能为空'
+        }
+        if (max === 0) {
+          return '最大体重不能为0'
+        }
+        if (min > max) {
+          return '最小体重不能大于最大体重'
+        }
+        return true
+      },
+    },
+  ],
+  sexNumber: [
+    {
+      validator: () => {
+        const male = Number(state.form.maleNumber) || 0
+        const female = Number(state.form.famaleNumber) || 0
+        if (!male && !female) {
+          return '雌性或雄性数量至少填写一个'
+        }
+        return true
+      },
     },
-  }],
+  ],
 }
 
 const licenseNumberFileList = ref<any[]>([])
@@ -610,15 +683,6 @@ const onSubmit = async () => {
     return
   }
 
-  if (!state.form.maleNumber && !state.form.famaleNumber) {
-    showNotify({
-      type: 'warning',
-      message: '请输入雄性或雌性数量!',
-    })
-    submitting.value = false // 重置提交状态
-    return
-  }
-
   // 验证自行购买时的必填项
   if (state.form.buyFrom === ProcurementChannels.PURCHASED_BY_MYSELF) {
     if (!state.form.licenseNumberFile.length) {

+ 5 - 3
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 :modelValue="state.form.age.min === state.form.age.max ? state.form.age.min : `${state.form.age.min} - ${state.form.age.max}`" label="周龄" readonly />
-            <van-field :modelValue="state.form.weight.min === state.form.weight.max ? state.form.weight.min : `${state.form.weight.min} - ${state.form.weight.max}`" label="体重" readonly />
+            <van-field :modelValue="state.form.age.min === state.form.age.max ? state.form.age.min : `${state.form.age.min} - ${state.form.age.max}`" label="周龄(w)" readonly />
+            <van-field :modelValue="state.form.weight.min === state.form.weight.max ? state.form.weight.min : `${state.form.weight.min} - ${state.form.weight.max}`" label="体重(g)" 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" />
@@ -291,7 +291,9 @@ const levelName = computed(() => {
       ethicsCheckFile: res?.data?.ethicsCheckFile ? JSON.parse(res?.data?.ethicsCheckFile) : [],
       ethicsAdviceFile: res?.data?.ethicsAdviceFile ? JSON.parse(res?.data?.ethicsAdviceFile) : [],
       geneIdentificationFile: res?.data?.geneIdentificationFile ? JSON.parse(res?.data?.geneIdentificationFile) : [],
-      totalNumber: res?.data?.maleNumber + res?.data?.famaleNumber
+      totalNumber: res?.data?.maleNumber + res?.data?.famaleNumber,
+      startDate: dayjs(res?.data?.createdTime).format('YYYY-MM-DD'),
+      comeTime: dayjs(res?.data?.comeTime).format('YYYY-MM-DD'),
     }
 
   }

+ 156 - 51
src/view/animal/applicationRemoval/components/addEdit.vue

@@ -53,59 +53,77 @@
               :rules="rules.isReturn"
             />
 
-            <template v-if="state.dialog.type === 'detail'">
+            <template v-if="state.dialog.type === 'detail' && turnBackInfoList.length">
               <h4 class="mb20 mt20">转回信息</h4>
-              <van-cell-group>
-                <van-field
-                  label="门禁卡是否归还"
-                  :model-value="accessCardReturnText"
-                  readonly
-                />
-                <van-field
-                  label="转回日期"
-                  :model-value="returnDateText"
-                  readonly
-                />
-                <van-field
-                  label="转回数量(雄性+雌性)"
-                  :model-value="returnAnimalNumberText"
-                  readonly
-                />
-                <van-field
-                  v-model="state.form.returnTransport"
-                  label="转回运输方式"
-                  readonly
-                />
-                <van-field
-                  v-model="state.form.notReturnReason"
-                  label="未返回动物情况说明"
-                  readonly
-                />
-              </van-cell-group>
-
+              <div class="simple-table">
+                <div class="simple-table__header">
+                  <div class="simple-table__cell" align="center">日期</div>
+                  <div class="simple-table__cell" align="center">转回数量</div>
+                  <div class="simple-table__cell" align="center">运输方式</div>
+                  <div class="simple-table__cell" align="center">门禁卡是否归还</div>
+                  <div class="simple-table__cell" align="center">审批记录</div>
+                </div>
+                <div
+                  v-for="(item, index) in turnBackInfoList"
+                  :key="index"
+                  class="simple-table__row"
+                >
+                  <div class="simple-table__cell" align="center">
+                    {{ formatYmd(item.returnDate) || '-' }}
+                  </div>
+                  <div class="simple-table__cell" align="center">
+                  {{ (item.returnMaleNumber || 0) + (item.returnFemaleNumber || 0) }}
+                  (雄:{{ item.returnMaleNumber || 0 }},雌:{{ item.returnFemaleNumber || 0 }})
+                  </div>
+                  <div class="simple-table__cell" align="center">
+                    {{ item.returnTransport || '-' }}
+                  </div>
+                  <div class="simple-table__cell" align="center">
+                    {{ item.accessCardReturn === 1 ? '是' : item.accessCardReturn === 0 ? '否' : '-' }}
+                  </div>
+                  <div class="simple-table__cell" align="center">
+                    <van-button
+                      v-if="item.approvalId || item.businessCode"
+                      size="small"
+                      type="primary"
+                      @click="onViewRebackApproval(item)"
+                    >
+                      详情
+                    </van-button>
+                    <span v-else>-</span>
+                  </div>
+                </div>
+              </div>
+            </template>
+            <template v-if="state.dialog.type === 'detail' && dieInfoList.length">
               <h4 class="mb20 mt20">淘汰上报信息</h4>
-              <van-cell-group>
-                <van-field
-                  label="淘汰日期"
-                  :model-value="dieTimeText"
-                  readonly
-                />
-                <van-field
-                  v-model="state.form.dieReason"
-                  label="淘汰原因"
-                  readonly
-                />
-                <van-field
-                  label="淘汰数量(雄性+雌性)"
-                  :model-value="dieAnimalNumberText"
-                  readonly
-                />
-                <van-field
-                  v-model="state.form.location"
-                  label="动物尸体存放位置"
-                  readonly
-                />
-              </van-cell-group>
+              <div class="simple-table">
+                <div class="simple-table__header-taotai">
+                  <div class="simple-table__cell" align="center">日期</div>
+                  <div class="simple-table__cell" align="center">淘汰数量</div>
+                  <div class="simple-table__cell" align="center">淘汰原因</div>
+                  <div class="simple-table__cell" align="center">尸体存放位置</div>
+                </div>
+                <div
+                  v-for="(item, index) in dieInfoList"
+                  :key="index"
+                  class="simple-table__row-taotai"
+                >
+                  <div class="simple-table__cell" align="center">
+                    {{ formatYmd(item.dieTime) || '-' }}
+                  </div>
+                  <div class="simple-table__cell" align="center">
+                    {{ (item.returnMaleNumber || 0) + (item.returnFemaleNumber || 0) }}
+                  (雄:{{ item.returnMaleNumber || 0 }},雌:{{ item.returnFemaleNumber || 0 }})
+                  </div>
+                  <div class="simple-table__cell" align="center">
+                    {{ item.dieReason || '-' }}
+                  </div>
+                  <div class="simple-table__cell" align="center">
+                    {{ item.location || '-' }}
+                  </div>
+                </div>
+              </div>
             </template>
             <div class="mt30 mb30 checkbox-wrapper">
               <van-checkbox v-model="safePromiseStatus" :disabled="isReadOnly">
@@ -162,6 +180,21 @@
                   @confirm="onReturnOptionConfirm" @cancel="showReturnPicker = false" />
     </van-popup>
 
+    <!-- 转回审批流程弹窗 -->
+    <van-popup v-model:show="showRebackApproval" position="bottom" :style="{ height: '80vh' }" round>
+      <div class="notice-content">
+        <h4 class="notice-title">转回审批流程</h4>
+        <div class="text">
+          <FlowTable
+            v-if="currentRebackApprovalId || currentRebackBusinessCode"
+            :id="currentRebackApprovalId || 0"
+            :businessCode="currentRebackBusinessCode"
+            defCode="plat_animal_takeway_reback"
+          />
+        </div>
+      </div>
+    </van-popup>
+
     <!-- 须知弹窗 -->
     <van-popup v-model:show="isShowNotice" position="bottom" round :closeable="true" :style="{ height: '70vh' }">
       <div class="notice-content">
@@ -217,6 +250,11 @@ const showCategoryPicker = ref<boolean>(false)
 const showDatePicker = ref<boolean>(false)
 const selectedDate = ref<Date | null>(null)
 const showReturnPicker = ref<boolean>(false)
+const turnBackInfoList = ref<any[]>([])
+const dieInfoList = ref<any[]>([])
+const showRebackApproval = ref<boolean>(false)
+const currentRebackApprovalId = ref<number | null>(null)
+const currentRebackBusinessCode = ref<string>('')
 
 const isReadOnly = computed(() => state.dialog.type === 'detail')
 
@@ -385,6 +423,9 @@ const loadRebackInfo = async (takeawayId?: number) => {
   const turnBackList = list.filter((item: any) => String(item.takeawayType) === '10')
   const dieList = list.filter((item: any) => String(item.takeawayType) === '20')
 
+  turnBackInfoList.value = turnBackList
+  dieInfoList.value = dieList
+
   const turnBack = turnBackList[turnBackList.length - 1]
   const die = dieList[dieList.length - 1]
 
@@ -416,6 +457,20 @@ const loadRebackInfo = async (takeawayId?: number) => {
   }
 }
 
+const onViewRebackApproval = (item: any) => {
+  const id = Number(item.approvalId || 0)
+  let businessCode = ''
+  if (item.businessCode) {
+    businessCode = String(item.businessCode)
+  } else if (item.id) {
+    businessCode = String(item.id)
+  }
+  if (!id && !businessCode) return
+  currentRebackApprovalId.value = id || null
+  currentRebackBusinessCode.value = businessCode
+  showRebackApproval.value = true
+}
+
 const openDialog = async (type: ActionType, sourceData?: TakeawayList) => {
   await getDicts()
   state.dialog.type = type
@@ -508,6 +563,8 @@ const closeDialog = () => {
   selectedDate.value = null
   state.loading = false
   state.dialog.isShowDialog = false
+  turnBackInfoList.value = []
+  dieInfoList.value = []
 }
 
 const onCancel = () => {
@@ -749,6 +806,54 @@ defineExpose({
       margin-top: 16px;
     }
   }
+
+  .simple-table {
+    width: 100%;
+    border: 1px solid #ebedf0;
+    border-radius: 8px;
+    overflow-x: auto;
+    overflow-y: hidden;
+    font-size: 12px;
+    background-color: #fff;
+    margin-top: 8px;
+  }
+
+  .simple-table__header,
+  .simple-table__row {
+    display: flex;
+    min-width: 550px;
+  }
+  .simple-table__header-taotai,
+  .simple-table__row-taotai {
+    display: flex;
+    min-width: 500px;
+  }
+
+  .simple-table__header-taotai,
+  .simple-table__header {
+    background-color: #f7f8fa;
+    font-weight: 600;
+  }
+
+  .simple-table__cell {
+    flex: 1;
+    padding: 8px;
+    border-bottom: 1px solid #ebedf0;
+    word-break: break-all;
+  }
+
+  .simple-table__header .simple-table__cell + .simple-table__cell,
+  .simple-table__row .simple-table__cell + .simple-table__cell {
+    border-left: 1px solid #ebedf0;
+  }
+
+  .simple-table__row:last-child .simple-table__cell {
+    border-bottom: none;
+  }
+
+  .simple-table__approval {
+    padding: 8px 0 16px;
+  }
 }
 
 :deep(.van-checkbox) {

+ 6 - 1
src/view/animal/applicationRemoval/components/dieModal.vue

@@ -14,7 +14,7 @@
                 @click="showDieDatePicker = true" />
 
               <van-field v-model="state.form.dieReason" label="淘汰原因" placeholder="请输入" type="textarea" rows="2" />
-              <van-field v-model="state.form.location" label="动物尸体存放位置" placeholder="请输入" type="textarea" rows="2" />
+              <van-field v-model="state.form.location" label="动物尸体存放位置" placeholder="请输入" type="textarea" rows="2"  />
 
               <van-field label="雄性">
                 <template #input>
@@ -94,6 +94,11 @@ const validateForm = () => {
     return false
   }
 
+  if (!state.form.location) {
+    showNotify({ type: 'warning', message: '请输入动物尸体存放位置' })
+    return false
+  }
+
   if (!state.form.returnMaleNumber && !state.form.returnFemaleNumber) {
     showNotify({ type: 'warning', message: '请至少填写一项淘汰数量' })
     return false

+ 6 - 10
src/view/animal/applicationRemoval/components/turnBack.vue

@@ -25,9 +25,9 @@
                   <van-stepper v-model="state.form.returnFemaleNumber" integer :min="0" />
                 </template>
               </van-field>
-
+<!-- 
               <van-field v-model="state.form.notReturnReason" label="未返回动物情况说明"
-                placeholder="请输入" type="textarea" rows="3" />
+                placeholder="请输入" type="textarea" rows="3" /> -->
 
               <van-field label="门禁卡是否归还">
                 <template #input>
@@ -112,14 +112,10 @@ const validateForm = () => {
       return false
     }
 
-    if (!state.form.notReturnReason) {
-      showNotify({ type: 'warning', message: '请输入未返回动物情况说明' })
-      return false
-    }
-    state.form.returnDate = ''
-    state.form.returnTransport = ''
-    state.form.returnMaleNumber = 0
-    state.form.returnFemaleNumber = 0
+    // if (!state.form.notReturnReason) {
+    //   showNotify({ type: 'warning', message: '请输入未返回动物情况说明' })
+    //   return false
+    // }
 
   return true
 }

+ 20 - 4
src/view/animal/applicationRemoval/index.vue

@@ -226,20 +226,36 @@
                     重新提交
                   </el-button>
                 </el-col>
-                <el-col :span="12">
+                <el-col
+                  v-if="item.approveStatus === ApplyLeaveApproveStatus.PASS && item.isReturn === 10"
+                  :span="12"
+                >
                   <el-button
                     style="width: 100%"
-                    v-if="item.approveStatus === ApplyLeaveApproveStatus.PASS"
                     type="primary"
                     @click.stop="handleReturn(item)"
                   >
                     转回
                   </el-button>
                 </el-col>
-                <el-col :span="12">
+                <el-col
+                  v-if="item.approveStatus === ApplyLeaveApproveStatus.PASS && item.isReturn === 10"
+                  :span="12"
+                >
+                  <el-button
+                    style="width: 100%"
+                    type="primary"
+                    @click.stop="handleDie(item)"
+                  >
+                    淘汰动物上报
+                  </el-button>
+                </el-col>
+                <el-col
+                  v-if="item.approveStatus === ApplyLeaveApproveStatus.PASS && item.isReturn !== 10"
+                  :span="24"
+                >
                   <el-button
                     style="width: 100%"
-                    v-if="item.approveStatus === ApplyLeaveApproveStatus.PASS"
                     type="primary"
                     @click.stop="handleDie(item)"
                   >

+ 373 - 0
src/view/instr/appointList/SampleAppointList.vue

@@ -371,3 +371,376 @@ defineExpose({
   cursor: pointer;
 }
 </style>
+<template>
+  <div class="panel-wrap">
+    <van-empty v-if="state.appointList.length === 0 && state.finished" description="暂无预约记录" />
+    <van-list v-else v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad"
+      class="data-list">
+      <div class="inst-item mb20" v-for="(v, index) in state.appointList" :key="index">
+        <div class="flex flex-between mb20">
+          <div>
+            <div class="mr10">
+              <span class="fontSize14 primary-color bold">{{ v.instName }}</span>
+            </div>
+          </div>
+          <!-- Status Tag moved here -->
+        </div>
+
+        <div class="flex mb20">
+          <div class="equ-tit">
+            <span class="fontSize14 bold">送样时间:</span>
+          </div>
+          <div>
+            <span class="fontSize14">{{ formatDate(new Date(v.deliverTime), 'YYYY-mm-dd HH:MM') }}</span>
+          </div>
+        </div>
+        <div class="flex mb20">
+          <div class="equ-tit">
+            <span class="fontSize14 bold">检测时间:</span>
+          </div>
+          <div>
+            <span class="fontSize14">{{ formatDate(new Date(v.testTime), 'YYYY-mm-dd HH:MM') }}</span>
+          </div>
+        </div>
+        <div class="flex mb20">
+          <div class="equ-tit">
+            <span class="fontSize14 bold">样品数:</span>
+          </div>
+          <div>
+            <span class="fontSize14">{{ v.sampleNum }}</span>
+          </div>
+        </div>
+        <div class="flex mb20">
+          <div class="equ-tit">
+            <span class="fontSize14 bold">申请人:</span>
+          </div>
+          <div>
+            <span class="fontSize14">{{ v.userName }}</span>
+          </div>
+        </div>
+        <div class="flex mb20">
+          <div class="equ-tit">
+            <span class="fontSize14 bold">状态:</span>
+          </div>
+          <van-tag :type="getStatusType(v.deliverStatus)">{{ setStatus(v.deliverStatus) }}</van-tag>
+        </div>
+        <!-- Detection Info Clickable -->
+        <div class="flex mb20" v-if="v.sampleItem !== '[]' || v.testSampleItem">
+          <div class="equ-tit">
+            <span class="fontSize14 bold">检测信息:</span>
+          </div>
+          <div>
+            <span v-if="v.sampleItem" class="fontSize14 primary-color pointer mr10"
+              @click="showSampleItem(v.sampleItem, '预约信息')">预约检测项目</span>
+            <span v-if="v.testSampleItem" class="fontSize14 primary-color pointer"
+              @click="showSampleItem(v.testSampleItem, '实测信息')">实测检测项目</span>
+          </div>
+        </div>
+
+        <div class="flex mb20" v-if="v.testResultName">
+          <div class="equ-tit">
+            <span class="fontSize14 bold">检测结果:</span>
+          </div>
+          <div style="flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;">
+            <span class="fontSize14 primary-color pointer">
+              <a :href="v.testResult" target="_blank" class="file-link">
+                {{ v.testResultName }}
+              </a>
+            </span>
+          </div>
+        </div>
+
+        <div v-if="v.order">
+          <van-collapse v-model="activeNames">
+            <van-collapse-item title="收费明细" :name="v.id">
+              <div class="flex mb10">
+                <div class="equ-tit">
+                  <span class="fontSize14 bold">计费编号:</span>
+                </div>
+                <div>
+                  <span class="fontSize14">{{ v.order.fboCode }}</span>
+                </div>
+              </div>
+              <div class="flex mb10">
+                <div class="equ-tit">
+                  <span class="fontSize14 bold">收费:</span>
+                </div>
+                <div>
+                  <span class="fontSize14 price">¥{{ v.order.fboActualAmount }}</span>
+                </div>
+              </div>
+              <div class="flex mb10">
+                <div class="equ-tit">
+                  <span class="fontSize14 bold">预估费用:</span>
+                </div>
+                <div>
+                  <span class="fontSize14 price">¥{{ v.order.fboExpectedAmount }}</span>
+                </div>
+              </div>
+            </van-collapse-item>
+          </van-collapse>
+        </div>
+
+        <!-- Cancel Button moved to bottom -->
+        <div class="flex mt20" style="justify-content: flex-end;"
+          v-if="v.deliverStatus == '10'">
+          <van-button class="scan-txt" plain type="danger" size="small" @click.stop="handleCancelAppoint(v)">
+            取消预约
+          </van-button>
+        </div>
+      </div>
+    </van-list>
+
+    <!-- Dialog for Sample Items -->
+    <van-dialog v-model:show="itemsDialogShow" :title="dialogTitle">
+      <div class="dialog-content">
+        <div class="flex mb10 flex-col" style="padding: 10px;">
+          <div class="flex mb4" v-for="(test, idx) in currentSampleItems" :key="idx">
+            <span class="fontSize14">{{ testItemInfo(test) }}</span>
+          </div>
+        </div>
+      </div>
+    </van-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { reactive, ref, onMounted } from 'vue'
+import { useSampleApi } from '/@/api/instr/sample'
+import to from 'await-to-js'
+import { formatDate } from '/@/utils/formatTime'
+import { showDialog, showNotify } from 'vant'
+
+const props = defineProps({
+  instId: {
+    type: Number,
+    default: 0
+  }
+})
+
+const sampleApi = useSampleApi()
+const activeNames = ref([])
+
+const state = reactive({
+  loading: false,
+  finished: false,
+  appointList: [] as any[],
+  queryForm: {
+    pageNum: 1,
+    pageSize: 10,
+    instId: props.instId
+  },
+  total: 0,
+})
+
+const toParse = (str: string) => {
+  try {
+    return JSON.parse(str) || []
+  } catch (e) {
+    return []
+  }
+}
+
+const testItemInfo = (test: any) => {
+  return `检测项:${test.name},数量:${test.count}`
+}
+
+const itemsDialogShow = ref(false)
+const currentSampleItems = ref<any[]>([])
+const dialogTitle = ref('检测信息')
+
+const showSampleItem = (itemsStr: string, title: string = '检测信息') => {
+  currentSampleItems.value = toParse(itemsStr)
+  dialogTitle.value = title
+  itemsDialogShow.value = true
+}
+
+const getStatusType = (key: string) => {
+  let type = 'default'
+  switch (key) {
+    case '10': // 待审核
+      type = 'warning'
+      break
+    case '20': // 已通过
+      type = 'success'
+      break
+    case '30': // 已驳回
+      type = 'danger'
+      break
+    case '50': // 已检测
+      type = 'primary'
+      break
+    case '40': // 已取消
+    case '11': // 已退回
+      type = 'default'
+      break
+  }
+  return type as 'primary' | 'success' | 'warning' | 'danger' | 'default'
+}
+
+const setStatus = (key: string) => {
+  let str = ''
+  switch (key) {
+    case '10':
+      str = '待审核'
+      break
+    case '11':
+      str = '已退回'
+      break
+    case '20':
+      str = '已通过'
+      break
+    case '30':
+      str = '已驳回'
+      break
+    case '40':
+      str = '已取消'
+      break
+    case '50':
+      str = '已检测'
+      break
+  }
+  return str
+}
+
+const handleCancelAppoint = (row: any) => {
+  showDialog({
+    title: '提示',
+    message: '确认取消预约?',
+    showCancelButton: true,
+  })
+    .then(async () => {
+      const params = { id: row.id }
+      const [err, res]: any = await to(sampleApi.userCancelAppoint(params))
+      if (err) return
+      if (res?.code === 200) {
+        showNotify({ type: 'success', message: '取消成功' })
+        state.queryForm.pageNum = 1
+        state.appointList = []
+        state.finished = false
+        onLoad()
+      }
+    })
+    .catch(() => {
+      // on cancel
+    })
+}
+const onLoad = async () => {
+  state.loading = true
+  state.queryForm.instId = props.instId
+
+  const [err, res]: any = await to(sampleApi.getList(state.queryForm))
+  state.loading = false
+  if (err) {
+    state.finished = true
+    return
+  }
+
+  if (res?.code === 200) {
+    const list = res?.data?.list || []
+    if (state.queryForm.pageNum === 1) {
+      state.appointList = list
+    } else {
+      state.appointList = [...state.appointList, ...list]
+    }
+
+    state.total = res?.data?.total || 0
+    state.queryForm.pageNum++
+
+    if (state.appointList.length >= state.total) {
+      state.finished = true
+    }
+  } else {
+    state.finished = true
+  }
+}
+
+onMounted(() => {
+  // onLoad will be triggered by van-list automatically initially
+})
+// Expose onLoad for parent component to call
+defineExpose({
+  onLoad: () => {
+    state.queryForm.pageNum = 1
+    state.finished = false
+    // Need to reset list? The onLoad usually appends... 
+    // If called from parent for refresh, we should probably reset.
+    // Let's modify logic slightly or just set pageNum to 1 and empty list
+    state.appointList = []
+    onLoad()
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+* {
+  box-sizing: border-box;
+}
+
+.panel-wrap {
+  height: 100%;
+
+  .data-list {
+    .inst-item {
+      border-radius: 10px;
+      padding: 15px;
+      box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
+      margin-bottom: 20px;
+      background-color: #fff;
+
+      .equ-tit {
+        width: 80px;
+        min-width: 80px;
+      }
+    }
+  }
+}
+
+.fontSize14 {
+  font-size: 14px;
+}
+
+.bold {
+  font-weight: bold;
+}
+
+.primary-color {
+  color: #1989fa;
+}
+
+.price {
+  color: #ee0a24;
+}
+
+.flex {
+  display: flex;
+}
+
+.flex-between {
+  justify-content: space-between;
+}
+
+.mb20 {
+  margin-bottom: 20px;
+}
+
+.mb10 {
+  margin-bottom: 10px;
+}
+
+.mr10 {
+  margin-right: 10px;
+}
+
+.mt20 {
+  margin-top: 20px;
+}
+
+.flex-col {
+  flex-direction: column;
+}
+
+.pointer {
+  cursor: pointer;
+}
+</style>

+ 1 - 0
src/view/instr/appointList/index.vue

@@ -39,6 +39,7 @@
           </van-tabs>
     <!-- </van-pull-refresh> -->
     <van-tabbar route :placeholder="true">
+      <van-tabbar-item replace to="/home" icon="wap-home-o">首页</van-tabbar-item>
       <van-tabbar-item replace to="/instr-follow" icon="star">收藏仪器</van-tabbar-item>
       <van-tabbar-item replace to="/instr-list" icon="printer">全部仪器</van-tabbar-item>
       <van-tabbar-item replace to="/instr-appoint-record" icon="label">我的预约</van-tabbar-item>

+ 441 - 507
src/view/instr/detail.vue

@@ -8,21 +8,11 @@
 -->
 <template>
   <div class="instr-detail">
-    <van-swipe
-      v-if="noticeInfo.noticeTitle"
-      class="my-swipe"
-      :autoplay="5000"
-      :show-indicators="false"
-      vertical
-      height="30"
-    >
+    <van-swipe v-if="noticeInfo.noticeTitle" class="my-swipe" :autoplay="5000" :show-indicators="false" vertical
+      height="30">
       <van-swipe-item @click="state.popupShow = true">
         <div class="flex">
-          <van-icon
-            name="volume-o"
-            class="mr4"
-            :size="20"
-          />
+          <van-icon name="volume-o" class="mr4" :size="20" />
           {{ noticeInfo.noticeTitle }}
         </div>
       </van-swipe-item>
@@ -30,11 +20,7 @@
     <header class="flex">
       <div class="h100">
         <!-- <img :showLoading="true" :src="state.instDetail.instPicture" width="80px" height="80px" /> -->
-        <van-image
-          width="80px"
-          height="80px"
-          :src="getImageUrl(state.instDetail.instPicture)"
-        />
+        <van-image width="80px" height="80px" :src="getImageUrl(state.instDetail.instPicture)" />
       </div>
       <div class="i-right ml10">
         <div class="h100 flex flex-top flex-column flex-between">
@@ -43,19 +29,11 @@
           </div>
           <footer>
             <div class="flex flex-top mb4 mt-auto">
-              <img
-                class="i-r-icon"
-                src="../../assets/img/user.png"
-                v-if="state.instDetail.instHeadName"
-              />
+              <img class="i-r-icon" src="../../assets/img/user.png" v-if="state.instDetail.instHeadName" />
               <div class="detailTxt">{{ state.instDetail.instHeadName }}</div>
             </div>
             <div class="flex flex-top">
-              <img
-                class="i-r-icon"
-                src="../../assets/img/address.png"
-                v-if="state.instDetail.placeAddress"
-              />
+              <img class="i-r-icon" src="../../assets/img/address.png" v-if="state.instDetail.placeAddress" />
               <div class="detailTxt">
                 {{ state.instDetail.placeAddress + setLaboratoryName(state.instDetail.laboratoryName) }}
               </div>
@@ -64,27 +42,12 @@
         </div>
       </div>
     </header>
-    <van-tabs
-      v-model:active="active"
-      @change="tabChange"
-    >
-      <van-tab
-        title="仪器信息"
-        name="info"
-      ></van-tab>
-      <van-tab
-        title="待审核"
-        name="approval"
-      ></van-tab>
-      <van-tab
-        title="历史申请"
-        name="history"
-      ></van-tab>
+    <van-tabs v-model:active="active" @change="tabChange">
+      <van-tab title="仪器信息" name="info"></van-tab>
+      <van-tab title="待审核" name="approval"></van-tab>
+      <van-tab title="历史申请" name="history"></van-tab>
     </van-tabs>
-    <div
-      v-if="active === 'info'"
-      class="content"
-    >
+    <div v-if="active === 'info'" class="content">
       <div class="card">
         <h4>仪器信息</h4>
         <ul>
@@ -130,18 +93,10 @@
         <h4>主要功能</h4>
         <div class="text">{{ state.instDetail.instFunctFeat }}</div>
       </div>
-      <div
-        class="card"
-        v-if="isNeedGrant"
-        @click="applicationAuth"
-      >
+      <div class="card" v-if="isNeedGrant" @click="applicationAuth">
         <h4>资质申请</h4>
       </div>
-      <div
-        class="card"
-        v-if="isNeedGrant"
-        @click="applyTraining"
-      >
+      <div class="card" v-if="isNeedGrant" @click="applyTraining">
         <h4>培训申请</h4>
       </div>
       <!-- <div class="card">
@@ -151,81 +106,42 @@
                 <a href="javascript: void(0);" @click="realDown(item.docName, item.docUrl)">{{ item.docName }}</a>
               </div>
             </template>
-          </div> -->
+</div> -->
     </div>
-    <van-list
-      v-else
-      v-model:loading="state.loading"
-      :finished="state.finished"
-      finished-text="没有更多了"
-      @load="onLoad"
-    >
-      <van-cell
-        v-for="item in state.list"
-        :key="item.id"
-      >
+    <van-list v-else v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad">
+      <van-cell v-for="item in state.list" :key="item.id">
         <template #default>
           <div class="list">
             <header class="flex justify-between">
               <strong class="title">{{ item.userName }}的预约</strong>
-              <van-tag
-                v-if="item.appointStatus == '10'"
-                type="default"
-              >
+              <van-tag v-if="item.appointStatus == '10'" type="default">
                 待审核
               </van-tag>
-              <van-tag
-                v-else-if="item.appointStatus == '11'"
-                type="warning"
-              >
+              <van-tag v-else-if="item.appointStatus == '11'" type="warning">
                 已退回
               </van-tag>
-              <van-tag
-                v-else-if="item.appointStatus == '20'"
-                type="success"
-              >
+              <van-tag v-else-if="item.appointStatus == '20'" type="success">
                 已通过
               </van-tag>
-              <van-tag
-                v-else-if="item.appointStatus == '30'"
-                type="danger"
-              >
+              <van-tag v-else-if="item.appointStatus == '30'" type="danger">
                 已驳回
               </van-tag>
-              <van-tag
-                v-else-if="item.appointStatus == '40'"
-                type="warning"
-              >
+              <van-tag v-else-if="item.appointStatus == '40'" type="warning">
                 已取消
               </van-tag>
-              <van-tag
-                v-else-if="item.appointStatus == '50'"
-                type="default"
-              >
+              <van-tag v-else-if="item.appointStatus == '50'" type="default">
                 已上机
               </van-tag>
-              <van-tag
-                v-else-if="item.appointStatus == '60'"
-                type="primary"
-              >
+              <van-tag v-else-if="item.appointStatus == '60'" type="primary">
                 已完成
               </van-tag>
-              <van-tag
-                v-else-if="item.appointStatus == '70'"
-                type="warning"
-              >
+              <van-tag v-else-if="item.appointStatus == '70'" type="warning">
                 审核超时
               </van-tag>
-              <van-tag
-                v-else-if="item.appointStatus == '80'"
-                type="danger"
-              >
+              <van-tag v-else-if="item.appointStatus == '80'" type="danger">
                 超时取消
               </van-tag>
-              <van-tag
-                v-else-if="item.appointStatus == '90'"
-                type="danger"
-              >
+              <van-tag v-else-if="item.appointStatus == '90'" type="danger">
                 超时未上机
               </van-tag>
             </header>
@@ -265,10 +181,7 @@
         </template>
       </van-cell>
     </van-list>
-    <van-back-top
-      target=".instr-detail"
-      bottom="10vh"
-    />
+    <van-back-top target=".instr-detail" bottom="10vh" />
   </div>
   <van-action-bar placeholder>
     <van-action-bar-icon
@@ -301,37 +214,17 @@
     />
   </van-action-bar>
   <!-- 通知 -->
-  <van-popup
-    v-model:show="state.popupShow"
-    round
-    :closeable="true"
-    position="top"
-    :style="{ padding: '20px' }"
-  >
+  <van-popup v-model:show="state.popupShow" round :closeable="true" position="top" :style="{ padding: '20px' }">
     <h4>{{ noticeInfo.noticeTitle }}</h4>
-    <div
-      class="notice-container"
-      v-html="noticeInfo.noticeContent"
-    ></div>
+    <div class="notice-container" v-html="noticeInfo.noticeContent"></div>
   </van-popup>
   <!-- 申请须知 -->
-  <van-popup
-    v-model:show="state.needToKnowShow"
-    round
-    :closeable="true"
-    position="bottom"
-    :style="{ height: '90vh' }"
-  >
+  <van-popup v-model:show="state.needToKnowShow" round :closeable="true" position="bottom" :style="{ height: '90vh' }">
     <div class="need-to-know">
       <h4 class="mt8 mb8">申请须知</h4>
       <p>{{ state.instDetail.applicationNotes }}</p>
       <footer>
-        <van-button
-          class="w100"
-          type="primary"
-          round
-          @click="confirmAppoint"
-        >
+        <van-button class="w100" type="primary" round @click="confirmAppoint">
           我知道了
         </van-button>
       </footer>
@@ -341,427 +234,468 @@
 </template>
 
 <script lang="ts" setup>
-  import to from 'await-to-js'
-  import { useRoute, useRouter } from 'vue-router'
-  import { ElMessageBox, ElMessage } from 'element-plus'
-  import { useInstrApi } from '/@/api/instr'
-  import { useInstDocApi } from '/@/api/instr/document'
-  import { onMounted, reactive, ref } from 'vue'
-  import { formatDate } from '/@/utils/formatTime'
-  import { showNotify } from 'vant'
-  import download from 'downloadjs'
-  import { useNoticeApi } from '/@/api/instr/notice'
-  import { useUseAppointApi } from '/@/api/instr/useAppoint'
-  import { useBlackApi } from '/@/api/blacklist'
-  import AddAuthDialog from './addAuthorization/index.vue'
-  import { useUserInfos } from '/@/hooks/useUserInfos'
-  import { useTrainingApi } from '/@/api/instr/inst/training'
-  import { getImageUrl } from '/@/utils/url'
-
-  const route = useRoute()
-  const router = useRouter()
-  const instApi = useInstrApi()
-  const instDocApi = useInstDocApi()
-  const noticeApi = useNoticeApi()
-  const useAppointApi = useUseAppointApi()
-  const blacklistApi = useBlackApi()
-  const trainingApi = useTrainingApi()
-  const active = ref('info')
-  const state = reactive({
-    detailsLoading: false,
-    instStatus: {
-      10: '正常',
-      20: '故障',
-      30: '报废',
-    },
-    instDetail: {} as any,
-    instFiles: [] as any[],
-    loading: false,
-    finished: false,
-    queryParams: {
-      pageNum: 1,
-      pageSize: 10,
-      instId: 0,
-      appointStatus: [],
-    },
-    list: [] as any[],
-    popupShow: false,
-    needToKnowShow: false,
+import to from 'await-to-js'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessageBox, ElMessage } from 'element-plus'
+import { useInstrApi } from '/@/api/instr'
+import { useInstDocApi } from '/@/api/instr/document'
+import { onMounted, reactive, ref } from 'vue'
+import { formatDate } from '/@/utils/formatTime'
+import { showNotify } from 'vant'
+import download from 'downloadjs'
+import { useNoticeApi } from '/@/api/instr/notice'
+import { useUseAppointApi } from '/@/api/instr/useAppoint'
+import { useBlackApi } from '/@/api/blacklist'
+import AddAuthDialog from './addAuthorization/index.vue'
+import { useUserInfos } from '/@/hooks/useUserInfos'
+import { useTrainingApi } from '/@/api/instr/inst/training'
+import { getImageUrl } from '/@/utils/url'
+
+const route = useRoute()
+const router = useRouter()
+const instApi = useInstrApi()
+const instDocApi = useInstDocApi()
+const noticeApi = useNoticeApi()
+const useAppointApi = useUseAppointApi()
+const blacklistApi = useBlackApi()
+const trainingApi = useTrainingApi()
+const active = ref('info')
+const state = reactive({
+  detailsLoading: false,
+  instStatus: {
+    10: '正常',
+    20: '故障',
+    30: '报废',
+  },
+  instDetail: {} as any,
+  instFiles: [] as any[],
+  loading: false,
+  finished: false,
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    instId: 0,
+    appointStatus: [],
+  },
+  list: [] as any[],
+  popupShow: false,
+  needToKnowShow: false,
+  appointType: '',
     appointType: '',
-  })
-  const noticeInfo = ref({ noticeTitle: '', noticeContent: '' })
+})
+const noticeInfo = ref({ noticeTitle: '', noticeContent: '' })
 
-  const isNeedGrant = ref(false)
+const isNeedGrant = ref(false)
 
-  const addAuthDialogRef = ref()
+const addAuthDialogRef = ref()
 
-  const { userInfos } = useUserInfos()
+const { userInfos } = useUserInfos()
 
-  const getNeedGrant = async (instId: number) => {
-    const [err, res]: ToResponse = await to(useAppointApi.getNeedGrant({ instId }))
-    if (err) return
-    isNeedGrant.value = res?.data
-  }
+const getNeedGrant = async (instId: number) => {
+  const [err, res]: ToResponse = await to(useAppointApi.getNeedGrant({ instId }))
+  if (err) return
+  isNeedGrant.value = res?.data
+}
 
-  // 获取仪器详情
-  const getDetail = async (id: number) => {
-    state.detailsLoading = true
-    const [err, res]: ToResponse = await to(instApi.getDetail({ id }))
-    state.detailsLoading = false
-    if (err) return
-    if (res?.code === 200) {
-      state.instDetail = res.data
-      getDocs()
-      getNotice()
-    }
+// 获取仪器详情
+const getDetail = async (id: number) => {
+  state.detailsLoading = true
+  const [err, res]: ToResponse = await to(instApi.getDetail({ id }))
+  state.detailsLoading = false
+  if (err) return
+  if (res?.code === 200) {
+    state.instDetail = res.data
+    getDocs()
+    getNotice()
   }
-  const getNotice = async () => {
-    const param = {
-      pageNum: 1,
-      pageSize: 1,
-      instId: state.instDetail.instId,
-    }
-    const [err, res]: ToResponse = await to(noticeApi.list({ ...param }))
-    if (err) return
-    noticeInfo.value = res?.data?.list.length > 0 ? res?.data?.list[0] : {}
+}
+const getNotice = async () => {
+  const param = {
+    pageNum: 1,
+    pageSize: 1,
+    instId: state.instDetail.instId,
   }
-  // 附件列表
-  const getDocs = async () => {
-    const [err, res]: ToResponse = await to(
-      instDocApi.list({ noPage: true, instId: state.instDetail.instId, docType: '' }),
-    )
-    if (err) return
-    state.instFiles = res?.data.list || []
+  const [err, res]: ToResponse = await to(noticeApi.list({ ...param }))
+  if (err) return
+  noticeInfo.value = res?.data?.list.length > 0 ? res?.data?.list[0] : {}
+}
+// 附件列表
+const getDocs = async () => {
+  const [err, res]: ToResponse = await to(
+    instDocApi.list({ noPage: true, instId: state.instDetail.instId, docType: '' }),
+  )
+  if (err) return
+  state.instFiles = res?.data.list || []
+}
+const realDown = (filename: string, fileurl: string) => {
+  let ua = navigator.userAgent.toLowerCase()
+  if (ua.includes('mac')) {
+    //iOS 将文件url转换为文件流 在下载
+    downloadFun(fileurl + '?response-content-type=application/octet-stream', filename)
+  } else {
+    //android 直接用插件的方法下载即可
+    download(fileurl, filename)
   }
-  const realDown = (filename: string, fileurl: string) => {
-    let ua = navigator.userAgent.toLowerCase()
-    if (ua.includes('mac')) {
-      //iOS 将文件url转换为文件流 在下载
-      downloadFun(fileurl + '?response-content-type=application/octet-stream', filename)
-    } else {
-      //android 直接用插件的方法下载即可
-      download(fileurl, filename)
-    }
-  }
-  // 创建a标签 实现下载
-  const downloadFun = async (blobFile, fileName) => {
-    let blob = new Blob([blobFile], {
-      type: 'application/pdf;charset=UTF-8',
-    })
+}
+// 创建a标签 实现下载
+const downloadFun = async (blobFile, fileName) => {
+  let blob = new Blob([blobFile], {
+    type: 'application/pdf;charset=UTF-8',
+  })
+  // @ts-ignore
+  if (window.navigator.msSaveOrOpenBlob) {
     // @ts-ignore
-    if (window.navigator.msSaveOrOpenBlob) {
-      // @ts-ignore
-      navigator.msSaveBlob(blob, fileName)
-    } else {
-      let link = document.createElement('a')
-      link.href = window.URL.createObjectURL(blob)
-      link.download = fileName
-      link.click()
-      window.URL.revokeObjectURL(link.href) //释放内存
-    }
-  }
-  const setLaboratoryName = (name) => {
-    return name ? `(${name})` : ''
+    navigator.msSaveBlob(blob, fileName)
+  } else {
+    let link = document.createElement('a')
+    link.href = window.URL.createObjectURL(blob)
+    link.download = fileName
+    link.click()
+    window.URL.revokeObjectURL(link.href) //释放内存
   }
-  const tabChange = (name: string) => {
-    if (name === 'history' || name === 'approval') {
-      state.finished = false
-      state.list = []
-      state.queryParams = {
-        pageNum: 1,
-        pageSize: 10,
-        instId: state.instDetail.id,
-        appointStatus: name === 'approval' ? ['10'] : [],
-      }
-      onLoad()
-    }
-  }
-  const onLoad = async () => {
-    state.loading = true
-    const [err, res]: ToResponse = await to(useAppointApi.getListByPermission(state.queryParams))
-    if (err) return
-    const list = res?.data?.list || []
-    for (const item of list) {
-      state.list.push(item)
-    }
-    state.loading = false
-    state.queryParams.pageNum++
-    if (list.length < state.queryParams.pageSize) {
-      state.finished = true
+}
+const setLaboratoryName = (name) => {
+  return name ? `(${name})` : ''
+}
+const tabChange = (name: string) => {
+  if (name === 'history' || name === 'approval') {
+    state.finished = false
+    state.list = []
+    state.queryParams = {
+      pageNum: 1,
+      pageSize: 10,
+      instId: state.instDetail.id,
+      appointStatus: name === 'approval' ? ['10'] : [],
     }
+    onLoad()
   }
-  const getBreachTypes = (row: any) => {
-    let breachTypes = <string[]>[]
-    if (row.isLate) breachTypes.push('迟到')
-    if (row.isOvertime) breachTypes.push('超时')
-    if (row.isLeaveEarly) breachTypes.push('早退')
-    if (row.isAbsence) breachTypes.push('爽约')
-    return breachTypes.join('、') || '-'
-  }
-  const getAppointTime = (row: any) => {
-    const startDate = new Date(row.startTime)
-    const endDate = new Date(row.endTime)
-    // 计算两个日期之间的时间差(以毫秒为单位)
-    const timeDifference = endDate.getTime() - startDate.getTime()
-    // 计算天数
-    const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24))
-    // 计算剩余的毫秒数
-    const remainingMilliseconds = timeDifference % (1000 * 60 * 60 * 24)
-    // 计算小时数
-    const hours = Math.floor(remainingMilliseconds / (1000 * 60 * 60))
-    // 计算剩余的毫秒数
-    const remainingMillisecondsAfterHours = remainingMilliseconds % (1000 * 60 * 60)
-    // 计算分钟数
-    const minutes = Math.floor(remainingMillisecondsAfterHours / (1000 * 60))
-    return `${days}天${hours}小时${minutes}分`
+}
+const onLoad = async () => {
+  state.loading = true
+  const [err, res]: ToResponse = await to(useAppointApi.getListByPermission(state.queryParams))
+  if (err) return
+  const list = res?.data?.list || []
+  for (const item of list) {
+    state.list.push(item)
   }
-  // 关注/取关
-  const handleFollowInst = async () => {
-    const [err] = state.instDetail.following
-      ? await to(instApi.unfollow({ ids: [state.instDetail.id] }))
-      : await to(instApi.follow({ ids: [state.instDetail.id] }))
-    if (err) return
-    showNotify({ type: 'success', message: !state.instDetail.following ? '收藏成功' : '已取消收藏' })
-    getDetail(state.instDetail.id)
+  state.loading = false
+  state.queryParams.pageNum++
+  if (list.length < state.queryParams.pageSize) {
+    state.finished = true
   }
-  const onAppoint = async (type: string) => {
+}
+const getBreachTypes = (row: any) => {
+  let breachTypes = <string[]>[]
+  if (row.isLate) breachTypes.push('迟到')
+  if (row.isOvertime) breachTypes.push('超时')
+  if (row.isLeaveEarly) breachTypes.push('早退')
+  if (row.isAbsence) breachTypes.push('爽约')
+  return breachTypes.join('、') || '-'
+}
+const getAppointTime = (row: any) => {
+  const startDate = new Date(row.startTime)
+  const endDate = new Date(row.endTime)
+  // 计算两个日期之间的时间差(以毫秒为单位)
+  const timeDifference = endDate.getTime() - startDate.getTime()
+  // 计算天数
+  const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24))
+  // 计算剩余的毫秒数
+  const remainingMilliseconds = timeDifference % (1000 * 60 * 60 * 24)
+  // 计算小时数
+  const hours = Math.floor(remainingMilliseconds / (1000 * 60 * 60))
+  // 计算剩余的毫秒数
+  const remainingMillisecondsAfterHours = remainingMilliseconds % (1000 * 60 * 60)
+  // 计算分钟数
+  const minutes = Math.floor(remainingMillisecondsAfterHours / (1000 * 60))
+  return `${days}天${hours}小时${minutes}分`
+}
+// 关注/取关
+const handleFollowInst = async () => {
+  const [err] = state.instDetail.following
+    ? await to(instApi.unfollow({ ids: [state.instDetail.id] }))
+    : await to(instApi.follow({ ids: [state.instDetail.id] }))
+  if (err) return
+  showNotify({ type: 'success', message: !state.instDetail.following ? '收藏成功' : '已取消收藏' })
+  getDetail(state.instDetail.id)
+}
+const onAppoint = async (type: stringtype: string) => {
+  state.appointType = type
     state.appointType = type
-    state.needToKnowShow = true
+  state.needToKnowShow = true
+}
+const confirmAppoint = async () => {
+  const [err, res]: ToResponse = await to(blacklistApi.checkInBlacklist())
+  if (err) return
+  if (res.data) {
+    showNotify({ type: 'danger', message: '您已被拉入黑名单,无法预约,请联系管理员' })
+    return
   }
-  const confirmAppoint = async () => {
-    const [err, res]: ToResponse = await to(blacklistApi.checkInBlacklist())
-    if (err) return
-    if (res.data) {
-      showNotify({ type: 'danger', message: '您已被拉入黑名单,无法预约,请联系管理员' })
-      return
-    }
+  if (state.appointType === 'sample') {
+    onRouterPush('/sample-appoint', { id: state.instDetail.id })
+  } else {
     if (state.appointType === 'sample') {
       onRouterPush('/sample-appoint', { id: state.instDetail.id })
     } else {
       onRouterPush('/instr-appoint', { id: state.instDetail.id })
-    }
-  }
-  const onRouterPush = (val: string, params?: any) => {
-    router.push({
-      path: val,
-      query: { ...params },
-    })
   }
+    }
+}
+const onRouterPush = (val: string, params?: any) => {
+  router.push({
+    path: val,
+    query: { ...params },
+  })
+}
 
-  const applicationAuth = async () => {
-    console.log('applicationAuth')
-    addAuthDialogRef.value.openDialog('personal', state.instDetail)
-  }
+const applicationAuth = async () => {
+  console.log('applicationAuth')
+  addAuthDialogRef.value.openDialog('personal', state.instDetail)
+}
 
-  const applyTraining = () => {
-    console.log('applyTraining')
-    ElMessageBox.confirm('确认发起培训申请?', '提示', {
-      confirmButtonText: '确认',
-      cancelButtonText: '取消',
-      type: 'warning',
+const applyTraining = () => {
+  console.log('applyTraining')
+  ElMessageBox.confirm('确认发起培训申请?', '提示', {
+    confirmButtonText: '确认',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(async () => {
+      const params = {
+        instCode: state.instDetail.instCode,
+        instId: state.instDetail.id,
+        instName: state.instDetail.instName,
+        userId: userInfos.value.id,
+        userName: userInfos.value.nickName,
+      }
+      const [err]: ToResponse = await to(trainingApi.add({ ...params }))
+      if (err) return
+      ElMessage.success('培训申请提交成功')
     })
-      .then(async () => {
-        const params = {
-          instCode: state.instDetail.instCode,
-          instId: state.instDetail.id,
-          instName: state.instDetail.instName,
-          userId: userInfos.value.id,
-          userName: userInfos.value.nickName,
-        }
-        const [err]: ToResponse = await to(trainingApi.add({ ...params }))
-        if (err) return
-        ElMessage.success('培训申请提交成功')
-      })
-      .catch(() => {})
-  }
+    .catch(() => { })
+}
 
-  onMounted(() => {
-    const id = route.query.id ? +route.query.id : 0
-    getDetail(id)
-    getNeedGrant(id)
-  })
+onMounted(() => {
+  const id = route.query.id ? +route.query.id : 0
+  getDetail(id)
+  getNeedGrant(id)
+})
 </script>
 
 <style lang="scss" scoped>
-  .instr-detail {
-    flex: 1;
-    overflow-y: auto;
-    background-color: #f7f8fa;
-    .my-swipe {
-      background-color: #fff;
-      height: 30px !important;
-      line-height: 30px !important;
-      :deep(.flex) {
+.instr-detail {
+  flex: 1;
+  overflow-y: auto;
+  background-color: #f7f8fa;
+
+  .my-swipe {
+    background-color: #fff;
+    height: 30px !important;
+    line-height: 30px !important;
+
+    :deep(.flex) {
+      height: 30px;
+      overflow: hidden;
+      padding: 0 12px;
+
+      span {
+        display: inline-block;
         height: 30px;
+        line-height: 30px;
+      }
+
+      span:first-child {
+        flex: 1;
+        white-space: nowrap;
         overflow: hidden;
-        padding: 0 12px;
-        span {
-          display: inline-block;
-          height: 30px;
-          line-height: 30px;
-        }
-        span:first-child {
-          flex: 1;
-          white-space: nowrap;
-          overflow: hidden;
-          text-overflow: ellipsis;
-        }
+        text-overflow: ellipsis;
       }
     }
-    > header {
-      height: auto;
-      min-height: 80px;
-      background-color: #fff;
-      padding: 12px;
+  }
+
+  >header {
+    height: auto;
+    min-height: 80px;
+    background-color: #fff;
+    padding: 12px;
+  }
+
+  .inst-info {
+    display: flex;
+  }
+
+  .i-right {
+    flex: 1;
+    font-size: 14px;
+    height: auto;
+    min-height: 80px;
+
+    .i-r-icon {
+      width: 15px;
+      height: 15px;
+      margin-right: 10px;
     }
-    .inst-info {
-      display: flex;
+  }
+
+  .detailTxt {
+    font-size: 12px;
+    color: #333333;
+    white-space: normal;
+    overflow: visible;
+    text-overflow: unset;
+    word-break: break-all;
+
+    &.name {
+      font-weight: bold;
+      font-size: 16px;
     }
-    .i-right {
-      flex: 1;
-      font-size: 14px;
-      height: auto;
-      min-height: 80px;
-      .i-r-icon {
-        width: 15px;
-        height: 15px;
-        margin-right: 10px;
+  }
+
+  .content {
+    padding: 10px;
+  }
+
+  .card {
+    border-radius: 4px;
+    background-color: #fff;
+    padding: 10px;
+    box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
+
+    &+.card {
+      margin-top: 10px;
+    }
+
+    h4 {
+      height: 18px;
+      line-height: 18px;
+      display: flex;
+      margin-bottom: 10px;
+
+      span {
+        font-weight: normal;
+        margin-left: auto;
+      }
+
+      &::before {
+        display: inline-block;
+        content: '';
+        width: 3px;
+        height: 18px;
+        background-color: #1c9bfd;
+        margin-right: 4px;
+        vertical-align: middle;
       }
     }
-    .detailTxt {
-      font-size: 12px;
-      color: #333333;
-      white-space: normal;
-      overflow: visible;
-      text-overflow: unset;
-      word-break: break-all;
-      &.name {
-        font-weight: bold;
-        font-size: 16px;
+
+    >ul {
+      li {
+        display: flex;
+        padding: 6px 0;
+
+        label {
+          width: 80px;
+          min-width: 80px;
+          color: #969799;
+        }
+
+        span {
+          word-break: break-all;
+        }
       }
     }
-    .content {
-      padding: 10px;
+
+    .text {
+      white-space: pre-wrap;
     }
-    .card {
-      border-radius: 4px;
+  }
+
+  .van-list {
+    padding: 10px;
+    border-radius: 4px;
+    flex: 1;
+
+    .van-cell {
       background-color: #fff;
-      padding: 10px;
-      box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
-      & + .card {
+
+      +.van-cell {
         margin-top: 10px;
       }
-      h4 {
-        height: 18px;
-        line-height: 18px;
-        display: flex;
-        margin-bottom: 10px;
-        span {
-          font-weight: normal;
-          margin-left: auto;
-        }
-        &::before {
-          display: inline-block;
-          content: '';
-          width: 3px;
-          height: 18px;
-          background-color: #1c9bfd;
-          margin-right: 4px;
-          vertical-align: middle;
-        }
-      }
-      > ul {
-        li {
-          display: flex;
-          padding: 6px 0;
-          label {
-            width: 80px;
-            min-width: 80px;
-            color: #969799;
-          }
-          span {
-            word-break: break-all;
-          }
-        }
+
+      header,
+      footer {
+        color: #333;
       }
-      .text {
-        white-space: pre-wrap;
+
+      .title {
+        flex: 1;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        text-align: left;
       }
-    }
-    .van-list {
-      padding: 10px;
-      border-radius: 4px;
-      flex: 1;
-      .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 {
-            display: inline-block;
-            width: 80px;
-            min-width: 80px;
-            color: rgb(120, 120, 120);
-          }
-        }
-        .time {
-          color: #f69a4d;
+
+      .inst-title {
+        color: #333;
+        text-align: left;
+        flex: 1;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        margin-top: 4px;
+
+        span:first-child {
+          display: inline-block;
+          width: 80px;
+          min-width: 80px;
+          color: rgb(120, 120, 120);
         }
       }
-    }
-  }
-  .btns {
-    flex: 1;
-    display: flex;
-    li {
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      justify-content: center;
-      padding: 0 8px;
-      font-size: 12px;
-      i {
-        margin-bottom: 4px;
+
+      .time {
+        color: #f69a4d;
       }
     }
   }
-  :deep(.follow .van-icon) {
-    color: #fdc33e;
-  }
-  .need-to-know {
-    height: calc(100% - 20px);
-    overflow: hidden;
+}
+
+.btns {
+  flex: 1;
+  display: flex;
+
+  li {
     display: flex;
     flex-direction: column;
-    padding: 10px 20px;
-    white-space: pre-wrap;
-    p {
-      flex: 1;
-      overflow-y: auto;
-    }
-    footer {
-      flex: 0 0 45px;
-      margin-top: 4px;
-      border-top: 1px solid #f7f8fa;
+    align-items: center;
+    justify-content: center;
+    padding: 0 8px;
+    font-size: 12px;
+
+    i {
+      margin-bottom: 4px;
     }
   }
+}
+
+:deep(.follow .van-icon) {
+  color: #fdc33e;
+}
+
+.need-to-know {
+  height: calc(100% - 20px);
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  padding: 10px 20px;
+  white-space: pre-wrap;
+
+  p {
+    flex: 1;
+    overflow-y: auto;
+  }
+
+  footer {
+    flex: 0 0 45px;
+    margin-top: 4px;
+    border-top: 1px solid #f7f8fa;
+  }
+}
 </style>

+ 371 - 0
src/view/instr/sampleAppoint.vue

@@ -441,4 +441,375 @@
   .mb10 {
     margin-bottom: 10px;
   }
+</style><!--
+ * @Author: wanglj wanglijie@dashoo.cn
+ * @Date: 2025-03-24 09:17:15
+ * @LastEditors: wanglj wanglijie@dashoo.cn
+ * @LastEditTime: 2025-03-28 11:44:38
+ * @FilePath: \labsop_h5\src\view\instr\detail.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+  <div class="container">
+    <van-form ref="formRef" required="auto">
+      <van-cell-group>
+        <van-field v-model="state.form.deliverTime" is-link readonly label="送样时间" placeholder="请选择计划送样时间"
+          @click="state.showDeliverTime = true" :rules="[{ required: true, message: '送样时间不能为空' }]" />
+        <van-field v-model="state.form.testTime" is-link readonly label="检测时间" placeholder="请选择检测时间"
+          @click="state.showTestTime = true" :rules="[{ required: true, message: '检测时间不能为空' }]" />
+        <van-field label="课题" placeholder="课题" readonly v-model="state.form.projectName"
+          :rules="[{ required: false }]" />
+        <van-field v-if="state.InstCfgCharge" label="经费卡" placeholder="请选择经费卡" is-link readonly
+          @click="state.showExpenseCard = true" v-model="state.form.expenseCardName"
+          :rules="[{ required: isRequiredExpense, message: '经费卡不能为空' }]"></van-field>
+        <van-field label="样品说明" placeholder="输入样品说明" v-model="state.form.sampleDesc"
+          :rules="[{ required: false }]"></van-field>
+        <van-field label="联系电话" placeholder="输入联系电话" v-model="state.form.userContact"
+          :rules="[{ required: false }]"></van-field>
+      </van-cell-group>
+
+      <div class="mt10 card-wrap" v-if="state.form.sampleItem.length > 0">
+        <h4>检测信息</h4>
+        <div class="card-item" v-for="(v, i) in state.form.sampleItem" :key="i">
+          <div class="flex flex-between mb10">
+            <span class="label">检测项:</span>
+            <span class="value bold">{{ v.name }}</span>
+          </div>
+          <div class="flex flex-between mb10 flex-center">
+            <span class="label">样本数量:</span>
+            <van-stepper v-model="v.count" min="0" integer />
+          </div>
+          <div class="flex flex-between mb10">
+            <span class="label">单价:</span>
+            <span class="value price">¥{{ v.price }}</span>
+          </div>
+          <div class="flex flex-between">
+            <span class="label">总金额:</span>
+            <span class="value price bold">¥{{ (v.count * v.price).toFixed(2) }}</span>
+          </div>
+        </div>
+      </div>
+
+      <CustomForm ref="customFormRef" :formData="state.form.sampleForm"></CustomForm>
+    </van-form>
+  </div>
+  <van-action-bar placeholder>
+    <van-action-bar-button class="w100" type="primary" text="保存" @click="onClickButton" :loading="state.loading" />
+  </van-action-bar>
+
+  <!-- 送样时间 -->
+  <van-popup v-model:show="state.showDeliverTime" position="bottom">
+    <van-date-picker v-model="state.currentDate" title="选择送样时间" @confirm="onConfirmDeliverDate"
+      @cancel="state.showDeliverTime = false" />
+  </van-popup>
+  <van-popup v-model:show="state.showDeliverTimeTime" position="bottom">
+    <van-time-picker v-model="state.currentTime" title="选择送样时间" @confirm="onConfirmDeliverTime"
+      @cancel="state.showDeliverTimeTime = false" />
+  </van-popup>
+
+  <!-- 检测时间 -->
+  <van-popup v-model:show="state.showTestTime" position="bottom">
+    <van-date-picker v-model="state.currentDateTest" title="选择检测时间" @confirm="onConfirmTestDate"
+      @cancel="state.showTestTime = false" />
+  </van-popup>
+  <van-popup v-model:show="state.showTestTimeTime" position="bottom">
+    <van-time-picker v-model="state.currentTimeTest" title="选择检测时间" @confirm="onConfirmTestTime"
+      @cancel="state.showTestTimeTime = false" />
+  </van-popup>
+
+  <!-- 选择经费卡 -->
+  <van-popup v-model:show="state.showExpenseCard" position="bottom">
+    <van-picker :columns="fundsList" :columns-field-names="{ text: 'finAccount', value: 'id' }"
+      @confirm="pickExpenseCard" @cancel="state.showExpenseCard = false" />
+  </van-popup>
+</template>
+
+<script lang="ts" setup>
+import to from 'await-to-js'
+import { useRoute, useRouter } from 'vue-router'
+import { useInstrApi } from '/@/api/instr'
+import { defineAsyncComponent, onMounted, reactive, ref, computed } from 'vue'
+import { formatDate } from '/@/utils/formatTime'
+import { showNotify, showToast } from 'vant'
+import { useProApi } from '/@/api/project'
+import { useSampleApi } from '/@/api/instr/sample'
+
+const CustomForm = defineAsyncComponent(() => import('/@/components/CustomForm.vue'))
+const route = useRoute()
+const router = useRouter()
+const projApi = useProApi()
+const instApi = useInstrApi()
+const sampleApi = useSampleApi()
+
+const fundsList = ref([])
+const formRef = ref()
+const customFormRef = ref()
+
+const state = reactive({
+  loading: false,
+  InstCfgCharge: false,
+  showDeliverTime: false,
+  showDeliverTimeTime: false,
+  currentDate: [],
+  currentTime: [],
+  showTestTime: false,
+  showTestTimeTime: false,
+  currentDateTest: [],
+  currentTimeTest: [],
+  showExpenseCard: false,
+  form: {
+    instId: 0,
+    deliverTime: '',
+    testTime: '',
+    projectName: '',
+    projectId: null,
+    expenseCardId: 0,
+    expenseCardName: '',
+    sampleDesc: '',
+    sampleItem: [] as any[],
+    userContact: '',
+    sampleForm: [] as any[],
+  },
+})
+const isRequiredExpense = computed(() => state.InstCfgCharge && state.form.sampleItem && state.form.sampleItem.length > 0)
+
+// Determine Default Time
+const now = new Date()
+const dateStr = formatDate(now, 'YYYY-mm-dd')
+const timeStr = formatDate(now, 'HH:MM')
+state.currentDate = dateStr.split('-')
+state.currentTime = timeStr.split(':')
+state.currentDateTest = dateStr.split('-')
+state.currentTimeTest = timeStr.split(':')
+state.form.deliverTime = ''
+state.form.testTime = ''
+
+
+const onConfirmDeliverDate = ({ selectedValues }) => {
+  state.currentDate = selectedValues
+  state.showDeliverTime = false
+  state.showDeliverTimeTime = true
+}
+const onConfirmDeliverTime = ({ selectedValues }) => {
+  state.currentTime = selectedValues
+  state.form.deliverTime = `${state.currentDate.join('-')} ${state.currentTime.join(':')}`
+  state.showDeliverTimeTime = false
+}
+
+const onConfirmTestDate = ({ selectedValues }) => {
+  state.currentDateTest = selectedValues
+  state.showTestTime = false
+  state.showTestTimeTime = true
+}
+const onConfirmTestTime = ({ selectedValues }) => {
+  state.currentTimeTest = selectedValues
+  state.form.testTime = `${state.currentDateTest.join('-')} ${state.currentTimeTest.join(':')}`
+  state.showTestTimeTime = false
+}
+
+
+const init = async () => {
+  getSampleConfig()
+  getChargeConfig()
+}
+
+// 送样配置信息
+const getSampleConfig = async () => {
+  const params = {
+    instId: state.form.instId,
+    code: 'InstCfgSample',
+  }
+  const [err, res]: ToResponse = await to(instApi.getSettingDetail({ ...params }))
+  if (err) return
+  state.form.sampleForm = res?.data?.config.sampleForm ? JSON.parse(res.data.config.sampleForm) : []
+}
+
+// 计费配置信息
+const getChargeConfig = async () => {
+  const params = {
+    instId: state.form.instId,
+    code: 'InstCfgCharge',
+  }
+  const [err, res]: ToResponse = await to(instApi.getSettingDetail({ ...params }))
+  if (err) return
+  state.InstCfgCharge = res?.data.config.enable && !res?.data.config.sampleFreeEnable
+  if (res?.data?.config?.sampleCountEnable) {
+    state.form.sampleItem = res?.data?.config?.sampleItemPrice || []
+    state.form.sampleItem = state.form.sampleItem.map((item) => {
+      return {
+        ...item,
+        count: 1,
+      }
+    })
+  }
+  await getMyProjectInfo()
+}
+
+const getMyProjectInfo = async () => {
+  const [err, res]: ToResponse = await to(projApi.getMySelfProjectGroup({}))
+  if (err) return
+  state.form.projectName = res?.data.pgName || ''
+  state.form.projectId = res?.data.id || null
+  if (state.form.projectId) {
+    getFundsData()
+    getTestList()
+  }
+}
+
+const getFundsData = async () => {
+  const [err, res]: ToResponse = await to(projApi.getFinanceAccountList({ projId: state.form.projectId }))
+  if (err) return
+  fundsList.value = res?.data.list ? [res?.data.list] : []
+  if (fundsList.value && fundsList.value.length > 0 && fundsList.value[0].length > 0) {
+    state.form.expenseCardId = fundsList.value[0][0].id
+    state.form.expenseCardName = fundsList.value[0][0].finAccount
+  }
+}
+
+const getTestList = async () => {
+  const [err, res]: ToResponse = await to(
+    sampleApi.getSampleTestOption({
+      instId: state.form.instId,
+      projectId: state.form.projectId,
+    }),
+  )
+  if (err) return
+  if (res.data.length > 0) {
+    state.form.sampleItem.forEach((item) => {
+      item.price = res.data.find((price) => price.name === item.name)?.price || 0
+    })
+  }
+}
+
+// 经费卡选择
+const pickExpenseCard = ({ selectedOptions }) => {
+  state.form.expenseCardId = selectedOptions[0].id
+  state.form.expenseCardName = selectedOptions[0].finAccount
+  state.showExpenseCard = false
+}
+
+const onClickButton = async () => {
+  state.loading = true
+  const [errValid] = await to(formRef.value.validate())
+  if (errValid) {
+    state.loading = false
+    return
+  }
+
+  if (isRequiredExpense.value && !state.form.expenseCardId) {
+    showNotify({ type: 'warning', message: '请选择经费卡' })
+    state.loading = false
+    return
+  }
+
+  const customForm = customFormRef.value.getFormData()
+  if (state.form.sampleForm.length > 0 && !customForm) {
+    state.loading = false
+    return
+  }
+
+  const sampleItem = state.form.sampleItem.map((item) => {
+    return {
+      name: item.name,
+      count: Number(item.count),
+    }
+  })
+
+  let params: any = Object.assign({}, state.form)
+  params.sampleItem = sampleItem
+  params.sampleForm = JSON.stringify(customForm || [])
+
+  const [err, res]: ToResponse = await to(sampleApi.add(params))
+  state.loading = false
+  if (err) return
+  if (res && res.code == 200) {
+    showToast({
+      type: 'success',
+      message: '提交成功'
+    })
+    router.back()
+  }
+}
+
+onMounted(() => {
+  state.form.instId = route.query.id ? +route.query.id : 0
+  init()
+})
+</script>
+
+<style lang="scss" scoped>
+.container {
+  flex: 1;
+  padding: 10px;
+  background-color: #f7f8fa;
+  overflow-y: auto;
+
+  h4 {
+    height: 18px;
+    line-height: 18px;
+    display: flex;
+    margin: 10px 0;
+
+    span {
+      font-weight: normal;
+      margin-left: auto;
+    }
+
+    &::before {
+      display: inline-block;
+      content: '';
+      width: 3px;
+      height: 18px;
+      background-color: #1c9bfd;
+      margin-right: 4px;
+      vertical-align: middle;
+    }
+  }
+}
+
+.card-wrap {
+  padding-bottom: 20px;
+}
+
+.card-item {
+  background: #fff;
+  padding: 15px;
+  border-radius: 8px;
+  margin-bottom: 10px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
+
+  .label {
+    color: #646566;
+    font-size: 14px;
+  }
+
+  .value {
+    color: #323233;
+    font-size: 14px;
+
+    &.bold {
+      font-weight: bold;
+    }
+
+    &.price {
+      color: #ee0a24;
+    }
+  }
+}
+
+.flex {
+  display: flex;
+}
+
+.flex-between {
+  justify-content: space-between;
+}
+
+.flex-center {
+  align-items: center;
+}
+
+.mb10 {
+  margin-bottom: 10px;
+}
 </style>

+ 2 - 2
src/view/todo/component/plat_animal_takeway_reback.vue

@@ -20,8 +20,8 @@
           </template>
         </van-cell>
       </template>
-
-      <van-cell v-else title="未返回动物情况说明" :value="state.form.notReturnReason || '-'" />
+<!-- 
+      <van-cell v-else title="未返回动物情况说明" :value="state.form.notReturnReason || '-'" /> -->
     </van-cell-group>
   </div>
 </template>

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

@@ -48,21 +48,22 @@
         :value="state.form.famaleNumber"
       />
       <van-cell
-        title="动物体重"
+        title="动物体重(g)"
         :value="state.form.weight.min === state.form.weight.max ? state.form.weight.max : `${state.form.weight.min} - ${state.form.weight.max}`"
       />
       <van-cell
-        title="动物周龄"
+        title="动物周龄(w)"
         :value="state.form.age.min === state.form.age.max ? state.form.age.max : `${state.form.age.min} - ${state.form.age.max}`"
       />
       <van-cell
         title="饲养总天数"
         :value="state.form.feedingDay"
       />
-      <van-cell
-        title="动物到达时间"
-        :value="state.form.comeTime"
-      />
+      <van-cell title="动物到达时间">
+        <template #value>
+          <span>{{ dayjs(state.form.comeTime).format('YYYY-MM-DD') }}</span>
+        </template>
+      </van-cell>
       <van-cell
         title="是否有特殊饲养要求"
         :value="checkSpecialFeedingRequirements"