浏览代码

feat: 新增管理员预约管理页面及相关功能

新增管理员预约管理模块,包含即将上机、正在上机、等待审核、预约记录四个标签页,添加首页仪器管理入口,新增管理员预约列表查询、强制下机接口,实现预约审核、取消、上机、下机等管理功能
张旭伟 1 月之前
父节点
当前提交
ca21bcc1ce

+ 8 - 0
src/api/instr/useAppoint.ts

@@ -67,5 +67,13 @@ export function useUseAppointApi() {
     getZunYiAppointRecord(query?: object) {
       return request.postRequest(basePath, 'ZunyiCustom', 'GetAppointmentList', query);
     },
+    // 管理员强制下机(不关闭终端)
+    adminForceOff(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentAppointment', 'AdminForceOff', query);
+    },
+    // 管理员查看全部预约列表
+    adminGetAppointList(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentAppointment', 'AdminGetAppointList', query);
+    },
   };
 }

二进制
src/assets/img/强制下机.png


+ 8 - 0
src/router.ts

@@ -98,6 +98,14 @@ const routes = [
       title: '上机详情'
     }
   },
+  {
+    name: 'adminInProgress',
+    path: '/admin-in-progress',
+    component: () => import('/@/view/instr/appointList/adminAppointList/index.vue'),
+    meta: {
+      title: '预约管理'
+    }
+  },
   {
     name: 'appoint',
     path: '/inst/appoint',

+ 4 - 0
src/view/home/index.vue

@@ -63,6 +63,10 @@
           <img src="../../assets/img/更多应用.png" alt="" />
           <p>巡检任务</p>
         </li>
+        <li @click="onRouterPush('/admin-in-progress')" v-auth="'h5-home-admin-in-progress'">
+          <img src="../../assets/img/强制下机.png" alt="" />
+          <p>仪器管理</p>
+        </li>
       </ul>
     </div>
     <div class="swipe-con flex justify-between mt10">

+ 85 - 0
src/view/instr/appointList/adminAppointList/adminAudit.vue

@@ -0,0 +1,85 @@
+<template>
+  <div class="panel-wrap">
+    <van-empty v-if="appointList.length == 0 && finished" mode="list" description="暂无待审核预约" />
+    <van-list v-else v-model:loading="listloading" :finished="finished" finished-text="没有更多了" @load="onLoad">
+      <div class="inst-item mb20" v-for="v in appointList" :key="v.id">
+        <div class="flex mb20"><div class="equ-tit"><span class="fontSize14 bold">设备名称:</span></div><div><span class="fontSize14">{{ v.instName }}</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><div><span class="fontSize14">{{ v.instNameEn }}</span></div></div>
+        <div class="flex mb20" v-if="v.projectName"><div class="equ-tit"><span class="fontSize14 bold">课题组:</span></div><div><span class="fontSize14">{{ v.projectName }}</span></div></div>
+        <div class="flex mb20"><div class="equ-tit"><span class="fontSize14 bold">所在位置:</span></div><div><span class="fontSize14">{{ v.placeAddress + '(' + (v.laboratoryName || '') + ')' }}</span></div></div>
+        <div class="flex flex-between">
+          <div class="flex"><div class="equ-tit"><span class="fontSize14 bold">开始时间:</span></div><div><span class="fontSize14">{{ v.startTime }}</span></div></div>
+          <span class="fontSize14 bold primary-color" v-auth="'h5-admin-cancel'" @click="handleCancelAppoint(v)">取消预约</span>
+        </div>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import { useUseAppointApi } from '/@/api/instr/useAppoint'
+const useAppointApi = useUseAppointApi()
+import instAppointApi from '/@/api/instr/instAppoint'
+import to from 'await-to-js'
+import { showConfirmDialog, showNotify } from 'vant'
+
+export default {
+  data() {
+    return {
+      appointList: [],
+      pageNum: 1,
+      pageSize: 10,
+      listloading: false,
+      finished: false
+    }
+  },
+  mounted() {
+    this.pageNum = 1
+    this.fetchList()
+  },
+  methods: {
+    onLoad() { this.pageNum++; this.fetchList() },
+    async fetchList() {
+      this.listloading = true
+      const [err, res] = await to(useAppointApi.adminGetAppointList({ pageNum: this.pageNum, pageSize: this.pageSize, appointStatus: ['10'] }))
+      this.listloading = false
+      if (err) return
+      if (res?.code === 200) {
+        this.appointList = this.pageNum === 1 ? [...(res.data?.list || [])] : [...this.appointList, ...(res.data?.list || [])]
+        if (this.pageNum * this.pageSize >= (res.data?.total || 0)) this.finished = true
+      }
+    },
+    handleCancelAppoint(row) {
+      showConfirmDialog({ title: '提示', message: '确认取消预约?' })
+        .then(() => { this.cancelAppoint(row.id) })
+    },
+    async cancelAppoint(id) {
+      const params = { id }
+      const [err, res] = await to(instAppointApi.userCancelAppoint(params))
+      if (err) return
+      if (res?.code === 200) {
+        this.pageNum = 1
+        this.appointList = []
+        this.finished = false
+        this.fetchList()
+        showNotify({ type: 'success', message: '取消成功' })
+      }
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+* { box-sizing: border-box; }
+.panel-wrap { height: 100%; }
+.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: 74px; }
+}
+.primary-color { color: #1989fa; }
+.bold { font-weight: bold; }
+.fontSize14 { font-size: 14px; }
+.flex { display: flex; }
+.flex-between { justify-content: space-between; }
+.mb20 { margin-bottom: 20px; }
+</style>

+ 193 - 0
src/view/instr/appointList/adminAppointList/adminProgress.vue

@@ -0,0 +1,193 @@
+<template>
+  <div class="panel-wrap">
+    <van-empty v-if="appointList.length == 0 && finished" mode="list" description="暂无正在上机" />
+    <van-list v-else v-model:loading="listloading" :finished="finished" finished-text="没有更多了" @load="onLoad">
+      <div class="inst-item mb20" v-for="v in appointList" :key="v.id" @click="toDetail(v)">
+        <div class="flex flex-between mb20"><span class="fontSize14 primary-color bold">{{ v.instName }}</span></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><div><span class="fontSize14">{{ v.instNameEn }}</span></div></div>
+        <div class="flex mb20" v-if="v.projectName"><div class="equ-tit"><span class="fontSize14 bold">课题组:</span></div><div><span class="fontSize14">{{ v.projectName }}</span></div></div>
+        <div class="flex mb20"><div class="equ-tit"><span class="fontSize14 bold">所在位置:</span></div><div><span class="fontSize14">{{ v.placeAddress + '(' + (v.laboratoryName || '') + ')' }}</span></div></div>
+        <div class="flex"><div class="equ-tit"><span class="fontSize14 bold">开始时间:</span></div><div><span class="fontSize14">{{ v.startTime }}</span></div></div>
+        <div class="flex mt20">
+          <div class="off-btn-group">
+            <van-button style="width:90px;height:30px;margin:0;font-size:14px" type="danger" size="small" v-auth="'h5-admin-normal-off'" @click.native.stop="handleIsGetOff(v)">正常下机</van-button>
+            <span class="off-hint">会关闭蓝牙终端</span>
+          </div>
+          <div class="off-btn-group">
+            <van-button style="width:90px;height:30px;margin:0;font-size:14px" type="danger" size="small" v-auth="'h5-admin-force-off'" @click.native.stop="handleAdminForceOff(v)">强制下机</van-button>
+            <span class="off-hint">不关闭蓝牙终端</span>
+          </div>
+        </div>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import { useUseAppointApi } from '/@/api/instr/useAppoint'
+const useAppointApi = useUseAppointApi()
+import instAppointApi from '/@/api/instr/instAppoint'
+import { useInstrApi } from '/@/api/instr'
+const instApi = useInstrApi()
+import { useUserInfo } from '/@/stores/userInfo'
+import { scanCodeWxUrl } from '/@/constants/pageConstants'
+import to from 'await-to-js'
+import { showConfirmDialog, showNotify } from 'vant'
+
+export default {
+  data() {
+    return {
+      appointList: [],
+      pageNum: 1,
+      pageSize: 10,
+      listloading: false,
+      finished: false,
+      curAppointInfo: {},
+      loading: false
+    }
+  },
+  mounted() {
+    this.pageNum = 1
+    this.fetchList()
+  },
+  methods: {
+    onLoad() { this.pageNum++; this.fetchList() },
+    async fetchList() {
+      this.listloading = true
+      const [err, res] = await to(useAppointApi.adminGetAppointList({ pageNum: this.pageNum, pageSize: this.pageSize, appointStatus: ['50'] }))
+      this.listloading = false
+      if (err) return
+      if (res?.code === 200) {
+        this.appointList = this.pageNum === 1 ? [...(res.data?.list || [])] : [...this.appointList, ...(res.data?.list || [])]
+        if (this.pageNum * this.pageSize >= (res.data?.total || 0)) this.finished = true
+      }
+    },
+    toDetail(v) { this.$router.push(`/onlineInfo?appointId=${v.id}`) },
+
+    calcTimeDiff(date) {
+      var new_date = new Date()
+      var old_date = new Date(date)
+      var difftime = new_date - old_date
+      return Math.round(difftime / 60000)
+    },
+    getShortWraningTime(time, unit) {
+      if (unit == 'hour') return time * 60
+      else if (unit == 'minute') return time
+    },
+    async handleIsGetOff(row) {
+      const [err, res] = await to(instApi.getSettingDetail({ instId: row.instId, code: 'InstCfgUse' }))
+      if (err) return
+      const useTime = this.calcTimeDiff(row.usedStartTime)
+      if (res?.data?.config.shortWarningEnable) {
+        const shortWraningTime = this.getShortWraningTime(res?.data?.config.shortWarning, res?.data?.config.shortWarningUnit)
+        if (useTime > shortWraningTime) {
+          if (row.controlMode === '30') {
+            this.handleBluetoothOffLine()
+          } else {
+            this.curAppointInfo = row
+            showConfirmDialog({ title: '提示', message: `本次上机时长共计${useTime}分钟,确定要下机吗?` })
+              .then(() => { this.getOff() })
+              .catch(() => {})
+          }
+        } else {
+          if (row.controlMode === '30') {
+            this.handleBluetoothOffLine()
+          } else {
+            this.curAppointInfo = row
+            showConfirmDialog({ title: '提示', message: `本次上机时长共计${useTime}分钟,不足${shortWraningTime}分钟,确定要下机吗?` })
+              .then(() => { this.getOff() })
+              .catch(() => {})
+          }
+        }
+      } else {
+        if (row.controlMode === '30') {
+          this.handleBluetoothOffLine()
+        } else {
+          this.curAppointInfo = row
+          showConfirmDialog({ title: '提示', message: `本次上机时长共计${useTime}分钟,确定要下机吗?` })
+            .then(() => { this.getOff() })
+            .catch(() => {})
+        }
+      }
+    },
+    async handleBluetoothOffLine() {
+      const res = await useUserInfo().scanCode()
+      this.handleDeCode(res, this.handleBluetoothDeCode)
+    },
+    async handleDeCode(code, func) {
+      if (this.loading) return
+      this.loading = true
+      const params = { content: code }
+      const [err, res] = await to(instApi.decode({ ...params }))
+      if (err) {
+        this.loading = false
+        showNotify({ type: 'danger', message: '解码失败,请重试' })
+        return
+      }
+      if (res?.code == 200 && res?.data?.content) {
+        func(res.data.content)
+      } else {
+        this.loading = false
+        showNotify({ type: 'danger', message: res?.message || '解码失败' })
+      }
+    },
+    async handleBluetoothDeCode(content) {
+      if (content.includes('id')) {
+        const terminal = JSON.parse(content)?.terminal
+        const url = scanCodeWxUrl(terminal, 'EndRun')
+        window.location.href = url
+      } else {
+        const url = scanCodeWxUrl(content, 'EndRun')
+        window.location.href = url
+      }
+    },
+    async getOff() {
+      const params = { appointId: this.curAppointInfo.id }
+      const [err, res] = await to(instAppointApi.getOff({ ...params }))
+      if (err) return
+      if (res.code == 200) {
+        this.pageNum = 1
+        this.appointList = []
+        this.finished = false
+        this.fetchList()
+        showNotify({ type: 'success', message: '正常下机成功' })
+      }
+    },
+
+    async handleAdminForceOff(row) {
+      showConfirmDialog({
+        title: '提示',
+        message: `确认强制下机【${row.userName}】的【${row.instName}】吗?此操作仅更改状态,不会关闭仪器终端。`
+      }).then(async () => {
+        const [err, res] = await to(useAppointApi.adminForceOff({ id: row.id }))
+        if (err) return
+        if (res.code == 200) {
+          this.pageNum = 1
+          this.appointList = []
+          this.finished = false
+          this.fetchList()
+          showNotify({ type: 'success', message: '强制下机成功' })
+        }
+      }).catch(() => {})
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+* { box-sizing: border-box; }
+.panel-wrap { height: 100%; }
+.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: 74px; }
+}
+.primary-color { color: #1989fa; }
+.bold { font-weight: bold; }
+.fontSize14 { font-size: 14px; }
+.flex { display: flex; }
+.flex-between { justify-content: space-between; }
+.mb20 { margin-bottom: 20px; }
+.mt20 { margin-top: 20px; }
+.off-btn-group { display: flex; flex-direction: column; align-items: center; margin-right: 10px; }
+.off-hint { font-size: 10px; color: #ee0a24; margin-top: 4px; }
+</style>

+ 64 - 0
src/view/instr/appointList/adminAppointList/adminRecord.vue

@@ -0,0 +1,64 @@
+<template>
+  <div class="panel-wrap">
+    <van-empty v-if="appointList.length == 0 && finished" mode="list" description="暂无预约记录" />
+    <van-list v-else v-model:loading="listloading" :finished="finished" finished-text="没有更多了" @load="onLoad">
+      <div class="inst-item mb20" v-for="v in appointList" :key="v.id">
+        <div class="flex flex-between mb20"><span class="fontSize14 primary-color bold">{{ v.instName }}</span></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><div><span class="fontSize14">{{ v.instNameEn }}</span></div></div>
+        <div class="flex mb20" v-if="v.projectName"><div class="equ-tit"><span class="fontSize14 bold">课题组:</span></div><div><span class="fontSize14">{{ v.projectName }}</span></div></div>
+        <div class="flex mb20"><div class="equ-tit"><span class="fontSize14 bold">所在位置:</span></div><div><span class="fontSize14">{{ v.placeAddress + '(' + (v.laboratoryName || '') + ')' }}</span></div></div>
+        <div class="flex"><div class="equ-tit"><span class="fontSize14 bold">开始时间:</span></div><div><span class="fontSize14">{{ v.startTime }}</span></div></div>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import { useUseAppointApi } from '/@/api/instr/useAppoint'
+const useAppointApi = useUseAppointApi()
+import to from 'await-to-js'
+
+export default {
+  data() {
+    return {
+      appointList: [],
+      pageNum: 1,
+      pageSize: 10,
+      listloading: false,
+      finished: false
+    }
+  },
+  mounted() {
+    this.pageNum = 1
+    this.fetchList()
+  },
+  methods: {
+    onLoad() { this.pageNum++; this.fetchList() },
+    async fetchList() {
+      this.listloading = true
+      const [err, res] = await to(useAppointApi.adminGetAppointList({ pageNum: this.pageNum, pageSize: this.pageSize, appointStatus: [] }))
+      this.listloading = false
+      if (err) return
+      if (res?.code === 200) {
+        this.appointList = this.pageNum === 1 ? [...(res.data?.list || [])] : [...this.appointList, ...(res.data?.list || [])]
+        if (this.pageNum * this.pageSize >= (res.data?.total || 0)) this.finished = true
+      }
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+* { box-sizing: border-box; }
+.panel-wrap { height: 100%; }
+.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: 74px; }
+}
+.primary-color { color: #1989fa; }
+.bold { font-weight: bold; }
+.fontSize14 { font-size: 14px; }
+.flex { display: flex; }
+.flex-between { justify-content: space-between; }
+.mb20 { margin-bottom: 20px; }
+</style>

+ 185 - 0
src/view/instr/appointList/adminAppointList/adminSoon.vue

@@ -0,0 +1,185 @@
+<template>
+  <div class="panel-wrap">
+    <van-empty v-if="appointList.length == 0 && finished" mode="list" description="暂无即将上机" />
+    <van-list v-else v-model:loading="listloading" :finished="finished" finished-text="没有更多了" @load="onLoad">
+      <div class="inst-item mb20" v-for="v in appointList" :key="v.id" @click="toDetail(v)">
+        <div class="flex flex-between mb20">
+          <span class="fontSize14 primary-color bold">{{ v.instName }}</span>
+          <div class="flex">
+            <van-button
+              style="width:80px;height:30px;margin:0;font-size:14px"
+              type="primary" size="small" :disabled="loading"
+              v-if="['10','20','30','50'].includes(v.controlMode)"
+              v-auth="'h5-admin-get-on'"
+              @click.native.stop="handleGetOn(v)">
+              {{ v.controlMode == '10' ? '上机' : '扫码上机' }}
+            </van-button>
+            <van-button
+              style="width:60px;height:30px;margin:0 0 0 10px;font-size:14px"
+              type="warning" size="small" :disabled="loading"
+              v-auth="'h5-admin-cancel'"
+              @click.native.stop="handleCancelAppoint(v)">
+              取消
+            </van-button>
+          </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><div><span class="fontSize14">{{ v.instNameEn }}</span></div></div>
+        <div class="flex mb20" v-if="v.projectName"><div class="equ-tit"><span class="fontSize14 bold">课题组:</span></div><div><span class="fontSize14">{{ v.projectName }}</span></div></div>
+        <div class="flex mb20"><div class="equ-tit"><span class="fontSize14 bold">所在位置:</span></div><div><span class="fontSize14">{{ v.placeAddress + '(' + (v.laboratoryName || '') + ')' }}</span></div></div>
+        <div class="flex"><div class="equ-tit"><span class="fontSize14 bold">开始时间:</span></div><div><span class="fontSize14">{{ v.startTime }}</span></div></div>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+import { useUseAppointApi } from '/@/api/instr/useAppoint'
+const useAppointApi = useUseAppointApi()
+import instAppointApi from '/@/api/instr/instAppoint'
+import { useInstrApi } from '/@/api/instr'
+const instApi = useInstrApi()
+import { useUserInfo } from '/@/stores/userInfo'
+import { scanCodeWxUrl } from '/@/constants/pageConstants'
+import to from 'await-to-js'
+import { showConfirmDialog, showNotify } from 'vant'
+
+export default {
+  data() {
+    return {
+      loading: false,
+      curAppointInfo: {},
+      appointList: [],
+      pageNum: 1,
+      pageSize: 10,
+      listloading: false,
+      finished: false
+    }
+  },
+  mounted() {
+    this.pageNum = 1
+    this.fetchList()
+  },
+  methods: {
+    onLoad() { this.pageNum++; this.fetchList() },
+    async fetchList() {
+      this.listloading = true
+      const [err, res] = await to(useAppointApi.adminGetAppointList({ pageNum: this.pageNum, pageSize: this.pageSize, appointStatus: ['20'] }))
+      this.listloading = false
+      if (err) return
+      if (res?.code === 200) {
+        this.appointList = this.pageNum === 1 ? [...(res.data?.list || [])] : [...this.appointList, ...(res.data?.list || [])]
+        if (this.pageNum * this.pageSize >= (res.data?.total || 0)) this.finished = true
+      }
+    },
+    toDetail(v) { this.$router.push(`/onlineInfo?appointId=${v.id}`) },
+    handleGetOn(row) { this.handleScanCode(row) },
+    async handleScanCode(row) {
+      this.curAppointInfo = row
+      if (row.controlMode == '10') {
+        showConfirmDialog({ title: '提示', message: '确认上机?' })
+          .then(() => {
+            if (this.loading) return
+            this.loading = true
+            this.handleGetOnByAppointId(this.curAppointInfo.id)
+          })
+          .catch(() => {})
+      } else if (row.controlMode == '20' || row.controlMode == '50') {
+        const that = this
+        const res = await useUserInfo().scanCode()
+        that.handleDeCode(res, row, that.handleCodeInfo)
+      } else if (row.controlMode == '30') {
+        const that = this
+        const res = await useUserInfo().scanCode()
+        that.handleDeCode(res, row, that.handleBluetoothDeCode)
+      }
+    },
+    async handleDeCode(code, row, func) {
+      if (this.loading) return
+      this.loading = true
+      const params = { content: code }
+      const [err, res] = await to(instApi.decode({ ...params }))
+      if (err) {
+        this.loading = false
+        showNotify({ type: 'danger', message: '解码失败,请重试' })
+        return
+      }
+      if (res?.code == 200 && res?.data?.content) {
+        func(res.data.content, row)
+      } else {
+        this.loading = false
+        showNotify({ type: 'danger', message: res?.message || '解码失败' })
+      }
+    },
+    async handleCodeInfo(content, appointRow) {
+      if (content.includes('id')) {
+        if (JSON.parse(content).id == appointRow.instId) {
+          this.handleGetOnByAppointId(appointRow.id)
+        } else {
+          this.loading = false
+          showNotify({ type: 'warning', message: '扫码机器不是预约机器' })
+        }
+      } else {
+        const [instErr, instRes] = await to(instApi.getIdByTerminal({ terminal: content }))
+        if (instErr) return
+        if (instRes.data.id == appointRow.instId) {
+          this.handleGetOnByAppointId(appointRow.id)
+        } else {
+          this.loading = false
+          showNotify({ type: 'warning', message: '扫码机器不是预约机器' })
+        }
+      }
+    },
+    async handleBluetoothDeCode(content) {
+      if (content.includes('id')) {
+        const terminal = JSON.parse(content)?.terminal
+        const url = scanCodeWxUrl(terminal, 'StartRun')
+        window.location.href = url
+      } else {
+        const url = scanCodeWxUrl(content, 'StartRun')
+        window.location.href = url
+      }
+    },
+    async handleGetOnByAppointId(id) {
+      const params = { appointId: id }
+      const [err, res] = await to(instAppointApi.getOn({ ...params }))
+      this.loading = false
+      if (err) return
+      if (res.code == 200) {
+        showNotify({ type: 'success', message: '上机成功' })
+        setTimeout(() => { this.$router.push(`/onlineInfo?appointId=${id}`) })
+      }
+    },
+    handleCancelAppoint(row) {
+      showConfirmDialog({ title: '提示', message: '确认取消预约?' })
+        .then(() => { this.cancelAppoint(row.id) })
+    },
+    async cancelAppoint(id) {
+      const params = { id }
+      const [err, res] = await to(instAppointApi.userCancelAppoint(params))
+      if (err) return
+      if (res?.code === 200) {
+        this.pageNum = 1
+        this.appointList = []
+        this.finished = false
+        this.fetchList()
+        showNotify({ type: 'success', message: '取消成功' })
+      }
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+* { box-sizing: border-box; }
+.panel-wrap { height: 100%; }
+.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: 74px; }
+}
+.primary-color { color: #1989fa; }
+.bold { font-weight: bold; }
+.fontSize14 { font-size: 14px; }
+.flex { display: flex; }
+.flex-between { justify-content: space-between; }
+.mb20 { margin-bottom: 20px; }
+</style>

+ 62 - 0
src/view/instr/appointList/adminAppointList/index.vue

@@ -0,0 +1,62 @@
+<template>
+  <div class="home">
+    <van-tabs v-model:active="active" @click-tab="onClickTab">
+      <van-tab title="即将上机">
+        <div class="list-container">
+          <admin-soon v-if="active === 0" />
+        </div>
+      </van-tab>
+      <van-tab title="正在上机">
+        <div class="list-container">
+          <admin-progress v-if="active === 1" />
+        </div>
+      </van-tab>
+      <van-tab title="等待审核">
+        <div class="list-container">
+          <admin-audit v-if="active === 2" />
+        </div>
+      </van-tab>
+      <van-tab title="预约记录">
+        <div class="list-container">
+          <admin-record v-if="active === 3" />
+        </div>
+      </van-tab>
+    </van-tabs>
+    <van-tabbar route :placeholder="true">
+      <van-tabbar-item replace to="/home" icon="wap-home-o">首页</van-tabbar-item>
+      <van-tabbar-item replace to="/instr-list" icon="printer">全部仪器</van-tabbar-item>
+    </van-tabbar>
+  </div>
+</template>
+
+<script>
+import AdminSoon from './adminSoon.vue'
+import AdminProgress from './adminProgress.vue'
+import AdminAudit from './adminAudit.vue'
+import AdminRecord from './adminRecord.vue'
+
+export default {
+  name: 'adminAppointList',
+  components: { AdminSoon, AdminProgress, AdminAudit, AdminRecord },
+  data() {
+    return {
+      active: 0
+    }
+  },
+  methods: {
+    onClickTab(e) {
+      this.active = e.name
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+* { box-sizing: border-box; }
+.home {
+  height: 100%; overflow: hidden; display: flex; flex-direction: column; padding-top: 10px;
+  .list-container { height: 100%; background-color: #f7f8fa; padding: 10px; overflow-y: auto; flex: 1; }
+  .van-tabs { flex: 1; height: 0; display: flex; flex-direction: column; }
+  :deep(.van-tabs__content) { flex: 1; height: 0; }
+  .van-tab__panel { height: 100%; }
+}
+</style>