Procházet zdrojové kódy

遵义移动端问题修复

xukai před 5 měsíci
rodič
revize
37a47cde5d

+ 47 - 0
src/api/instr/inst/grant.ts

@@ -0,0 +1,47 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-07-14 17:15:49
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-08-17 16:29:09
+ * @Description: file content
+ * @FilePath: \frontend\packages\vue-next-admin-sub\src\api\inst\index.ts
+ */
+import request from '/@/utils/micro_request.js'
+const basePath = import.meta.env.VITE_INSTR_ADMIN
+// 仪器授权
+export function useGrantApi() {
+  return {
+    // 添加授权
+    add(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrant', 'Create', query)
+    },
+    // 删除授权
+    del(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrant', 'DeleteByIds', query)
+    },
+    // 授权详情
+    detail(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrant', 'GetEntityById', query)
+    },
+    // 查询授权
+    getList(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrant', 'GetList', query)
+    },
+    // 已过期授权列表
+    expiredList(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrant', 'GetListExpired', query)
+    },
+    // 更新授权
+    update(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrant', 'UpdateById', query)
+    },
+    // 证书资质
+    getAllListByUserId(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrant', 'GetAllListByUserId', query)
+    },
+    // 授权设备
+    getAuthDeviceList(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrant', 'GetAuthDeviceList', query)
+    },
+  }
+}

+ 39 - 0
src/api/instr/inst/grantApply.ts

@@ -0,0 +1,39 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-07-14 17:15:49
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-08-17 16:29:09
+ * @Description: file content
+ * @FilePath: \frontend\packages\vue-next-admin-sub\src\api\inst\index.ts
+ */
+import request from '/@/utils/micro_request.js'
+const basePath = import.meta.env.VITE_INSTR_ADMIN
+// 仪器授权
+export function useGrantApplyApi() {
+  return {
+    // 添加授权
+    add(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrantRequest', 'Create', query)
+    },
+    // 删除授权
+    del(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrantRequest', 'DeleteByIds', query)
+    },
+    // 授权详情
+    detail(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrantRequest', 'GetEntityById', query)
+    },
+    // 查询授权
+    getList(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrantRequest', 'GetList', query)
+    },
+    // 更新授权 /TusInstrumentGrantRequest.
+    update(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrantRequest', 'Grant', query)
+    },
+    // 我的授权申请列表
+    myList(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrantRequest', 'MyApply', query)
+    },
+  }
+}

+ 39 - 0
src/api/instr/inst/training.ts

@@ -0,0 +1,39 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-07-14 17:15:49
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-08-17 16:29:09
+ * @Description: file content
+ * @FilePath: \frontend\packages\vue-next-admin-sub\src\api\inst\index.ts
+ */
+import request from '/@/utils/micro_request.js'
+const basePath = import.meta.env.VITE_INSTR_ADMIN
+// 培训申请
+export function useTrainingApi() {
+  return {
+    // 创建申请
+    add(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrantTrainingRequest', 'Create', query)
+    },
+    // 删除申请
+    del(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrantTrainingRequest', 'DeleteByIds', query)
+    },
+    // 申请详情
+    detail(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrantTrainingRequest', 'GetEntityById', query)
+    },
+    // 查询申请
+    getList(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrantTrainingRequest', 'GetList', query)
+    },
+    // 我的申请
+    myApplyList(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrantTrainingRequest', 'MyApply', query)
+    },
+    // 安排培训
+    arrange(query?: object) {
+      return request.postRequest(basePath, 'TusInstrumentGrantTrainingRequest', 'Arrange', query)
+    },
+  }
+}

+ 59 - 0
src/api/instr/system/index.ts

