Selaa lähdekoodia

Merge branch 'master' of https://code.dashoo.cn/dashoo/labsop_h5

LYK 1 kuukausi sitten
vanhempi
commit
34269a5db3

+ 2 - 0
components.d.ts

@@ -24,6 +24,8 @@ declare module 'vue' {
     VanCheckbox: typeof import('vant/es')['Checkbox']
     VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
     VanCol: typeof import('vant/es')['Col']
+    VanCollapse: typeof import('vant/es')['Collapse']
+    VanCollapseItem: typeof import('vant/es')['CollapseItem']
     VanDatePicker: typeof import('vant/es')['DatePicker']
     VanDialog: typeof import('vant/es')['Dialog']
     VanEmpty: typeof import('vant/es')['Empty']

+ 30 - 0
src/api/instr/sample.ts

@@ -0,0 +1,30 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-19 14:06:58
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-02-15 18:45:00
+ * @Description: file content
+ * @FilePath: \oms\api\system\user.js
+ */
+import request from '/@/utils/micro_request.js'
+const instrPath = import.meta.env.VITE_INSTR_ADMIN
+
+export function useSampleApi() {
+  return {
+    // 预添加送样预约
+    add(query?: object) {
+      return request.postRequest(instrPath, 'TusInstrumentSampleDelivery', 'Create', query)
+    },
+    // 获取送样选项
+    getSampleTestOption(query?: object) {
+      return request.postRequest(instrPath, 'TusInstrumentSampleDelivery', 'OptionByUser', query)
+    },
+    getList(query?: object) {
+      return request.postRequest(instrPath, 'TusInstrumentSampleDelivery', 'GetListByInst', query)
+    },
+    // 使用人取消送样预约
+    userCancelAppoint(query?: object) {
+      return request.postRequest(instrPath, 'TusInstrumentSampleDelivery', 'UserCancel', query)
+    },
+  }
+}

+ 8 - 0
src/router.ts

@@ -66,6 +66,14 @@ const routes = [
       title: '仪器预约'
     }
   },
