Browse Source

feature:仪器增加预约状态 、 我的增加上下机、调整切换账号逻辑

liuzhenlin 4 ngày trước cách đây
mục cha
commit
05d3b6b23e

+ 3 - 0
src/api/login/index.ts

@@ -22,6 +22,9 @@ export function useLoginApi() {
     signOut: (query?: object) => {
       return request.postRequest(basePath, 'System', 'Logout', query)
     },
+    WeChatUnBindOpenId: (query?: object) => {
+      return request.postRequest(basePath, 'System', 'WeChatUnBindOpenId', query)
+    },
     getCaptchaImg: (query?: object) => {
       return request.postRequest(basePath, 'System', 'GetCaptchaImg', query)
     },

+ 9 - 1
src/constants/pageConstants.ts

@@ -215,7 +215,7 @@ export const SUPPORT_FILE_UPLOAD_TYPE_MAX = ".jpg,.jpeg,.png,.doc,.docx,.xls,.xl
 const APPID = 'wx979e7a671a498586'
 
 const PATH = 'pages/appointList/user'
-export const scanCodeWxUrl = (terminal: string, type: 'StartRun' | 'EndRun') => {
+export const scanCodeWxUrl = (terminal: string, type: InstSwitchType) => {
   // 将 terminal 和 type 作为 query 传入小程序,需进行编码避免特殊字符截断
   const query = encodeURIComponent(`terminal=${terminal}&type=${type}`)
   return `weixin://dl/business/?appid=${APPID}&path=${PATH}&query=${query}`
@@ -223,3 +223,11 @@ export const scanCodeWxUrl = (terminal: string, type: 'StartRun' | 'EndRun') =>
 
 // 用户 头像大小 / M
 export const userImgSize = 3
+
+// 开关设备类型
+export enum InstSwitchType {
+  OPEN = 'OPEN_INST', // 打开设备
+  CLOSE = 'CLOSE_INST', // 关闭设备
+  START_RUN = 'StartRun', // 开始运行
+  END_RUN = 'EndRun', // 结束运行
+}

+ 2 - 2
src/stores/userInfo.ts

@@ -150,14 +150,14 @@ export const useUserInfo = defineStore('userInfo', {
       this.openIdFlag = true
       const [err, res]: ToResponse = await to(sysApi.getOpenId({ code }))
       if (err) {
-        this.openIdFlag = false 
+        this.openIdFlag = false
         return
       }
       this.openId = res?.data?.openid || ''
       this.unionId = res?.data?.unionid || ''
       localStorage.setItem('openId', this.openId)
       localStorage.setItem('unionId', this.unionId)
-      this.openIdFlag = false 
+      this.openIdFlag = false
     },
     async getSdkConfig() {
       const [err, res]: ToResponse = await to(sysApi.getSDKTicket({ url: window.location.href }))

+ 10 - 1
src/view/instr/detail.vue

@@ -160,7 +160,10 @@
       finished-text="没有更多了"
       @load="onLoad"
     >
-      <van-cell v-for="item in state.list" :key="item.id">
+      <van-cell
+        v-for="item in state.list"
+        :key="item.id"
+      >
         <template #default>
           <div class="list">
             <header class="flex justify-between">
@@ -279,7 +282,13 @@
       :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"

+ 463 - 333
src/view/instr/list.vue

@@ -1,38 +1,86 @@
 <template>
   <div class="home">
     <div class="search-wrap">
-      <van-search placeholder="请输入仪器名称" v-model="state.queryForm.instName" :clearabled="true"
-        style="padding: 0;flex: 1;margin-right: 10px;"></van-search>
-      <van-button type="primary" style="width: 60px" shape="circle" size="small" @click="onSearch">
+      <van-search
+        placeholder="请输入仪器名称"
+        v-model="state.queryForm.instName"
+        :clearabled="true"
+        style="padding: 0; flex: 1; margin-right: 10px"
+      ></van-search>
+      <van-button
+        type="primary"
+        style="width: 60px"
+        shape="circle"
+        size="small"
+        @click="onSearch"
+      >
         搜索
       </van-button>
     </div>
     <div class="inst-list">
-      <van-empty v-if="state.instList.length == 0" image="search" description="暂无数据"></van-empty>
+      <van-empty
+        v-if="state.instList.length == 0"
+        image="search"
+        description="暂无数据"
+      ></van-empty>
       <div class="inst-wrap">
-        <van-list v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad">
-          <div class="inst-item mb40" v-for="(v, index) in state.instList" :key="index">
+        <van-list
+          v-model:loading="state.loading"
+          :finished="state.finished"
+          finished-text="没有更多了"
+          @load="onLoad"
+        >
+          <div
+            class="inst-item mb40"
+            v-for="(v, index) in state.instList"
+            :key="index"
+          >
             <div class="inst-info mt4 mb4">
-              <div class="i-left" @click="openDetail(v)">
+              <div
+                class="i-left"
+                @click="openDetail(v)"
+              >
                 <!-- <img :showLoading="true" :src="v.instPicture" width="100px" height="100%" /> -->
-                <van-image width="100px" height="100px" :src="getImageUrl(v.instPicture)" />
+                <van-image
+                  width="100px"
+                  height="100px"
+                  :src="getImageUrl(v.instPicture)"
+                />
               </div>
               <div class="i-right ml10">
-                <div class="h100 flex flex-top flex-column flex-between" @click="openDetail(v)">
+                <div
+                  class="h100 flex flex-top flex-column flex-between"
+                  @click="openDetail(v)"
+                >
                   <!-- <van-button @click="openAppoint(v)">预约</van-button> -->
                   <div class="flex flex-top mb4 ml2">
                     <div class="detailTxt name">{{ v.instName }}(编号{{ v.instCode }})</div>
                   </div>
                   <div class="flex flex-top mb4 ml2">
                     <div class="detailTxt name">{{ v.instNameEn }}</div>
+                    <van-tag
+                      v-if="v.instStatus == '10' && v.isAppointment == '10'"
+                      type="primary"
+                      class="status-tag ml6"
+                    >
+                      可预约
+                    </van-tag>
                   </div>
                   <footer>
                     <div class="flex flex-top mb4 mt-auto">
-                      <img class="i-r-icon" src="../../assets/img/user.png" v-if="v.instHeadName" />
+                      <img
+                        class="i-r-icon"
+                        src="../../assets/img/user.png"
+                        v-if="v.instHeadName"
+                      />
                       <div class="detailTxt">{{ v.instHeadName }}</div>
                     </div>
                     <div class="flex flex-top">
-                      <img class="i-r-icon" src="../../assets/img/address.png" v-if="v.placeAddress" />
+                      <img
+                        class="i-r-icon"
+                        src="../../assets/img/address.png"
+                        v-if="v.placeAddress"
+                      />
                       <div class="detailTxt">{{ v.placeAddress + setLaboratoryName(v.laboratoryName) }}</div>
                     </div>
                   </footer>
@@ -43,25 +91,51 @@
         </van-list>
       </div>
     </div>
-    <van-tabbar route :placeholder="true">
-      <van-tabbar-item replace to="/home" icon="wap-home-o">
+    <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
+        replace
+        to="/instr-follow"
+        icon="star"
+      >
         收藏仪器
       </van-tabbar-item>
-      <van-tabbar-item replace to="/instr-list" icon="printer">
+      <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
+        replace
+        to="/instr-appoint-record"
+        icon="label"
+      >
         我的预约
       </van-tabbar-item>
     </van-tabbar>
     <!-- 仪器详情 -->
-    <van-popup v-model:show="state.popupShow" round :closeable="true">
+    <van-popup
+      v-model:show="state.popupShow"
+      round
+      :closeable="true"
+    >
       <div class="detail-box">
         <div class="instNameTxt">{{ state.instDetail.instName }}</div>
-        <div v-if="state.instDetail.instName" class="dc-content">
+        <div
+          v-if="state.instDetail.instName"
+          class="dc-content"
+        >
           <div class="flex">
             <div class="label-item">仪器型号:</div>
             <div class="detailTxt">{{ state.instDetail.instNameEn }}</div>
@@ -98,20 +172,49 @@
             <div class="detailTxt">{{ state.instDetail.instSpecParam }}</div>
           </div>
           <div class="mb10">
-            <div class="detailTxt" text="主要功能及特色" customStyle="margin-bottom:10px" align="center" size="15" bold></div>
-            <div class="detailTxt" :text="state.instDetail.instFunctFeat" size="14"></div>
+            <div
+              class="detailTxt"
+              text="主要功能及特色"
+              customStyle="margin-bottom:10px"
+              align="center"
+              size="15"
+              bold
+            ></div>
+            <div
+              class="detailTxt"
+              :text="state.instDetail.instFunctFeat"
+              size="14"
+            ></div>
           </div>
           <div class="mb10">
-            <div class="detailTxt" text="主要附件及配置" customStyle="margin-bottom:10px" align="center" size="15" bold></div>
-            <div class="detailTxt" :text="state.instDetail.instAttConfig" size="14"></div>
+            <div
+              class="detailTxt"
+              text="主要附件及配置"
+              customStyle="margin-bottom:10px"
+              align="center"
+              size="15"
+              bold
+            ></div>
+            <div
+              class="detailTxt"
+              :text="state.instDetail.instAttConfig"
+              size="14"
+            ></div>
           </div>
         </div>
       </div>
     </van-popup>
     <!-- 实验室 -->
-    <van-popup v-model:show="state.showLab" position="bottom">
-      <van-picker :columns="state.laboratoryColumns" :columns-field-names="{ text: 'name', value: 'id' }"
-        @confirm="pickLab" @cancel="state.showLab = false" />
+    <van-popup
+      v-model:show="state.showLab"
+      position="bottom"
+    >
+      <van-picker
+        :columns="state.laboratoryColumns"
+        :columns-field-names="{ text: 'name', value: 'id' }"
+        @confirm="pickLab"
+        @cancel="state.showLab = false"
+      />
     </van-popup>
     <!-- <van-picker :show="showLab" :columns="laboratoryColumns" keyName="name" @cancel="showLab = false" @confirm="pickLab"></van-picker> -->
     <!-- <shutDown ref="shutDownRef"></shutDown> -->
@@ -119,363 +222,390 @@
 </template>
 
 <script lang="ts" setup>
-import to from 'await-to-js'
-import { onMounted, reactive } from 'vue'
-import { useRouter } from 'vue-router'
-import { useInstrApi } from '/@/api/instr'
-import { useBlackApi } from '/@/api/blacklist'
-import { usePositionApi } from '/@/api/instr/position'
-import { showNotify } from 'vant'
-import { getImageUrl } from '/@/utils/url'
-const props = defineProps({
-  following: {
-    type: String,
-    default: '20',
-  },
-})
-const router = useRouter()
-const instApi = useInstrApi()
-const blacklistApi = useBlackApi()
-const positionApi = usePositionApi()
-
-const state = reactive({
-  showLab: false,
-  laboratoryColumns: [],
-  instStatus: {
-    10: '正常',
-    20: '故障',
-    30: '报废',
-  },
-  instList: [],
-  current: 1,
-  queryForm: {
-    laboratoryId: 0,
-    laboratoryName: '',
-    pageNum: 1,
-    pageSize: 10,
-    instName: '',
-  },
-  popupShow: false,
-  instDetail: {} as any,
-  total: 0,
-  detailsLoading: false, //详情加载
-  loading: false,
-  finished: false,
-})
-const setLaboratoryName = (name) => {
-  return name ? `(${name})` : ''
-}
-const getPosition = () => {
-  Promise.all([positionApi.getLaboratoryList({ noPage: true })]).then(([lab]) => {
-    const instr_is_concat_lab = JSON.parse(localStorage.getItem('instr_is_concat_lab') || '{}')
-    if (instr_is_concat_lab == '10') {
-      const list = lab?.data?.list.map((item) => ({ id: item.id, name: item.labName })) || []
-      state.laboratoryColumns = list
-    } else {
-      state.laboratoryColumns = lab?.data.list
-    }
+  import to from 'await-to-js'
+  import { onMounted, reactive } from 'vue'
+  import { useRouter } from 'vue-router'
+  import { useInstrApi } from '/@/api/instr'
+  import { useBlackApi } from '/@/api/blacklist'
+  import { usePositionApi } from '/@/api/instr/position'
+  import { showNotify } from 'vant'
+  import { getImageUrl } from '/@/utils/url'
+  const props = defineProps({
+    following: {
+      type: String,
+      default: '20',
+    },
   })
-}
-const openSelectLaboratory = () => {
-  state.showLab = true
-}
-const pickLab = ({ selectedOptions }: { selectedOptions: any[] }) => {
-  state.showLab = false
-  state.queryForm.laboratoryId = selectedOptions[selectedOptions.length - 1].id
-  state.queryForm.laboratoryName = selectedOptions[selectedOptions.length - 1].name
-}
-// 打开设备预约
-const openAppoint = async (v) => {
-  // 验证是否拉入了黑名单
-  const [err, res]: ToResponse = await to(blacklistApi.checkInBlacklist())
-  if (err) return
-  if (res.data) {
-    return showNotify({ type: 'danger', message: '您已被拉入黑名单,无法预约,请联系管理员' })
-  }
-  router.push(`/inst/appoint?id=${v.id}&name=${v.instName}`)
-}
-// 设备详情
-const openDetail = (v) => {
-  router.push({
-    path: '/instr-detail',
-    query: {
-      id: v.id,
+  const router = useRouter()
+  const instApi = useInstrApi()
+  const blacklistApi = useBlackApi()
+  const positionApi = usePositionApi()
+
+  const state = reactive({
+    showLab: false,
+    laboratoryColumns: [],
+    instStatus: {
+      10: '正常',
+      20: '故障',
+      30: '报废',
     },
+    instList: [],
+    current: 1,
+    queryForm: {
+      laboratoryId: 0,
+      laboratoryName: '',
+      pageNum: 1,
+      pageSize: 10,
+      instName: '',
+    },
+    popupShow: false,
+    instDetail: {} as any,
+    total: 0,
+    detailsLoading: false, //详情加载
+    loading: false,
+    finished: false,
   })
-  // state.popupShow = true
-  // getDetail(v.id)
-}
-const onLoad = () => {
-  state.queryForm.pageNum++
-  getInstList()
-}
-// 查询列表
-const getInstList = async () => {
-  state.loading = true
-  const params = JSON.parse(JSON.stringify(state.queryForm))
-  params.following = props.following
-  params.instStatus = "10"
-  const [err, res]: ToResponse = await to(instApi.getList(params))
-  state.loading = false
-  if (err) return
-  if (res?.code === 200) {
-    state.instList =
-      state.queryForm.pageNum == 1 ? [...(res?.data?.list || [])] : [...state.instList, ...(res?.data?.list || [])]
-    state.total = res?.data?.total
-    if (state.queryForm.pageNum * state.queryForm.pageSize >= res.data.total) {
-      state.finished = true
+  const setLaboratoryName = (name) => {
+    return name ? `(${name})` : ''
+  }
+  const getPosition = () => {
+    Promise.all([positionApi.getLaboratoryList({ noPage: true })]).then(([lab]) => {
+      const instr_is_concat_lab = JSON.parse(localStorage.getItem('instr_is_concat_lab') || '{}')
+      if (instr_is_concat_lab == '10') {
+        const list = lab?.data?.list.map((item) => ({ id: item.id, name: item.labName })) || []
+        state.laboratoryColumns = list
+      } else {
+        state.laboratoryColumns = lab?.data.list
+      }
+    })
+  }
+  const openSelectLaboratory = () => {
+    state.showLab = true
+  }
+  const pickLab = ({ selectedOptions }: { selectedOptions: any[] }) => {
+    state.showLab = false
+    state.queryForm.laboratoryId = selectedOptions[selectedOptions.length - 1].id
+    state.queryForm.laboratoryName = selectedOptions[selectedOptions.length - 1].name
+  }
+  // 打开设备预约
+  const openAppoint = async (v) => {
+    // 验证是否拉入了黑名单
+    const [err, res]: ToResponse = await to(blacklistApi.checkInBlacklist())
+    if (err) return
+    if (res.data) {
+      return showNotify({ type: 'danger', message: '您已被拉入黑名单,无法预约,请联系管理员' })
     }
+    router.push(`/inst/appoint?id=${v.id}&name=${v.instName}`)
   }
-}
-const onSearch = () => {
-  state.queryForm.pageNum = 1
-  getInstList()
-}
-// 获取仪器详情
-const getDetail = async (id) => {
-  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
+  // 设备详情
+  const openDetail = (v) => {
+    router.push({
+      path: '/instr-detail',
+      query: {
+        id: v.id,
+      },
+    })
+    // state.popupShow = true
+    // getDetail(v.id)
   }
-}
-// 关注/取关
-const handleFollowInst = async (row) => {
-  const [err] = row.following
-    ? await to(instApi.unfollow({ ids: [row.id] }))
-    : await to(instApi.follow({ ids: [row.id] }))
-  if (err) return
-  showNotify({ type: 'success', message: !row.following ? '收藏成功' : '已取消收藏' })
-  state.instList.forEach((item, index) => {
-    if (item.id == row.id) {
-      // 关注之前的关注状态
-      if (item.following) {
-        state.queryForm.pageNum = 1
-        getInstList()
-      } else {
-        item.following = true
-        const obj = item
-        state.instList.splice(index, 1)
-        state.instList.unshift(obj)
+  const onLoad = () => {
+    state.queryForm.pageNum++
+    getInstList()
+  }
+  // 查询列表
+  const getInstList = async () => {
+    state.loading = true
+    const params = JSON.parse(JSON.stringify(state.queryForm))
+    params.following = props.following
+    params.instStatus = '10'
+    const [err, res]: ToResponse = await to(instApi.getList(params))
+    state.loading = false
+    if (err) return
+    if (res?.code === 200) {
+      state.instList =
+        state.queryForm.pageNum == 1 ? [...(res?.data?.list || [])] : [...state.instList, ...(res?.data?.list || [])]
+      state.total = res?.data?.total
+      if (state.queryForm.pageNum * state.queryForm.pageSize >= res.data.total) {
+        state.finished = true
       }
-      return
     }
+  }
+  const onSearch = () => {
+    state.queryForm.pageNum = 1
+    getInstList()
+  }
+  // 获取仪器详情
+  const getDetail = async (id) => {
+    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
+    }
+  }
+  // 关注/取关
+  const handleFollowInst = async (row) => {
+    const [err] = row.following
+      ? await to(instApi.unfollow({ ids: [row.id] }))
+      : await to(instApi.follow({ ids: [row.id] }))
+    if (err) return
+    showNotify({ type: 'success', message: !row.following ? '收藏成功' : '已取消收藏' })
+    state.instList.forEach((item, index) => {
+      if (item.id == row.id) {
+        // 关注之前的关注状态
+        if (item.following) {
+          state.queryForm.pageNum = 1
+          getInstList()
+        } else {
+          item.following = true
+          const obj = item
+          state.instList.splice(index, 1)
+          state.instList.unshift(obj)
+        }
+        return
+      }
+    })
+  }
+  // const onEnroll = (row: any) => {
+  //   router.push({
+  //     path: '/training/enroll',
+  //     query: {
+  //       id: row.id
+  //     }
+  //   })
+  // }
+  onMounted(() => {
+    getPosition()
+    state.queryForm.pageNum = 1
+    getInstList()
   })
-}
-// const onEnroll = (row: any) => {
-//   router.push({
-//     path: '/training/enroll',
-//     query: {
-//       id: row.id
-//     }
-//   })
-// }
-onMounted(() => {
-  getPosition()
-  state.queryForm.pageNum = 1
-  getInstList()
-})
 </script>
 
 <style lang="scss" scoped>
-* {
-  box-sizing: border-box;
-}
-
-.home {
-  height: 100%;
-  overflow: hidden;
-  display: flex;
-  flex-direction: column;
-
-  .search-wrap {
-    height: 40px;
-    display: flex;
-    align-items: center;
-    justify-content: space-around;
-    padding: 0 15px;
-    margin: 10px 0 0;
+  * {
+    box-sizing: border-box;
   }
 
-  .inst-list {
-    flex: 1;
-    height: 0;
-    background-color: #f7f8fa;
+  .home {
+    height: 100%;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
 
-    // padding-bottom: 20px;
-    // padding: 0 15px;
-    // height: calc(100% - 60px);
-    .inst-wrap {
-      height: 100%;
-      overflow: auto;
-      padding: 10px 10px 0 10px;
-      // padding: 10px 10px 0 10px;
+    .search-wrap {
+      height: 40px;
+      display: flex;
+      align-items: center;
+      justify-content: space-around;
+      padding: 0 15px;
+      margin: 10px 0 0;
     }
 
-    .inst-item {
-      background-color: #fff;
-      padding: 8px;
-      box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
-      margin-bottom: 14px;
-      border-radius: 6px;
-
-      .follow-icon {
-        width: 24px;
-        height: 24px;
+    .inst-list {
+      flex: 1;
+      height: 0;
+      background-color: #f7f8fa;
+
+      // padding-bottom: 20px;
+      // padding: 0 15px;
+      // height: calc(100% - 60px);
+      .inst-wrap {
+        height: 100%;
+        overflow: auto;
+        padding: 10px 10px 0 10px;
+        // padding: 10px 10px 0 10px;
       }
 
-      .inst-info {
-        display: flex;
-      }
+      .inst-item {
+        background-color: #fff;
+        padding: 8px;
+        box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
+        margin-bottom: 14px;
+        border-radius: 6px;
 
-      .i-right {
-        flex: 1;
-        font-size: 14px;
+        .follow-icon {
+          width: 24px;
+          height: 24px;
+        }
 
-        .i-r-icon {
-          width: 15px;
-          height: 15px;
-          margin-right: 10px;
+        .inst-info {
+          display: flex;
+          width: 100%;
         }
-      }
-    }
-  }
 
-  .detail-box {
-    height: 80vh;
-    background: #fff;
-    border-radius: 10px;
-    padding: 15px;
+        .i-right {
+          flex: 1;
+          font-size: 14px;
+          min-width: 0; // 允许内部内容收缩,配合省略号
+          overflow: hidden;
 
-    .dc-content {
-      width: 100%;
-      height: calc(100% - 30px);
-      overflow: auto;
+          .flex {
+            min-width: 0; // 避免子元素撑破容器
+            width: 100%;
+          }
 
-      .label-item {
-        width: 90px;
-        font-size: 14px;
-        padding: 6px;
-      }
+          footer {
+            width: 100%;
+            min-width: 0;
+            display: flex;
+            flex-direction: column;
+            gap: 4px;
+          }
 
-      .detailTxt {
-        flex: 1;
+          .i-r-icon {
+            width: 15px;
+            height: 15px;
+            margin-right: 10px;
+          }
+        }
       }
     }
-  }
 
-  .filter-popup {
-    background: rgba(0, 0, 0, 0.8);
-    position: fixed;
-    width: 100%;
-    height: 100%;
-    left: 0;
-    z-index: 1;
-    top: 50px;
-
-    .filter-wrap {
-      width: 100%;
-      padding: 10px;
-      background: #ffffff;
+    .detail-box {
+      height: 80vh;
+      background: #fff;
+      border-radius: 10px;
+      padding: 15px;
 
-      .filter-item {
-        padding-bottom: 14px;
+      .dc-content {
+        width: 100%;
+        height: calc(100% - 30px);
+        overflow: auto;
 
-        .tit {
+        .label-item {
+          width: 90px;
           font-size: 14px;
-          color: #323232;
-          font-weight: bold;
-          padding-bottom: 10px;
+          padding: 6px;
         }
 
-        .menu-list {
-          display: flex;
-          flex-wrap: wrap;
-
-          .menu-item {
-            margin-right: 20px;
-            margin-bottom: 10px;
-          }
+        .detailTxt {
+          flex: 1;
         }
       }
     }
 
-    .btn-box {
+    .filter-popup {
+      background: rgba(0, 0, 0, 0.8);
+      position: fixed;
       width: 100%;
-      height: 40px;
-      padding: 10px;
-      background: #ffffff;
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
+      height: 100%;
+      left: 0;
+      z-index: 1;
+      top: 50px;
+
+      .filter-wrap {
+        width: 100%;
+        padding: 10px;
+        background: #ffffff;
+
+        .filter-item {
+          padding-bottom: 14px;
+
+          .tit {
+            font-size: 14px;
+            color: #323232;
+            font-weight: bold;
+            padding-bottom: 10px;
+          }
 
-      >div {
-        width: 160px;
-        height: 32px;
-        border-radius: 20px;
-        border: solid 1px #ec652b;
-        align-items: center;
-        justify-content: center;
-        text-align: center;
-        line-height: 32px;
-      }
+          .menu-list {
+            display: flex;
+            flex-wrap: wrap;
 
-      .reset {
-        color: #ec652b;
+            .menu-item {
+              margin-right: 20px;
+              margin-bottom: 10px;
+            }
+          }
+        }
       }
 
-      .submit {
-        color: #fff;
-        background-color: #ec652b;
+      .btn-box {
+        width: 100%;
+        height: 40px;
+        padding: 10px;
+        background: #ffffff;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+
+        > div {
+          width: 160px;
+          height: 32px;
+          border-radius: 20px;
+          border: solid 1px #ec652b;
+          align-items: center;
+          justify-content: center;
+          text-align: center;
+          line-height: 32px;
+        }
+
+        .reset {
+          color: #ec652b;
+        }
+
+        .submit {
+          color: #fff;
+          background-color: #ec652b;
+        }
       }
     }
   }
-}
 
-.instNameTxt {
-  margin-bottom: 10px;
-  font-size: 16px;
-  font-weight: bold;
-  text-align: center;
-}
+  .instNameTxt {
+    margin-bottom: 10px;
+    font-size: 16px;
+    font-weight: bold;
+    text-align: center;
+  }
+
+  .detailTxt {
+    font-size: 12px;
+    color: #333333;
+    display: block;
+    max-width: 100%;
+    flex: 1 1 auto;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+
+    &.name {
+      font-weight: bold;
+      font-size: 16px;
+    }
+  }
 
-.detailTxt {
-  font-size: 12px;
-  color: #333333;
+  .status-tag {
+    flex: 0 0 auto;
+    margin-left: 6px;
+  }
 
-  &.name {
+  .labelTit {
+    margin-bottom: 10px;
+    text-align: center;
+    font-size: 15px;
     font-weight: bold;
-    font-size: 16px;
   }
-}
-
-.labelTit {
-  margin-bottom: 10px;
-  text-align: center;
-  font-size: 15px;
-  font-weight: bold;
-}
-
-.primary-color {
-  color: #3c9cff;
-}
-
-.warning-color {
-  color: #ff976a;
-}
-
-.danger-color {
-  color: #ee0a24;
-}
-
-.instr-status-btn {
-  font-weight: bold;
-  font-size: 14px;
-}
-
-.mt-auto {
-  margin-top: auto;
-}
+
+  .primary-color {
+    color: #3c9cff;
+  }
+
+  .warning-color {
+    color: #ff976a;
+  }
+
+  .danger-color {
+    color: #ee0a24;
+  }
+
+  .instr-status-btn {
+    font-weight: bold;
+    font-size: 14px;
+  }
+
+  .mt-auto {
+    margin-top: auto;
+  }
 </style>

+ 89 - 24
src/view/user/index.vue

@@ -62,7 +62,21 @@
         class="card"
         @click="showOperatingGuidelines"
       >
-        <h4>点击查看操作指引</h4>
+        <h4>平台入室操作指引</h4>
+      </div>
+      <div class="card">
+        <van-cell
+          v-auth="'instr_open'"
+          title="打开设备"
+          is-link
+          @click="openInst"
+        />
+        <van-cell
+          v-auth="'instr_close'"
+          title="关闭设备"
+          is-link
+          @click="closeInst"
+        />
       </div>
     </div>
     <footer>
@@ -120,10 +134,10 @@
             <div class="step-content">
               <h4 class="step-title">{{ item.title }}</h4>
               <p class="step-desc">{{ item.desc }}</p>
-              <!-- <van-button 
-                v-if="item.btn" 
-                size="small" 
-                type="primary" 
+              <!-- <van-button
+                v-if="item.btn"
+                size="small"
+                type="primary"
                 class="step-btn"
                 @click="handleStepAction(item)"
               >
@@ -146,18 +160,24 @@
   import { ref, onMounted } from 'vue'
   import { storeToRefs } from 'pinia'
   import to from 'await-to-js'
-  import { showConfirmDialog } from 'vant'
+  import { showConfirmDialog, showToast } from 'vant'
   import { useRouter } from 'vue-router'
 
   import { useUserInfo } from '/@/stores/userInfo'
-  import { Local } from '/@/utils/storage'
+  import { Local, Session } from '/@/utils/storage'
   import { useBillApi } from '/@/api/instr/finance/bill'
+  import { useLoginApi } from '/@/api/login'
+
+  import { scanCodeWxUrl, InstSwitchType } from '/@/constants/pageConstants'
+
+  import wx from 'weixin-js-sdk'
 
   const billApi = useBillApi()
+  const loginApi = useLoginApi()
 
   const router = useRouter()
   const storesUseUserInfo = useUserInfo()
-  const { userInfos, sdkConfig, openId } = storeToRefs(storesUseUserInfo)
+  const { userInfos, openId } = storeToRefs(storesUseUserInfo)
 
   const showPopup = ref<boolean>(false)
   const billInfo = ref<{
@@ -173,26 +193,26 @@
   const handleStepList = ref([
     {
       title: '第一步:入室培训和考试',
-      desc: '请先完成入室必要培训,掌握安全操作知识,并通过考试',
+      desc: '请先完成入室培训,掌握必要的安全操作知识和实验室规章制度,并通过考试',
       btn: '培训报名',
       step: 1,
     },
     {
       title: '第二步:提交入室申请',
-      desc: '在线完成入室前的培训,掌握必要的安全操作知识,并通过考试',
+      desc: '完成入室培训考核合格后,在入室管理里面提交入室申请,等待平台管理员审核',
       btn: '前往提交',
       step: 2,
     },
+    // {
+    //   title: '第三步:上传入室资料',
+    //   desc: '整理相关凭证并将入室资料上传到系统中',
+    //   step: 3,
+    // },
     {
-      title: '第三步:上传入室资料',
-      desc: '整理相关凭证并将入室资料上传到系统中',
+      title: '第三步:等待审核和房间分配',
+      desc: '由平台管理老师进行审核并分配细胞房或分配分子储物柜,收到审核通过的通知后,即可在申请的时段进入细胞房或分子室进行实验',
       step: 3,
     },
-    {
-      title: '第四步:等待审核和房间分配',
-      desc: '由平台老师进行审核,通过后分配操作室',
-      step: 4,
-    },
   ])
 
   const onRouterPush = (val: string) => {
@@ -201,15 +221,23 @@
   const signOut = () => {
     showConfirmDialog({
       message: '确认切换账号?',
-    }).then(() => {
-      // 清除 token
-      Local.remove('token')
-      // 清除微信 openId 和 unionId
-      localStorage.removeItem('openId')
-      localStorage.removeItem('unionId')
+    }).then(async () => {
+      // 如果有 openId,先解绑微信
+      if (openId.value) {
+        await to(loginApi.WeChatUnBindOpenId({ openId: openId.value }))
+      }
+      
+      // 调用退出登录接口
+      const [err]: ToResponse = await to(loginApi.signOut())
+      if (err) return
+      
+      // 清理所有本地缓存
+      Local.clear()
+      Session.clear()
       // 重置用户信息
       storesUseUserInfo.resetUserInfo()
-      window.location.reload()
+      // 关闭窗口
+      wx.closeWindow()
     })
   }
   const handleSubscribe = (errMsg: any, subscribeDetails: any) => {
@@ -224,6 +252,21 @@
     // Implementation of handleStepAction
   }
 
+  const checkPermi = (perms: string[]) => {
+    const list = userInfos.value?.authBtnList || []
+    return perms.some((p) => list.includes(p))
+  }
+
+  const openInst = () => {
+    const url = scanCodeWxUrl('START_RUN', InstSwitchType.OPEN)
+    window.location.href = url
+  }
+
+  const closeInst = () => {
+    const url = scanCodeWxUrl('END_RUN', InstSwitchType.CLOSE)
+    window.location.href = url
+  }
+
   // 获取个人账单信息
   const getBillInfo = async () => {
     const [err, res]: ToResponse = await to(billApi.getMyOrderTotal())
@@ -240,6 +283,7 @@
   .app-container {
     display: flex;
     flex-direction: column;
+
     header {
       background-color: #1c9bfd;
       color: #fff;
@@ -247,31 +291,37 @@
       border-radius: 8px;
       margin-top: 10px;
       display: flex;
+
       .content {
         flex: 1;
         display: flex;
         flex-direction: column;
         justify-content: space-around;
         overflow: hidden;
+
         .bold {
           display: flex;
           align-items: center;
           font-weight: bold;
           // flex-wrap: nowrap;
           white-space: nowrap;
+
           .van-text-ellipsis {
             flex: 1;
           }
         }
       }
     }
+
     .main {
       flex: 1;
       overflow-y: auto;
+
       > ul {
         display: flex;
         height: 60px;
         margin-top: 10px;
+
         li {
           flex: 1;
           background: #fff;
@@ -280,34 +330,42 @@
           align-items: center;
           justify-content: center;
           border-radius: 4px;
+
           & + li {
             margin-left: 10px;
           }
         }
+
         &.links {
           li {
             color: #fff;
             background: #5eb7fc;
+
             &:nth-child(2) {
               background: #fd9c5e;
             }
+
             &:nth-child(3) {
               background: #53e3a7;
             }
+
             &:nth-child(4) {
               background: #888ac3;
             }
           }
         }
       }
+
       .card {
         margin-top: 10px;
         border-radius: 4px;
         background-color: #fff;
         padding: 10px;
+
         h4 {
           height: 18px;
           line-height: 18px;
+
           &::before {
             display: inline-block;
             content: '';
@@ -318,23 +376,28 @@
             vertical-align: middle;
           }
         }
+
         .nav {
           display: flex;
           margin: 10px 0;
           flex-wrap: wrap;
           justify-content: space-between;
+
           li {
             flex: 0 0 25%;
             display: flex;
             flex-direction: column;
             align-items: center;
             justify-content: center;
+
             span {
               font-size: 20px;
             }
+
             p {
               margin-top: 4px;
             }
+
             &:nth-child(n + 5) {
               margin-top: 10px;
             }
@@ -342,11 +405,13 @@
         }
       }
     }
+
     footer {
       flex: 0 0 44px;
       margin-bottom: 20px;
     }
   }
+
   :deep(.van-popup--bottom) {
     padding: 20px !important;
   }