@@ -0,0 +1,59 @@
+/*
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2023-07-19 13:42:40
+ * @LastEditors: wanglj
+ * @LastEditTime: 2023-07-21 10:16:53
+ * @Description: file content
+ * @FilePath: \labsop_meno\frontend\packages\vue-next-admin\src\api\system\dict.ts
+ */
+import request from '/@/utils/micro_request.js'
+const basePath = import.meta.env.VITE_ADMIN
+// 参数设置
+export function useSystemApi() {
+  return {
+    // 根据字典类型获取字典项明细
+    getDictDataByType(str: string) {
+      return request.postRequest(basePath, 'Dict', 'GetDictDataByType', { dictType: str })
+    },
+    // 获取用户列表
+    getUserList(query?: object) {
+      return request.postRequest(basePath, 'User', 'GetList', query)
+    },
+    // 获取用户字典
+    getUserDict(query?: object) {
+      return request.postRequest(basePath, 'User', 'GetDictList', query)
+    },
+    // 获取仪器责任人角色用户列表
+    getInstUserList(query?: object) {
+      return request.postRequest(basePath, 'User', 'GetUserByRoleCode', query)
+    },
+    // 部门树
+    getDeptTree(query?: object) {
+      return request.postRequest(basePath, 'Dept', 'GetDeptTree', query)
+    },
+    // 部门列表
+    getDeptList(query?: object) {
+      return request.postRequest(basePath, 'Dept', 'GetList', query)
+    },
+    // 根据部门查人员列表
+    getUserListByDept(query?: object) {
+      return request.postRequest(basePath, 'User', 'GetDeptAndUserByDeptPid', query)
+    },
+    // 根据名称查人员列表
+    getUserByUserName(query?: object) {
+      return request.postRequest(basePath, 'User', 'GetUserInfoByName', query)
+    },
+    // 查询用户组
+    getUserGroupList(query?: object) {
+      return request.postRequest(basePath, 'UserGroup', 'GetGroupList', query)
+    },
+    //获取用户组列表
+    getGroupListByInstr(query?: object) {
+      return request.postRequest(basePath, 'UserGroup', 'GetGroupListWithInst', query)
+    },
+    //获取角色列表
+    getRoleList(query?: object) {
+      return request.postRequest(basePath, 'Role', 'GetList', query)
+    },
+  }
+}

+ 1 - 1
src/view/entry/components/edit.vue