+  {
+    name: 'sampleAppoint',
+    path: '/sample-appoint',
+    component: () => import('/@/view/instr/sampleAppoint.vue'),
+    meta: {
+      title: '送样预约'
+    }
+  },
   {
     name: 'instrCalendar',
     path: '/instr-calendar',

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

@@ -0,0 +1,373 @@
+<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>

+ 32 - 24
src/view/instr/appointList/index.vue

@@ -10,28 +10,33 @@
   <!-- 页面内容 -->
   <div class="home">
     <!-- <van-pull-refresh v-model="loading" @refresh="onRefresh"> -->
-    <van-tabs v-model:active="active" @click-tab="onClickTab">
-      <van-tab title="即将上机">
-        <div class="list-container">
-          <soon-geton v-if="active === 0" ref="soonGetonRef" />
-        </div>
-      </van-tab>
-      <van-tab title="正在上机">
-        <div class="list-container">
-          <in-progress v-if="active === 1" ref="inProgressRef" />
-        </div>
-      </van-tab>
-      <van-tab title="等待审核">
-        <div class="list-container">
-          <my-appoint v-if="active === 2" ref="myAppointRef" />
-        </div>
-      </van-tab>
-      <van-tab title="预约记录">
-        <div class="list-container">
-          <appoint-record v-if="active === 3" ref="recordRef" />
-        </div>
-      </van-tab>
-    </van-tabs>
+          <van-tabs v-model:active="active" @click-tab="onClickTab">
+            <van-tab title="即将上机">
+              <div class="list-container">
+                <soon-geton v-if="active === 0" ref="soonGetonRef" />
+              </div>
+            </van-tab>
+            <van-tab title="正在上机">
+              <div class="list-container">
+                <in-progress v-if="active === 1" ref="inProgressRef" />
+              </div>
+            </van-tab>
+            <van-tab title="等待审核">
+              <div class="list-container">
+                <my-appoint v-if="active === 2" ref="myAppointRef" />
+              </div>
+            </van-tab>
+            <van-tab title="预约记录">
+              <div class="list-container">
+                <appoint-record v-if="active === 3" ref="recordRef" />
+              </div>
+            </van-tab>
+            <van-tab title="送样预约">
+              <div class="list-container">
+                <sample-appoint-list v-if="active === 4" ref="sampleAppointListRef" />
+              </div>
+            </van-tab>
+          </van-tabs>
     <!-- </van-pull-refresh> -->
     <van-tabbar route :placeholder="true">
       <van-tabbar-item replace to="/home" icon="wap-home-o">首页</van-tabbar-item>
@@ -46,13 +51,16 @@
   import InProgress from './inProgress/index.vue'
   import MyAppoint from './myAppoint/index.vue'
   import AppointRecord from './appointRecord/index.vue'
+  import SampleAppointList from './SampleAppointList.vue'
+
   export default {
     name: 'appointList',
-    components: { SoonGeton, InProgress, MyAppoint, AppointRecord },
+    components: { SoonGeton, InProgress, MyAppoint, AppointRecord, SampleAppointList },
     data() {
       return {
         loading: false,
-        active: 0
+        active: 0,
+        activeTab: 0
       }
     },
     // onLoad(option) {

+ 449 - 528
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,67 +181,31 @@
         </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
-      icon="wap-home-o"
-      text="首页"
-      @click="onRouterPush('/home')"
-    />
-    <van-action-bar-icon
-      :icon="state.instDetail.following ? 'star' : 'star-o'"
-      :class="{ follow: state.instDetail.following }"
-      :text="state.instDetail.following ? '取消收藏' : '收藏'"
-      @click="handleFollowInst"
-    />
-    <van-action-bar-icon
-      icon="revoke"
-      text="返回"
-      @click="onRouterPush('/instr-list')"
-    />
-    <van-action-bar-button
-      v-if="state.instDetail.instStatus == '10' && state.instDetail.isAppointment == '10'"
-      type="primary"
-      text="立即预约"
-      @click="onAppoint"
-    />
+    <van-action-bar-icon icon="wap-home-o" text="首页" @click="onRouterPush('/home')" />
+    <van-action-bar-icon :icon="state.instDetail.following ? 'star' : 'star-o'"
+      :class="{ follow: state.instDetail.following }" :text="state.instDetail.following ? '取消收藏' : '收藏'"
+      @click="handleFollowInst" />
+    <van-action-bar-icon icon="revoke" text="返回" @click="onRouterPush('/instr-list')" />
+    <van-action-bar-button v-if="state.instDetail.instStatus == '10' && state.instDetail.isAppointment == '10'"
+      v-auth="'instr_appoint_btn'" type="primary" text="使用预约" @click="onAppoint('use')" />
+    <van-action-bar-button v-if="state.instDetail.instStatus == '10' && state.instDetail.isSampleDelivery == '10'"
+      v-auth="'instr_sampleDelivery_btn'" type="warning" text="送样预约" @click="onAppoint('sample')" />
   </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>
@@ -335,421 +215,462 @@
 </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,
-  })
-  const noticeInfo = ref({ noticeTitle: '', noticeContent: '' })
+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 isNeedGrant = ref(false)
+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: '',
+})
+const noticeInfo = ref({ noticeTitle: '', noticeContent: '' })
 
-  const addAuthDialogRef = ref()
+const isNeedGrant = ref(false)
 
-  const { userInfos } = useUserInfos()
+const addAuthDialogRef = ref()
 
-  const getNeedGrant = async (instId: number) => {
-    const [err, res]: ToResponse = await to(useAppointApi.getNeedGrant({ instId }))
-    if (err) return
-    isNeedGrant.value = res?.data
-  }
+const { userInfos } = useUserInfos()
 
-  // 获取仪器详情
-  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 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 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 getNotice = async () => {
+  const param = {
+    pageNum: 1,
+    pageSize: 1,
+    instId: state.instDetail.instId,
   }
-  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 [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)
   }
-  // 创建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})` : ''
-  }
-  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()
-    }
+    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 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 () => {
-    state.needToKnowShow = true
+}
+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: string) => {
+  state.appointType = type
+  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 {
     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>

+ 8 - 1
src/view/instr/list.vue

@@ -65,6 +65,13 @@
                     >
                       可预约
                     </van-tag>
+                    <van-tag
+                      v-if="v.instStatus == '10' && v.isSampleDelivery == '10'"
+                      type="warning"
+                      class="status-tag ml6"
+                    >
+                      可送样
+                    </van-tag>
                   </div>
                   <footer>
                     <div class="flex flex-top mb4 mt-auto">
@@ -572,7 +579,7 @@
 
     &.name {
       font-weight: bold;
-      font-size: 16px;
+      font-size: 14px;
     }
   }
 

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

@@ -0,0 +1,372 @@
+<!--
+ * @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>