@@ -245,7 +245,7 @@
   const openDialog = async (type: string, row: any) => {
     await getDicts()
     state.dialog.type = type
-    state.dialog.title = '入室预约'
+    state.dialog.title = '使用预约'
     if (type == 'add') {
       state.form = {
         id: 0,

+ 1 - 1
src/view/entry/mine.vue

@@ -140,7 +140,7 @@
                     style="height: 30px"
                     @click="handleAppointment(item)"
                   >
-                    入室预约
+                    使用预约
                   </van-button>
                 </div>
                 <div

+ 1 - 1
src/view/home/index.vue

@@ -57,7 +57,7 @@
         </li> -->
         <li @click="onRouterPush('/inst/repairReport/home')">
           <img src="../../assets/img/更多应用.png" alt="" />
-          <p>故障报修</p>
+          <p>仪器报修</p>
         </li>
         <li @click="onRouterPush('/lab/inspection/home')">
           <img src="../../assets/img/更多应用.png" alt="" />

+ 451 - 0
src/view/instr/addAuthorization/index.vue

@@ -0,0 +1,451 @@
+<template>
+  <div class="add-inst-wrap">
+    <el-dialog
+      v-model="state.isShowDialog"
+      :title="title"
+      width="90%"
+      :close-on-click-modal="false"
+    >
+      <el-form
+        ref="editFormRef"
+        :model="state.form"
+        label-width="100px"
+        size="default"
+        label-position="top"
+        :rules="rules"
+        closeable
+      >
+        <el-row>
+          <el-col
+            :span="24"
+            class="mb24"
+          >
+            <el-form-item
+              label="姓名"
+              prop="userName"
+            >
+              <el-input
+                :disabled="dialogType != 'add'"
+                v-model="state.form.userName"
+                @focus="openSelectUser"
+                placeholder="请选择"
+              ></el-input>
+              <!-- <el-select
+								:disabled="dialogType != 'add'"
+								filterable
+								style="width: 100%"
+								v-model.number="state.form.curUser"
+								@change="selectUser"
+								value-key="userId"
+								class="w100"
+								placeholder="请选择"
+								clearable
+							>
+								<el-option :label="v.userName" :value="v" :key="v.userId" v-for="v in userOptions" />
+							</el-select> -->
+            </el-form-item>
+          </el-col>
+          <el-col
+            :span="24"
+            class="mb10"
+          >
+            <el-form-item
+              label="预约资格有效开始时间"
+              prop="startTime"
+            >
+              <el-date-picker
+                style="width: 100%"
+                v-model="state.form.startTime"
+                type="date"
+                placeholder="选择预约资格有效开始时间"
+                value-format="YYYY-MM-DD"
+                clearable
+                :disabledDate="disabledDateFn"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col
+            :span="24"
+            class="mb10"
+          >
+            <el-form-item
+              label="预约资格有效结束时间"
+              prop="endTime"
+            >
+              <el-date-picker
+                style="width: 100%"
+                v-model="state.form.endTime"
+                type="date"
+                placeholder="选择预约资格有效结束时间"
+                value-format="YYYY-MM-DD"
+                clearable
+                :disabledDate="disabledDateFn"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item
+              label="上传资格证书"
+              prop="fileList"
+            >
+              <el-upload
+                :class="{ disUoloadSty: state.form.fileList.length > 0 }"
+                v-model:file-list="state.form.fileList"
+                :action="uploadUrl"
+                :limit="1"
+                style="width: 100%"
+                accept=".jpg,.png"
+                list-type="picture-card"
+                :on-preview="handlePictureCardPreview"
+                :before-upload="beforeAvatarFileUpload"
+                :on-remove="(res, uploadFile: UploadFile) => handleRemove()"
+                :on-success="(res, uploadFile: UploadFile) => handleSuccess(res, uploadFile)"
+              >
+                <el-icon><Plus /></el-icon>
+              </el-upload>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button
+            @click="onCancel"
+            type="info"
+            size="default"
+          >
+            取 消
+          </el-button>
+          <el-button
+            type="primary"
+            @click="subAdd"
+            size="default"
+          >
+            提 交
+          </el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <el-dialog v-model="dialogVisible">
+      <img
+        w-full
+        :src="dialogImageUrl"
+        alt="Preview Image"
+      />
+    </el-dialog>
+    <SelectUser
+      ref="selectUserRef"
+      :multiple="false"
+      @selectedUserData="selectedUserData"
+    />
+  </div>
+</template>
+
+<script setup lang="ts" name="addInstrument">
+  import { reactive, ref, nextTick, defineAsyncComponent } from 'vue'
+  import { ElMessage } from 'element-plus'
+  import to from 'await-to-js'
+  import type { UploadFile } from 'element-plus'
+  import { useSystemApi } from '/@/api/instr/system'
+  const systemApi = useSystemApi()
+  import { useGrantApi } from '/@/api/instr/inst/grant'
+  import { useGrantApplyApi } from '/@/api/instr/inst/grantApply'
+  const grantApi = useGrantApi()
+  const grantApplyApi = useGrantApplyApi()
+  import { deepClone } from '/@/utils/other'
+  import { storeToRefs } from 'pinia'
+  import mittBus from '/@/utils/mitt'
+  import { useUserInfo } from '/@/stores/userInfo'
+  const SelectUser = defineAsyncComponent(() => import('/@/views/instr/component/selectUserByDept/index.vue'))
+  // 向父组件传值/事件
+  const emit = defineEmits(['refresh'])
+  // 定义变量内容
+  const selectUserRef = ref()
+  const stores = useUserInfo()
+  const { userInfos } = storeToRefs(stores)
+  const uploadUrl = import.meta.env.VITE_UPLOAD
+  const editFormRef = ref()
+  const title = ref('')
+  const dialogVisible = ref(false) //查看大图
+  const dialogImageUrl = ref('') //查看大图
+  const userOptions = ref<RowUserType[]>([]) //用户列表
+  const dialogType = ref('') //编辑、新增
+  const disabledDateFn = (time: any) => {
+    // 获取当前日期的0点时间戳
+    const today = new Date()
+    today.setHours(0, 0, 0, 0)
+    // 如果传递进来的日期时间戳小于今天的0点时间戳,则禁止选择
+    return time.getTime() < today.getTime()
+  }
+  const state = reactive({
+    isShowDialog: false,
+    form: {
+      id: 0,
+      userId: null,
+      userName: '',
+      instCode: '', //	仪器编码
+      instId: 0, //仪器ID
+      instName: '', //仪器名称
+      date: null,
+      startTime: '',
+      endTime: '',
+      curtName: '',
+      curtFile: '',
+      fileList: [],
+      curUser: {},
+    },
+  })
+
+  const rules = reactive({
+    userId: [{ required: true, message: '人员不能为空', trigger: 'blur' }],
+    startTime: [{ required: true, message: '预约资格有效开始时间不能为空', trigger: 'blur' }],
+    endTime: [{ required: true, message: '预约资格有效结束时间不能为空', trigger: 'blur' }],
+    // fileList: [{ required: true, message: '资格证书不能为空', trigger: 'blur' }],
+  })
+
+  const selectUser = (data) => {
+    console.log(data)
+    state.form.userId = data.userId
+    state.form.userName = data.userName
+  }
+
+  // 打开弹窗
+  const openDialog = (type: 'add' | 'edit', row) => {
+    console.log(row)
+    state.isShowDialog = true
+    getDicts()
+    nextTick(() => {
+      editFormRef.value.clearValidate()
+      editFormRef.value.resetFields()
+      state.form.fileList = []
+      state.form.date = null
+      state.form.curUser = []
+      dialogType.value = type
+      if (type == 'edit') {
+        getInstDetail(row.id)
+        title.value = '编辑人员授权'
+      } else if (type == 'add') {
+        title.value = '新增人员授权'
+        state.form.instCode = row.instCode
+        state.form.instId = row.id
+        state.form.instName = row.instName
+      } else if (type == 'personal') {
+        title.value = '申请预约资格授权'
+        state.form.instCode = row.instCode
+        state.form.instId = row.id
+        state.form.instName = row.instName
+        state.form.userId = userInfos.value.id
+        state.form.userName = userInfos.value.nickName
+        state.form.curUser = { userId: userInfos.value.id, userName: userInfos.value.nickName }
+      }
+    })
+  }
+
+  // 获取仪器详情
+  const getInstDetail = async (id) => {
+    const [err, res]: ToResponse = await to(grantApi.detail({ id }))
+    if (err) return
+    if (res?.code == 200) {
+      console.log(res.data)
+      state.form = res.data
+      state.form.fileList = [{ name: res.data.curtName, url: res.data.curtFile }]
+      state.form.curUser = { userId: res.data.userId, userName: res.data.userName }
+      state.form.date = res.data.startTime ? [res.data.startTime, res.data.endTime] : null
+    }
+  }
+
+  const getDicts = () => {
+    Promise.all([systemApi.getUserList({ noPage: true })]).then(([user]) => {
+      userOptions.value = user.data.list.map((item: any) => ({ userId: item.id, userName: item.nickName }))
+    })
+  }
+  // 关闭弹窗
+  const closeDialog = () => {
+    // 清空表单,此项需加表单验证才能使用
+    editFormRef.value.resetFields()
+    state.form.fileList = []
+    state.form.curtName = ''
+    state.form.curtFile = ''
+    state.form.userId = null
+    state.isShowDialog = false
+  }
+  const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
+    dialogImageUrl.value = uploadFile.url!
+    dialogVisible.value = true
+  }
+  const handleSuccess = (res: any, uploadFile: UploadFile) => {
+    state.form.curtName = uploadFile.name
+    state.form.curtFile = res?.Data || ''
+  }
+  const handleRemove = () => {
+    state.form.fileList = []
+    state.form.curtName = ''
+    state.form.curtFile = ''
+  }
+  const beforeAvatarFileUpload = (file) => {
+    let isLt10m = file.size / 1024 / 1024 / 20 < 1
+    if (!isLt10m) {
+      ElMessage.error('上传文件大小不能超过 20MB!')
+      return false
+    }
+    return true
+  }
+  const openSelectUser = () => {
+    const param = state.form.userId
+      ? { checkedUser: [{ id: state.form.userId, name: state.form.userName }] }
+      : undefined
+    selectUserRef.value.openDialog(param)
+  }
+  const selectedUserData = (data) => {
+    state.form.userId = data.user[0].id
+    state.form.userName = data.user[0].name
+  }
+  // 取消
+  const onCancel = () => {
+    closeDialog()
+  }
+
+  // 创建
+  const subAdd = () => {
+    editFormRef.value.validate(async (valid: any, error: any) => {
+      if (valid) {
+        let params = deepClone(state.form)
+        let post = grantApi.add
+        if (dialogType.value == 'edit') {
+          post = grantApi.update
+        } else if (dialogType.value == 'add') {
+          post = grantApi.add
+        } else if (dialogType.value == 'personal') {
+          post = grantApplyApi.add
+        }
+        const [err, res]: ToResponse = await to(post({ ...params }))
+        if (err) return
+        if (res?.code == 200) {
+          ElMessage.success('提交成功')
+          closeDialog()
+          mittBus.emit('refresAuthOrApplicationList', 'myAuth')
+          mittBus.emit('refresAuthOrApplicationList', 'auth')
+          emit('refresh')
+        }
+      } else {
+        Object.keys(error).forEach((key, i) => {
+          if (i == 0) {
+            const firstFiled = error[key][0].field
+            editFormRef.value.scrollToField(firstFiled)
+          }
+        })
+      }
+    })
+  }
+  // 暴露方法
+  defineExpose({
+    openDialog,
+  })
+</script>
+
+<style scoped lang="scss">
+  .add-inst-wrap {
+    // :deep(.el-dialog) {
+    // 	border-radius: 8px;
+    // }
+    :deep(.el-dialog__header) {
+      border-radius: 8px 8px 0 0;
+      margin: 0;
+      font-size: 16px;
+      font-weight: bold;
+      color: #333333;
+      padding: 12px 0 12px 22px;
+    }
+    :deep(.el-dialog__headerbtn) {
+      top: 0;
+    }
+    .add-inst-content {
+      .r-form {
+        padding: 10px 0 0 5px;
+        .w100 {
+          width: 100%;
+        }
+        .upload-box {
+          :deep(.el-form-item__content) {
+            justify-content: center;
+          }
+          :deep(.el-form-item__error) {
+            width: 100%;
+            text-align: center;
+            margin-top: 40px;
+          }
+          .upload-btn {
+            width: 200px;
+            height: 200px;
+            border-radius: 4px;
+            border: 1px solid #cdcdcd;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            > p {
+              font-size: 14px;
+              color: #969696;
+              margin-top: 30px;
+            }
+          }
+        }
+        .file-size-tips {
+          text-align: center;
+          margin-top: 8px;
+          font-size: 16px;
+          color: #969696;
+        }
+        .form-group {
+          // padding: 15px 0 0 15px;
+          .form-item-tit {
+            font-size: 14px;
+            font-weight: bold;
+            color: #333333;
+            position: relative;
+            .form-item-icon {
+              position: absolute;
+              top: -2px;
+              left: -5px;
+              width: 12px;
+              height: 12px;
+              background: linear-gradient(138deg, #4e82ff 0%, #2c46ff 100%);
+              opacity: 0.3;
+              border-radius: 50%;
+            }
+          }
+          .form-row-wrap {
+            // padding-left: 27px;
+          }
+        }
+        :deep(.el-form-item__label) {
+          font-size: 14px;
+          // font-weight: bold;
+          color: #343a3f;
+        }
+        .report-link {
+          background: #eef3fe;
+          border-radius: 12px;
+          padding: 0 12px;
+          line-height: 24px;
+        }
+      }
+      :deep(.el-input-number) {
+        width: 90px;
+      }
+      .add-btn {
+        width: 113px;
+        height: 32px;
+        color: #2c78ff;
+      }
+    }
+    .algin-center {
+      align-items: center;
+    }
+  }
+  .disUoloadSty {
+    :deep(.el-upload--picture-card) {
+      display: none; /* 上传按钮隐藏 */
+    }
+  }
+</style>

+ 246 - 40
src/view/instr/detail.vue

@@ -8,10 +8,21 @@
 -->
 <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>
@@ -19,7 +30,11 @@
     <header class="flex">
       <div class="h100">
         <!-- <img :showLoading="true" :src="state.instDetail.instPicture" width="80px" height="80px" /> -->
-        <van-image width="80px" height="80px" :src="state.instDetail.instPicture" />
+        <van-image
+          width="80px"
+          height="80px"
+          :src="state.instDetail.instPicture"
+        />
       </div>
       <div class="i-right ml10">
         <div class="h100 flex flex-top flex-column flex-between">
@@ -28,23 +43,48 @@
           </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" />
-              <div class="detailTxt">{{ state.instDetail.placeAddress + setLaboratoryName(state.instDetail.laboratoryName) }}</div>
+              <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>
             </div>
           </footer>
         </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>
@@ -90,6 +130,20 @@
         <h4>主要功能</h4>
         <div class="text">{{ state.instDetail.instFunctFeat }}</div>
       </div>
+      <div
+        class="card"
+        v-if="isNeedGrant"
+        @click="applicationAuth"
+      >
+        <h4>资质申请</h4>
+      </div>
+      <div
+        class="card"
+        v-if="isNeedGrant"
+        @click="applyTraining"
+      >
+        <h4>培训申请</h4>
+      </div>
       <!-- <div class="card">
             <h4>相关附件</h4>
             <template v-for="item in state.instFiles">
@@ -99,22 +153,78 @@
             </template>
           </div> -->
     </div>
-    <van-list v-else v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad">
+    <van-list
+      v-else
+      v-model:loading="state.loading"
+      :finished="state.finished"
+      finished-text="没有更多了"
+      @load="onLoad"
+    >
       <van-cell v-for="item in state.list">
         <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>
-              <van-tag v-else-if="item.appointStatus == '11'" type="warning">已退回</van-tag>
-              <van-tag v-else-if="item.appointStatus == '20'" type="success">已通过</van-tag>
-              <van-tag v-else-if="item.appointStatus == '30'" type="danger">已驳回</van-tag>
-              <van-tag v-else-if="item.appointStatus == '40'" type="warning">已取消</van-tag>
-              <van-tag v-else-if="item.appointStatus == '50'" type="default">已上机</van-tag>
-              <van-tag v-else-if="item.appointStatus == '60'" type="primary">已完成</van-tag>
-              <van-tag v-else-if="item.appointStatus == '70'" type="warning">审核超时</van-tag>
-              <van-tag v-else-if="item.appointStatus == '80'" type="danger">超时取消</van-tag>
-              <van-tag v-else-if="item.appointStatus == '90'" type="danger">超时未上机</van-tag>
+              <van-tag
+                v-if="item.appointStatus == '10'"
+                type="default"
+              >
+                待审核
+              </van-tag>
+              <van-tag
+                v-else-if="item.appointStatus == '11'"
+                type="warning"
+              >
+                已退回
+              </van-tag>
+              <van-tag
+                v-else-if="item.appointStatus == '20'"
+                type="success"
+              >
+                已通过
+              </van-tag>
+              <van-tag
+                v-else-if="item.appointStatus == '30'"
+                type="danger"
+              >
+                已驳回
+              </van-tag>
+              <van-tag
+                v-else-if="item.appointStatus == '40'"
+                type="warning"
+              >
+                已取消
+              </van-tag>
+              <van-tag
+                v-else-if="item.appointStatus == '50'"
+                type="default"
+              >
+                已上机
+              </van-tag>
+              <van-tag
+                v-else-if="item.appointStatus == '60'"
+                type="primary"
+              >
+                已完成
+              </van-tag>
+              <van-tag
+                v-else-if="item.appointStatus == '70'"
+                type="warning"
+              >
+                审核超时
+              </van-tag>
+              <van-tag
+                v-else-if="item.appointStatus == '80'"
+                type="danger"
+              >
+                超时取消
+              </van-tag>
+              <van-tag
+                v-else-if="item.appointStatus == '90'"
+                type="danger"
+              >
+                超时未上机
+              </van-tag>
             </header>
             <p class="inst-title">
               <span>预约仪器</span>
@@ -122,7 +232,11 @@
             </p>
             <p class="inst-title">
               <span>预约时间</span>
-              <span class="title ml8">{{ formatDate(new Date(item.startTime), 'mm-dd HH:MM') }}~{{ formatDate(new Date(item.endTime), 'mm-dd HH:MM') }}</span>
+              <span class="title ml8">
+                {{ formatDate(new Date(item.startTime), 'mm-dd HH:MM') }}~{{
+                  formatDate(new Date(item.endTime), 'mm-dd HH:MM')
+                }}
+              </span>
             </p>
             <p class="inst-title">
               <span>预约时长</span>
@@ -148,34 +262,77 @@
         </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="calendar-o" text="周视图" />
-    <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-button type="primary" text="立即预约" @click="onAppoint" />
+    <van-action-bar-icon
+      icon="wap-home-o"
+      text="首页"
+      @click="onRouterPush('/home')"
+    />
+    <van-action-bar-icon
+      icon="calendar-o"
+      text="周视图"
+    />
+    <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-button
+      type="primary"
+      text="立即预约"
+      @click="onAppoint"
+    />
   </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>
+        <van-button
+          class="w100"
+          type="primary"
+          round
+          @click="confirmAppoint"
+        >
+          我知道了
+        </van-button>
       </footer>
     </div>
   </van-popup>
+  <AddAuthDialog ref="addAuthDialogRef" />
 </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'
@@ -185,6 +342,10 @@
   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'
+
   const route = useRoute()
   const router = useRouter()
   const instApi = useInstrApi()
@@ -192,13 +353,14 @@
   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: '报废'
+      30: '报废',
     },
     instDetail: {} as any,
     instFiles: [] as any[],
@@ -208,13 +370,26 @@
       pageNum: 1,
       pageSize: 10,
       instId: 0,
-      appointStatus: []
+      appointStatus: [],
     },
     list: [] as any[],
     popupShow: false,
-    needToKnowShow: false
+    needToKnowShow: false,
   })
   const noticeInfo = ref({ noticeTitle: '', noticeContent: '' })
+
+  const isNeedGrant = ref(false)
+
+  const addAuthDialogRef = ref()
+
+  const { userInfos } = useUserInfos()
+
+  const getNeedGrant = async (instId: number) => {
+    const [err, res]: ToResponse = await to(useAppointApi.getNeedGrant({ instId }))
+    if (err) return
+    isNeedGrant.value = res?.data
+  }
+
   // 获取仪器详情
   const getDetail = async (id: number) => {
     state.detailsLoading = true
@@ -231,7 +406,7 @@
     const param = {
       pageNum: 1,
       pageSize: 1,
-      instId: state.instDetail.instId
+      instId: state.instDetail.instId,
     }
     const [err, res]: ToResponse = await to(noticeApi.list({ ...param }))
     if (err) return
@@ -239,7 +414,9 @@
   }
   // 附件列表
   const getDocs = async () => {
-    const [err, res]: ToResponse = await to(instDocApi.list({ noPage: true, instId: state.instDetail.instId, docType: '' }))
+    const [err, res]: ToResponse = await to(
+      instDocApi.list({ noPage: true, instId: state.instDetail.instId, docType: '' }),
+    )
     if (err) return
     state.instFiles = res?.data.list || []
   }
@@ -256,7 +433,7 @@
   // 创建a标签 实现下载
   const downloadFun = async (blobFile, fileName) => {
     let blob = new Blob([blobFile], {
-      type: 'application/pdf;charset=UTF-8'
+      type: 'application/pdf;charset=UTF-8',
     })
     // @ts-ignore
     if (window.navigator.msSaveOrOpenBlob) {
@@ -281,7 +458,7 @@
         pageNum: 1,
         pageSize: 10,
         instId: state.instDetail.id,
-        appointStatus: name === 'approval' ? ['10'] : []
+        appointStatus: name === 'approval' ? ['10'] : [],
       }
       onLoad()
     }
@@ -349,12 +526,41 @@
   const onRouterPush = (val: string, params?: any) => {
     router.push({
       path: val,
-      query: { ...params }
+      query: { ...params },
     })
   }
+
+  const applicationAuth = async () => {
+    console.log('applicationAuth')
+    addAuthDialogRef.value.openDialog('personal', state.instDetail)
+  }
+
+  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('培训申请提交成功')
+      })
+      .catch(() => {})
+  }
+
   onMounted(() => {
     const id = route.query.id ? +route.query.id : 0
     getDetail(id)
+    getNeedGrant(id)
   })
 </script>
 

+ 173 - 37
src/view/instr/list.vue

@@ -1,49 +1,122 @@
 <template>
   <div class="home">
     <div class="search-wrap">
-      <van-search placeholder="请输入仪器名称" v-model="state.queryForm.searchText" :clearabled="true" style="padding: 0"></van-search>
+      <van-search
+        placeholder="请输入仪器名称"
+        v-model="state.queryForm.searchText"
+        :clearabled="true"
+        style="padding: 0"
+      ></van-search>
       <div class="ml10 mr10">
-        <van-button type="primary" style="width: 60px" shape="circle" size="small" @click="state.filterVisible = true">过滤</van-button>
+        <van-button
+          type="primary"
+          style="width: 60px"
+          shape="circle"
+          size="small"
+          @click="state.filterVisible = true"
+        >
+          过滤
+        </van-button>
       </div>
       <div>
-        <van-button type="primary" style="width: 60px" shape="circle" size="small" @click="onSearch"> 搜索 </van-button>
+        <van-button
+          type="primary"
+          style="width: 60px"
+          shape="circle"
+          size="small"
+          @click="onSearch"
+        >
+          搜索
+        </van-button>
       </div>
     </div>
     <!-- 筛选 -->
-    <div class="filter-popup" v-if="state.filterVisible">
+    <div
+      class="filter-popup"
+      v-if="state.filterVisible"
+    >
       <div class="filter-wrap">
         <div @click="openSelectLaboratory">
-          <van-field readonly v-model="state.queryForm.laboratoryName" label="实验室:" placeholder="请选择实验室" />
+          <van-field
+            readonly
+            v-model="state.queryForm.laboratoryName"
+            label="实验室:"
+            placeholder="请选择实验室"
+          />
         </div>
       </div>
       <div class="btn-box">
-        <div class="reset" @click="reset()">重置</div>
-        <div class="submit" @click="confirmFilter()">确定</div>
+        <div
+          class="reset"
+          @click="reset()"
+        >
+          重置
+        </div>
+        <div
+          class="submit"
+          @click="confirmFilter()"
+        >
+          确定
+        </div>
       </div>
     </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="v.instPicture" />
+                <van-image
+                  width="100px"
+                  height="100px"
+                  :src="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 }}</div>
                   </div>
+                  <div class="flex flex-top mb4 ml2">
+                    <div class="detailTxt name">{{ v.instNameEn }}</div>
+                  </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>
@@ -54,16 +127,44 @@
         </van-list>
       </div>
     </div>
-    <van-tabbar route :placeholder="true">
-      <van-tabbar-item replace to="/instr-follow" icon="star">收藏仪器</van-tabbar-item>
-      <van-tabbar-item replace to="/instr-list" icon="printer">全部仪器</van-tabbar-item>
-      <van-tabbar-item replace to="/instr-appoint-record" icon="label">我的预约</van-tabbar-item>
+    <van-tabbar
+      route
+      :placeholder="true"
+    >
+      <van-tabbar-item
+        replace
+        to="/instr-follow"
+        icon="star"
+      >
+        收藏仪器
+      </van-tabbar-item>
+      <van-tabbar-item
+        replace
+        to="/instr-list"
+        icon="printer"
+      >
+        全部仪器
+      </van-tabbar-item>
+      <van-tabbar-item
+        replace
+        to="/instr-appoint-record"
+        icon="label"
+      >
+        我的预约
+      </van-tabbar-item>
     </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>
@@ -78,7 +179,9 @@
           </div>
           <div class="flex">
             <div class="label-item">位置:</div>
-            <div class="detailTxt">{{ state.instDetail.placeAddress + '(' + state.instDetail.laboratoryName + ')' }}</div>
+            <div class="detailTxt">
+              {{ state.instDetail.placeAddress + '(' + state.instDetail.laboratoryName + ')' }}
+            </div>
           </div>
           <!--<div>
            <div class="label-item">开放时段:</div>
@@ -98,19 +201,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> -->
@@ -128,8 +261,8 @@
   const props = defineProps({
     following: {
       type: String,
-      default: '10'
-    }
+      default: '10',
+    },
   })
   const router = useRouter()
   const instApi = useInstrApi()
@@ -143,7 +276,7 @@
     instStatus: {
       10: '正常',
       20: '故障',
-      30: '报废'
+      30: '报废',
     },
     instList: [],
     current: 1,
@@ -152,14 +285,14 @@
       laboratoryName: '',
       pageNum: 1,
       pageSize: 10,
-      searchText: ''
+      searchText: '',
     },
     popupShow: false,
     instDetail: {} as any,
     total: 0,
     detailsLoading: false, //详情加载
     loading: false,
-    finished: false
+    finished: false,
   })
   const setLaboratoryName = (name) => {
     return name ? `(${name})` : ''
@@ -198,8 +331,8 @@
     router.push({
       path: '/instr-detail',
       query: {
-        id: v.id
-      }
+        id: v.id,
+      },
     })
     // state.popupShow = true
     // getDetail(v.id)
@@ -217,7 +350,8 @@
     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.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
@@ -252,7 +386,9 @@
   }
   // 关注/取关
   const handleFollowInst = async (row) => {
-    const [err] = row.following ? await to(instApi.unfollow({ ids: [row.id] })) : await to(instApi.follow({ ids: [row.id] }))
+    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) => {