Преглед изворни кода

feature:(仪器)增加预约和预约记录

liuzhenlin пре 10 месеци
родитељ
комит
1de56f5e8a

+ 1 - 0
.env.development

@@ -21,6 +21,7 @@ VITE_FINANCE = dashoo.labsop.finance-34000
 VITE_SCI = dashoo.labsop.scientific-34000
 VITE_INSTR_ADMIN = dashoo.labsop.apparatus-34000
 VITE_LEARNING = dashoo.labsop.learning-34000
+VITE_LABORATORY: dashoo.labsop.laboratory-34000
 
 #公共配置
 VITE_UPLOAD = http://192.168.0.218:9933/weedfs/upload

+ 1 - 0
.gitignore

@@ -22,3 +22,4 @@ dist-ssr
 *.njsproj
 *.sln
 *.sw?
+.history/

+ 8 - 9
components.d.ts

@@ -12,24 +12,23 @@ declare module 'vue' {
     VanButton: typeof import('vant/es')['Button']
     VanCell: typeof import('vant/es')['Cell']
     VanCellGroup: typeof import('vant/es')['CellGroup']
-    VanCheckbox: typeof import('vant/es')['Checkbox']
-    VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
-    VanCol: typeof import('vant/es')['Col']
-    VanDatePicker: typeof import('vant/es')['DatePicker']
+    VanEmpty: typeof import('vant/es')['Empty']
     VanField: typeof import('vant/es')['Field']
     VanForm: typeof import('vant/es')['Form']
     VanIcon: typeof import('vant/es')['Icon']
     VanList: typeof import('vant/es')['List']
     VanNotify: typeof import('vant/es')['Notify']
     VanPicker: typeof import('vant/es')['Picker']
-    VanPickerGroup: typeof import('vant/es')['PickerGroup']
     VanPopup: typeof import('vant/es')['Popup']
     VanRadio: typeof import('vant/es')['Radio']
     VanRadioGroup: typeof import('vant/es')['RadioGroup']
-    VanRow: typeof import('vant/es')['Row']
-    VanStep: typeof import('vant/es')['Step']
-    VanSteps: typeof import('vant/es')['Steps']
-    VanTag: typeof import('vant/es')['Tag']
+    VanSearch: typeof import('vant/es')['Search']
+    VanTab: typeof import('vant/es')['Tab']
+    VanTabbar: typeof import('vant/es')['Tabbar']
+    VanTabbarItem: typeof import('vant/es')['TabbarItem']
+    VanTabs: typeof import('vant/es')['Tabs']
+    VanTextarea: typeof import('vant/es')['Textarea']
     VanTextEllipsis: typeof import('vant/es')['TextEllipsis']
+    VanToast: typeof import('vant/es')['Toast']
   }
 }

+ 1 - 0
package.json

@@ -12,6 +12,7 @@
     "@vitejs/plugin-basic-ssl": "^2.0.0",
     "await-to-js": "^3.0.0",
     "axios": "^1.8.2",
+    "moment": "^2.30.1",
     "pinia": "^3.0.1",
     "postcss-px-to-viewport": "^1.1.1",
     "sm-crypto": "^0.3.13",

+ 8 - 1
pnpm-lock.yaml

@@ -1,7 +1,7 @@
 lockfileVersion: '6.0'
 
 settings:
-  autoInstallPeers: true
+  autoInstallPeers: false
   excludeLinksFromLockfile: false
 
 dependencies:
@@ -14,6 +14,9 @@ dependencies:
   axios:
     specifier: ^1.8.2
     version: 1.8.2
+  moment:
+    specifier: ^2.30.1
+    version: 2.30.1
   pinia:
     specifier: ^3.0.1
     version: 3.0.1(typescript@4.9.5)(vue@3.5.13)
@@ -1013,6 +1016,10 @@ packages:
     resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
     dev: false
 
+  /moment@2.30.1:
+    resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
+    dev: false
+
   /ms@2.1.3:
     resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
     dev: true

+ 35 - 0
src/api/appoint/index.ts

@@ -0,0 +1,35 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-19 14:06:58
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-02-15 18:45:00
+ * @Description: file content
+ * @FilePath: \oms\api\system\user.js
+ */
+import request from '/@/utils/micro_request.js'
+const instPath = import.meta.env.VITE_INSTR_ADMIN
+
+export function useMyAppointApi() {
+  return {
+    // 查询未审核(我的预约)
+    myAppoint: (query?: object) => {
+      return request.postRequest(instPath, 'MyAppointment', 'CreatedList', query)
+    },
+    // 查询预约记录
+    appointRecords: (query?: object) => {
+      return request.postRequest(instPath, 'MyAppointment', 'GetList', query)
+    },
+    // 详情
+    appointDetails: (query?: object) => {
+      return request.postRequest(instPath, 'MyAppointment', 'InProgressDetail', query)
+    },
+    // 正在上机
+    inProgressList: (query?: object) => {
+      return request.postRequest(instPath, 'MyAppointment', 'InProgressList', query)
+    },
+    // 即将上机
+    toGetonList: (query?: object) => {
+      return request.postRequest(instPath, 'MyAppointment', 'ToGetonList', query)
+    },
+  }
+}

+ 37 - 0
src/api/blacklist/index.ts

@@ -0,0 +1,37 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-19 14:06:58
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-02-15 18:45:00
+ * @Description: file content
+ * @FilePath: \oms\api\system\user.js
+ */
+import request from '/@/utils/micro_request.js'
+const instrPath = import.meta.env.VITE_INSTR_ADMIN
+export function useBlackApi() {
+  return {
+    add: (query?: object) => {
+      return request.postRequest(instrPath, 'TusBlacklist', 'Create', query)
+    },
+    //详情
+    getDetail: (query?: object) => {
+      return request.postRequest(instrPath, 'TusBlacklist', 'GetEntityById', query)
+    },
+    //查询
+    getList: (query?: object) => {
+      return request.postRequest(instrPath, 'TusBlacklist', 'GetList', query)
+    },
+    //违约记录
+    getRecord: (query?: object) => {
+      return request.postRequest(instrPath, 'TusBlacklist', 'Record', query)
+    },
+    //移除
+    remove: (query?: object) => {
+      return request.postRequest(instrPath, 'TusBlacklist', 'Reset', query)
+    },
+    // 是否在黑名单中
+    checkInBlacklist: (query?: object) => {
+      return request.postRequest(instrPath, 'TusBlacklist', 'IsBlocked', query)
+    },
+  }
+}

+ 66 - 0
src/api/instr/index.ts

@@ -0,0 +1,66 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-19 14:06:58
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-02-15 18:45:00
+ * @Description: file content
+ * @FilePath: \oms\api\system\user.js
+ */
+import request from '/@/utils/micro_request.js'
+const instrPath = import.meta.env.VITE_INSTR_ADMIN
+
+export function useInstrApi() {
+  return {
+    // 获取使用人仪器列表
+    getList: (query?: object) => {
+      return request.postRequest(instrPath, 'TusInstrument', 'GetList', query)
+    },
+    // 获取仪器详情
+    getDetail: (query?: object) => {
+      return request.postRequest(instrPath, 'TusInstrument', 'GetEntityById', query)
+    },
+    // 获取设置详情
+    getSettingDetail: (query?: object) => {
+      return request.postRequest(instrPath, 'TusInstrument', 'GetConfig', query)
+    },
+    // 获取全部预约情况
+    getAppointInfo: (query?: object) => {
+      return request.postRequest(instrPath, 'TusInstrumentAppointment', 'AppointInfo', query)
+    },
+    // 关注
+    follow: (query?: object) => {
+      return request.postRequest(instrPath, 'TusInstrument', 'Follow', query)
+    },
+    // 取关
+    unfollow: (query?: object) => {
+      return request.postRequest(instrPath, 'TusInstrument', 'Unfollow', query)
+    },
+    // 二维码解码
+    decode: (query?: object) => {
+      return request.postRequest(instrPath, 'TusInstrument', 'QrcodeDecrypto', query)
+    },
+    // 扫码开关
+    switch: (query?: object) => {
+      return request.postRequest(instrPath, 'TusInstrument', 'Switch', query)
+    },
+    // 蓝牙获取连接码
+    getBlueToothCode: (query?: object) => {
+      return request.postRequest(instrPath, 'TusInstrument', 'BluetoothCode', query)
+    },
+    // 根据编码获取仪器详情
+    getIdByTerminal: (query?: object) => {
+      return request.postRequest(instrPath, 'TusInstrument', 'GetEntityByTerminal', query)
+    },
+    // 获取我的信用分
+    getMyScore: (query?: object) => {
+      return request.postRequest(instrPath, 'TusBlacklist', 'MyScore', query)
+    },
+    // 下一个预约是否可以直接上机
+    nextCanGeton: (query?: object) => {
+      return request.postRequest(instrPath, 'TusInstrumentAppointment', 'NextCanGeton', query)
+    },
+    openBlueLock: (query?: object) => {
+      return request.postRequest(instrPath, 'TusInstrument', 'OpenLock', query)
+    }
+  }
+}

+ 40 - 0
src/api/instr/instAppoint.js

@@ -0,0 +1,40 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-19 14:06:58
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-02-15 18:45:00
+ * @Description: file content
+ * @FilePath: \oms\api\system\user.js
+ */
+import micro_request from '../../utils/micro_request'
+const instPath = import.meta.env.VITE_INSTR_ADMIN
+export default {
+  // 预约
+  add(query) {
+    return micro_request.postRequest(instPath, 'TusInstrumentAppointment', 'Create', query)
+  },
+  // 延长预约
+  delayAdd(query) {
+    return micro_request.postRequest(instPath, 'TusInstrumentAppointment', 'GetoffDelay', query)
+  },
+  // 取消预约
+  userCancelAppoint(query) {
+    return micro_request.postRequest(instPath, 'TusInstrumentAppointment', 'UserCancel', query)
+  },
+  // 下机
+  getOff(query) {
+    return micro_request.postRequest(instPath, 'TusInstrumentAppointment', 'Getoff', query)
+  },
+  // 上机
+  getOn(query) {
+    return micro_request.postRequest(instPath, 'TusInstrumentAppointment', 'Geton', query)
+  },
+  // 检查是否可以上机
+  checkGetOn(query) {
+    return micro_request.postRequest(instPath, 'TusInstrumentAppointment', 'GetonCheck', query)
+  },
+  // 上机 提示使用延期功能
+  getoffDelayAlert(query) {
+    return micro_request.postRequest(instPath, 'TusInstrumentAppointment', 'GetoffDelayAlert', query)
+  }
+}

+ 54 - 0
src/api/instr/position.ts

@@ -0,0 +1,54 @@
+import request from '/@/utils/micro_request.js'
+const instrPath = import.meta.env.VITE_INSTR_ADMIN
+const labPath = import.meta.env.VITE_LABORATORY
+
+// 楼栋管理、实验室管理
+export function usePositionApi() {
+  return {
+    // 创建楼栋
+    createBuilding: (query?: object) => {
+      return request.postRequest(instrPath, 'TusBuilding', 'Create', query)
+    },
+    // 删除楼栋
+    deleteBuilding: (query?: object) => {
+      return request.postRequest(instrPath, 'TusBuilding', 'DeleteByIds', query)
+    },
+    // 楼栋详情
+    getBuildingEntity: (query?: object) => {
+      return request.postRequest(instrPath, 'TusBuilding', 'GetEntityById', query)
+    },
+    // 楼栋列表
+    getBuildingList: (query?: object) => {
+      return request.postRequest(instrPath, 'TusBuilding', 'GetList', query)
+    },
+    // 楼栋更新
+    updateBuilding: (query?: object) => {
+      return request.postRequest(instrPath, 'TusBuilding', 'UpdateById', query)
+    },
+    // 创建楼栋
+    createLaboratory: (query?: object) => {
+      return request.postRequest(instrPath, 'TusLaboratory', 'Create', query)
+    },
+    // 删除楼栋
+    deleteLaboratory: (query?: object) => {
+      return request.postRequest(instrPath, 'TusLaboratory', 'DeleteByIds', query)
+    },
+    // 楼栋详情
+    getLaboratoryEntity: (query?: object) => {
+      return request.postRequest(instrPath, 'TusLaboratory', 'GetEntityById', query)
+    },
+    // 楼栋列表
+    getLaboratoryList: (query?: object) => {
+      const instr_is_concat_lab = JSON.parse(localStorage.getItem('instr_is_concat_lab') || '{}')
+      if (instr_is_concat_lab == '10') {
+        return request.postRequest(labPath, 'laboratory', 'GetList', query)
+      } else {
+        return request.postRequest(instrPath, 'TusLaboratory', 'GetList', query)
+      }
+    },
+    // 楼栋更新
+    updateLaboratory: (query?: object) => {
+      return request.postRequest(instrPath, 'TusLaboratory', 'UpdateById', query)
+    },
+  }
+}

+ 17 - 0
src/api/technical/index.js

@@ -0,0 +1,17 @@
+/*
+ * @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 micro_request from '../../utils/micro_request'
+const basePath = process.uniEnv.VITE_INSTR_ADMIN
+// 技术服务
+export default {
+  // 列表
+  getList(query) {
+    return micro_request.postRequest(basePath, 'TechTechnicalService', 'GetList', query)
+  },
+}

BIN
src/assets/img/address.png


BIN
src/assets/img/follow.png


BIN
src/assets/img/unfollow.png


BIN
src/assets/img/user.png


+ 34 - 1
src/router.ts

@@ -33,6 +33,39 @@ const routes = [
       title: '注册'
     }
   },
+  {
+    name: 'instrList',
+    path: '/instr-list',
+    component: () => import('/@/view/instr/list.vue'),
+    meta: {
+      title: '仪器列表'
+    }
+  },
+  {
+    name: 'appointInfo',
+    path: '/instr-appoint-record',
+    component: () => import('/@/view/instr/appointList/index.vue'),
+    meta: {
+      title: '预约详情'
+    }
+  },
+  {
+    name: 'onlineInfo',
+    path: '/onlineInfo',
+    component: () => import('/@/view/instr/appointList/onlineInfo/index.vue'),
+    meta: {
+      title: '上机详情'
+    }
+  },
+  {
+    name: 'appoint',
+    path: '/inst/appoint',
+    component: () => import('/@/view/instr/appoint/index.vue'),
+    meta: {
+      title: '仪器预约'
+    }
+  },
+
   {
     path: '/',
     redirect: '/login',
@@ -125,7 +158,7 @@ const routes = [
         meta: {
           title: '在线考试'
         }
-      }
+      },
     ]
   }
 ]

+ 141 - 25
src/theme/index.scss

@@ -14,9 +14,11 @@ body,
   overflow: auto;
   position: relative;
 }
+
 #app {
   display: flex;
   flex-direction: column;
+
   .app-container {
     height: calc(100vh - 48px);
     padding: 0 10px;
@@ -24,58 +26,172 @@ body,
     background-color: #f7f8fa;
   }
 }
-h1, h2, h3, h4, p {
+
+h1,
+h2,
+h3,
+h4,
+p {
   margin: 0;
 }
+
 /* 宽高 100%
 ------------------------------- */
 .w100 {
   width: 100% !important;
 }
+
 .h100 {
   height: 100% !important;
 }
+
 .vh100 {
   height: 100vh !important;
 }
+
 .max100vh {
   max-height: 100vh !important;
 }
+
 .min100vh {
   min-height: 100vh !important;
 }
+
 /* 外边距、内边距全局样式
 ------------------------------- */
 @for $i from 1 through 35 {
-	.mt#{$i} {
-		margin-top: #{$i}px !important;
-	}
-	.mr#{$i} {
-		margin-right: #{$i}px !important;
-	}
-	.mb#{$i} {
-		margin-bottom: #{$i}px !important;
-	}
-	.ml#{$i} {
-		margin-left: #{$i}px !important;
-	}
-	.pt#{$i} {
-		padding-top: #{$i}px !important;
-	}
-	.pr#{$i} {
-		padding-right: #{$i}px !important;
-	}
-	.pb#{$i} {
-		padding-bottom: #{$i}px !important;
-	}
-	.pl#{$i} {
-		padding-left: #{$i}px !important;
-	}
+  .mt#{$i} {
+    margin-top: #{$i}px !important;
+  }
+
+  .mr#{$i} {
+    margin-right: #{$i}px !important;
+  }
+
+  .mb#{$i} {
+    margin-bottom: #{$i}px !important;
+  }
+
+  .ml#{$i} {
+    margin-left: #{$i}px !important;
+  }
+
+  .pt#{$i} {
+    padding-top: #{$i}px !important;
+  }
+
+  .pr#{$i} {
+    padding-right: #{$i}px !important;
+  }
+
+  .pb#{$i} {
+    padding-bottom: #{$i}px !important;
+  }
+
+  .pl#{$i} {
+    padding-left: #{$i}px !important;
+  }
 }
+
 .flex {
   display: flex;
   align-items: center;
 }
+
 .justify-between {
   justify-content: space-between;
 }
+
+/* flex */
+
+.center {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.flex_between {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.flex_l {
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+}
+
+.flex_r {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+
+.flex-center {
+  justify-content: center;
+}
+
+.flex-around {
+  justify-content: space-around;
+}
+
+.flex-between {
+  justify-content: space-between;
+}
+
+.flex-top {
+  align-items: flex-start;
+}
+
+.flex-middle {
+  align-items: center;
+}
+
+.justify-end {
+  justify-content: flex-end;
+}
+
+.flex-bottom {
+  align-items: flex-end;
+}
+
+.flex-column {
+  flex-direction: column;
+}
+
+.flex-wrap {
+  flex-wrap: wrap;
+}
+
+.flex_1 {
+  flex: 1;
+}
+
+.primary-color {
+  color: #3c9cff;
+}
+
+.warning-color {
+  color: #ff976a;
+}
+
+.danger-color {
+  color: #ee0a24;
+}
+
+.success-color {
+  color: #07c160;
+}
+
+.bold {
+  font-weight: bold;
+}
+
+/* 文字大小全局样式
+------------------------------- */
+@for $i from 1 through 100 {
+  .fontSize#{$i} {
+    font-size: #{$i}px !important;
+  }
+}

+ 134 - 132
src/utils/formatTime.ts

@@ -10,87 +10,89 @@
  * @returns 返回拼接后的时间字符串
  */
 export function formatDate(date: Date, format: string): string {
-	let we = date.getDay(); // 星期
-	let z = getWeek(date); // 周
-	let qut = Math.floor((date.getMonth() + 3) / 3).toString(); // 季度
-	const opt: { [key: string]: string } = {
-		'Y+': date.getFullYear().toString(), // 年
-		'm+': (date.getMonth() + 1).toString(), // 月(月份从0开始,要+1)
-		'd+': date.getDate().toString(), // 日
-		'H+': date.getHours().toString(), // 时
-		'M+': date.getMinutes().toString(), // 分
-		'S+': date.getSeconds().toString(), // 秒
-		'q+': qut, // 季度
-	};
-	// 中文数字 (星期)
-	const week: { [key: string]: string } = {
-		'0': '日',
-		'1': '一',
-		'2': '二',
-		'3': '三',
-		'4': '四',
-		'5': '五',
-		'6': '六',
-	};
-	// 中文数字(季度)
-	const quarter: { [key: string]: string } = {
-		'1': '一',
-		'2': '二',
-		'3': '三',
-		'4': '四',
-	};
-	if (/(W+)/.test(format))
-		format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]);
-	if (/(Q+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]);
-	if (/(Z+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 3 ? '第' + z + '周' : z + '');
-	for (let k in opt) {
-		let r = new RegExp('(' + k + ')').exec(format);
-		// 若输入的长度不为1,则前面补零
-		if (r) format = format.replace(r[1], RegExp.$1.length == 1 ? opt[k] : opt[k].padStart(RegExp.$1.length, '0'));
-	}
-	return format;
+  console.log(date);
+
+  let we = date.getDay(); // 星期
+  let z = getWeek(date); // 周
+  let qut = Math.floor((date.getMonth() + 3) / 3).toString(); // 季度
+  const opt: { [key: string]: string } = {
+    'Y+': date.getFullYear().toString(), // 年
+    'm+': (date.getMonth() + 1).toString(), // 月(月份从0开始,要+1)
+    'd+': date.getDate().toString(), // 日
+    'H+': date.getHours().toString(), // 时
+    'M+': date.getMinutes().toString(), // 分
+    'S+': date.getSeconds().toString(), // 秒
+    'q+': qut, // 季度
+  };
+  // 中文数字 (星期)
+  const week: { [key: string]: string } = {
+    '0': '日',
+    '1': '一',
+    '2': '二',
+    '3': '三',
+    '4': '四',
+    '5': '五',
+    '6': '六',
+  };
+  // 中文数字(季度)
+  const quarter: { [key: string]: string } = {
+    '1': '一',
+    '2': '二',
+    '3': '三',
+    '4': '四',
+  };
+  if (/(W+)/.test(format))
+    format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]);
+  if (/(Q+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]);
+  if (/(Z+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 3 ? '第' + z + '周' : z + '');
+  for (let k in opt) {
+    let r = new RegExp('(' + k + ')').exec(format);
+    // 若输入的长度不为1,则前面补零
+    if (r) format = format.replace(r[1], RegExp.$1.length == 1 ? opt[k] : opt[k].padStart(RegExp.$1.length, '0'));
+  }
+  return format;
 }
 export function parseTime(time: Date, format: string): string {
   const date = new Date(time)
-	let we = date.getDay(); // 星期
-	let z = getWeek(date); // 周
-	let qut = Math.floor((date.getMonth() + 3) / 3).toString(); // 季度
-	const opt: { [key: string]: string } = {
-		'Y+': date.getFullYear().toString(), // 年
-		'm+': (date.getMonth() + 1).toString(), // 月(月份从0开始,要+1)
-		'd+': date.getDate().toString(), // 日
-		'H+': date.getHours().toString(), // 时
-		'M+': date.getMinutes().toString(), // 分
-		'S+': date.getSeconds().toString(), // 秒
-		'q+': qut, // 季度
-	};
-	// 中文数字 (星期)
-	const week: { [key: string]: string } = {
-		'0': '日',
-		'1': '一',
-		'2': '二',
-		'3': '三',
-		'4': '四',
-		'5': '五',
-		'6': '六',
-	};
-	// 中文数字(季度)
-	const quarter: { [key: string]: string } = {
-		'1': '一',
-		'2': '二',
-		'3': '三',
-		'4': '四',
-	};
-	if (/(W+)/.test(format))
-		format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]);
-	if (/(Q+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]);
-	if (/(Z+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 3 ? '第' + z + '周' : z + '');
-	for (let k in opt) {
-		let r = new RegExp('(' + k + ')').exec(format);
-		// 若输入的长度不为1,则前面补零
-		if (r) format = format.replace(r[1], RegExp.$1.length == 1 ? opt[k] : opt[k].padStart(RegExp.$1.length, '0'));
-	}
-	return format;
+  let we = date.getDay(); // 星期
+  let z = getWeek(date); // 周
+  let qut = Math.floor((date.getMonth() + 3) / 3).toString(); // 季度
+  const opt: { [key: string]: string } = {
+    'Y+': date.getFullYear().toString(), // 年
+    'm+': (date.getMonth() + 1).toString(), // 月(月份从0开始,要+1)
+    'd+': date.getDate().toString(), // 日
+    'H+': date.getHours().toString(), // 时
+    'M+': date.getMinutes().toString(), // 分
+    'S+': date.getSeconds().toString(), // 秒
+    'q+': qut, // 季度
+  };
+  // 中文数字 (星期)
+  const week: { [key: string]: string } = {
+    '0': '日',
+    '1': '一',
+    '2': '二',
+    '3': '三',
+    '4': '四',
+    '5': '五',
+    '6': '六',
+  };
+  // 中文数字(季度)
+  const quarter: { [key: string]: string } = {
+    '1': '一',
+    '2': '二',
+    '3': '三',
+    '4': '四',
+  };
+  if (/(W+)/.test(format))
+    format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]);
+  if (/(Q+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]);
+  if (/(Z+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 3 ? '第' + z + '周' : z + '');
+  for (let k in opt) {
+    let r = new RegExp('(' + k + ')').exec(format);
+    // 若输入的长度不为1,则前面补零
+    if (r) format = format.replace(r[1], RegExp.$1.length == 1 ? opt[k] : opt[k].padStart(RegExp.$1.length, '0'));
+  }
+  return format;
 }
 /**
  * 获取当前日期是第几周
@@ -98,19 +100,19 @@ export function parseTime(time: Date, format: string): string {
  * @returns 返回第几周数字值
  */
 export function getWeek(dateTime: Date): number {
-	let temptTime = new Date(dateTime.getTime());
-	// 周几
-	let weekday = temptTime.getDay() || 7;
-	// 周1+5天=周六
-	temptTime.setDate(temptTime.getDate() - weekday + 1 + 5);
-	let firstDay = new Date(temptTime.getFullYear(), 0, 1);
-	let dayOfWeek = firstDay.getDay();
-	let spendDay = 1;
-	if (dayOfWeek != 0) spendDay = 7 - dayOfWeek + 1;
-	firstDay = new Date(temptTime.getFullYear(), 0, 1 + spendDay);
-	let d = Math.ceil((temptTime.valueOf() - firstDay.valueOf()) / 86400000);
-	let result = Math.ceil(d / 7);
-	return result;
+  let temptTime = new Date(dateTime.getTime());
+  // 周几
+  let weekday = temptTime.getDay() || 7;
+  // 周1+5天=周六
+  temptTime.setDate(temptTime.getDate() - weekday + 1 + 5);
+  let firstDay = new Date(temptTime.getFullYear(), 0, 1);
+  let dayOfWeek = firstDay.getDay();
+  let spendDay = 1;
+  if (dayOfWeek != 0) spendDay = 7 - dayOfWeek + 1;
+  firstDay = new Date(temptTime.getFullYear(), 0, 1 + spendDay);
+  let d = Math.ceil((temptTime.valueOf() - firstDay.valueOf()) / 86400000);
+  let result = Math.ceil(d / 7);
+  return result;
 }
 
 /**
@@ -125,38 +127,38 @@ export function getWeek(dateTime: Date): number {
  * @returns 返回拼接后的时间字符串
  */
 export function formatPast(param: string | Date, format: string = 'YYYY-mm-dd'): string {
-	// 传入格式处理、存储转换值
-	let t: any, s: number;
-	// 获取js 时间戳
-	let time: number = new Date().getTime();
-	// 是否是对象
-	typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param);
-	// 当前时间戳 - 传入时间戳
-	time = Number.parseInt(`${time - t}`);
-	if (time < 10000) {
-		// 10秒内
-		return '刚刚';
-	} else if (time < 60000 && time >= 10000) {
-		// 超过10秒少于1分钟内
-		s = Math.floor(time / 1000);
-		return `${s}秒前`;
-	} else if (time < 3600000 && time >= 60000) {
-		// 超过1分钟少于1小时
-		s = Math.floor(time / 60000);
-		return `${s}分钟前`;
-	} else if (time < 86400000 && time >= 3600000) {
-		// 超过1小时少于24小时
-		s = Math.floor(time / 3600000);
-		return `${s}小时前`;
-	} else if (time < 259200000 && time >= 86400000) {
-		// 超过1天少于3天内
-		s = Math.floor(time / 86400000);
-		return `${s}天前`;
-	} else {
-		// 超过3天
-		let date = typeof param === 'string' || 'object' ? new Date(param) : param;
-		return formatDate(date, format);
-	}
+  // 传入格式处理、存储转换值
+  let t: any, s: number;
+  // 获取js 时间戳
+  let time: number = new Date().getTime();
+  // 是否是对象
+  typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param);
+  // 当前时间戳 - 传入时间戳
+  time = Number.parseInt(`${time - t}`);
+  if (time < 10000) {
+    // 10秒内
+    return '刚刚';
+  } else if (time < 60000 && time >= 10000) {
+    // 超过10秒少于1分钟内
+    s = Math.floor(time / 1000);
+    return `${s}秒前`;
+  } else if (time < 3600000 && time >= 60000) {
+    // 超过1分钟少于1小时
+    s = Math.floor(time / 60000);
+    return `${s}分钟前`;
+  } else if (time < 86400000 && time >= 3600000) {
+    // 超过1小时少于24小时
+    s = Math.floor(time / 3600000);
+    return `${s}小时前`;
+  } else if (time < 259200000 && time >= 86400000) {
+    // 超过1天少于3天内
+    s = Math.floor(time / 86400000);
+    return `${s}天前`;
+  } else {
+    // 超过3天
+    let date = typeof param === 'string' || 'object' ? new Date(param) : param;
+    return formatDate(date, format);
+  }
 }
 
 /**
@@ -166,13 +168,13 @@ export function formatPast(param: string | Date, format: string = 'YYYY-mm-dd'):
  * @returns 返回拼接后的时间字符串
  */
 export function formatAxis(param: Date): string {
-	let hour: number = new Date(param).getHours();
-	if (hour < 6) return '凌晨好';
-	else if (hour < 9) return '早上好';
-	else if (hour < 12) return '上午好';
-	else if (hour < 14) return '中午好';
-	else if (hour < 17) return '下午好';
-	else if (hour < 19) return '傍晚好';
-	else if (hour < 22) return '晚上好';
-	else return '夜里好';
+  let hour: number = new Date(param).getHours();
+  if (hour < 6) return '凌晨好';
+  else if (hour < 9) return '早上好';
+  else if (hour < 12) return '上午好';
+  else if (hour < 14) return '中午好';
+  else if (hour < 17) return '下午好';
+  else if (hour < 19) return '傍晚好';
+  else if (hour < 22) return '晚上好';
+  else return '夜里好';
 }

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

@@ -23,7 +23,7 @@
     <div class="card">
       <h4>常用功能</h4>
       <ul class="nav">
-        <li>
+        <li @click="onRouterPush('/instr-list')">
           <img src="../../assets/img/home-equip.png" alt="" />
           <p>仪器预约</p>
         </li>

+ 344 - 0
src/view/instr/appoint/components/selectTime.vue

@@ -0,0 +1,344 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-04-18 15:16:21
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-08-10 16:27:44
+ * @Description: file content
+ * @FilePath: \frontend_mobile\src\pages\reserve\components\selectTime.vue
+-->
+<template>
+  <view class="time-select-container">
+    <view class="time-box">
+      <view class="content">
+        <view class="tit">选择预约时间段</view>
+        <view>
+          <picker mode="multiSelector" :range="startDateColumns" disabled>
+            <u-cell-group>
+              <u-cell icon="calendar" title="开始时间>" :value="selectedStartDate"></u-cell>
+            </u-cell-group>
+            <!-- <view>开始时间:</view>
+            <view>{{ selectedStartDate }}</view> -->
+          </picker>
+        </view>
+        <view>
+          <picker mode="multiSelector" :range="endDateColumns" @change="confirmEndColumn" @columnchange="changeEndDateColumn">
+            <u-cell-group>
+              <u-cell icon="calendar" title="结束时间>" :value="selectedEndDate"></u-cell>
+            </u-cell-group>
+            <!-- <AtList>
+              <AtListItem title="结束时间 >" :extra-text="selectedEndDate" />
+            </AtList> -->
+          </picker>
+        </view>
+      </view>
+      <view class="footer">
+        <view class="btn cancel" v-if="showCancel" @tap="cancel()">取 消</view>
+        <view class="btn sub" @tap="nextSubmit()">下一步</view>
+      </view>
+    </view>
+    <u-notify ref="uNotify"></u-notify>
+    <!-- <sub-appoint ref="subAppointRef" @isSubmitted="isSubmitted()" /> -->
+  </view>
+</template>
+
+<script>
+  import to from 'await-to-js'
+  // import SubAppoint from '/@/view/instr/appoint/components/subAppoint.vue'
+  import moment from 'moment'
+  import instAppointApi from '/@/api/instr/instAppoint'
+  export default {
+    name: 'FrontendMobileTimeInfo',
+    // components: { SubAppoint },
+
+    props: {
+      // 提前预约时间
+      furtherLimit: {
+        type: String,
+        default: () => null,
+        required: true
+      },
+      instrInfo: {
+        type: Object,
+        default: () => {}
+      },
+      currentWeekAppointList: {
+        type: Array,
+        default: () => [],
+        required: true
+      },
+      // 设备工作时间
+      instrBusinessTime: {
+        type: String,
+        default: () => '',
+        required: true
+      },
+      // 间隔
+      intervalTime: {
+        type: Number,
+        default: () => 30,
+        required: true
+      },
+      // 是否显示取消按钮
+      showCancel: {
+        type: Boolean,
+        default: () => true,
+        required: true
+      },
+      currentSelectAppointEndDate: {
+        type: String,
+        default: () => '',
+        required: true
+      },
+      appointId: {
+        type: Number,
+        default: () => 0
+      }
+    },
+    data() {
+      return {
+        endDateRange: [], //picker结束日期范围
+        //下次可预约的日期时间范围
+        selectedStartDate: '', //选择的开始时间
+        selectedEndDate: '', //结束时间
+        endDateColumns: [],
+        startDateColumns: [],
+        endStartDate: '', //结束的开始时间
+        endEndDate: '' //结束的结束时间
+      }
+    },
+    mounted() {
+      this.selectedStartDate = moment(this.currentSelectAppointEndDate).format('YYYY/MM/DD HH:mm:ss')
+      this.initEndRange()
+    },
+    methods: {
+      isSubmitted() {
+        this.$emit('backInstAppoint')
+      },
+
+      // 初始化结束选项
+      initEndRange() {
+        const [startTime, endTime] = this.instrBusinessTime.split('-')
+
+        // 开始时间
+        const targetDate = new Date(this.currentSelectAppointEndDate.replace(/\-/g, '/'))
+
+        // 当前周的数据
+        const date = this.currentWeekAppointList
+
+        let closestDate = null
+
+        //找出最接近开始时间的start
+        for (const d of date) {
+          const startDate = new Date(d.start.replace(/\-/g, '/'))
+          if (startDate > targetDate && (!closestDate || startDate < new Date(closestDate.start.replace(/\-/g, '/')))) {
+            closestDate = d
+          }
+        }
+        //获取结束时间的范围 结束的开始时间要加上设置的间隔时间
+        const curTime = new Date(this.currentSelectAppointEndDate.replace(/\-/g, '/'))
+        this.endStartDate = moment(new Date(curTime.setMinutes(curTime.getMinutes() + this.intervalTime))).format('YYYY/MM/DD HH:mm:ss')
+
+        // 本周日的结束时间
+        let sunday = moment(targetDate).isoWeekday(7).format('YYYY/MM/DD')
+
+        sunday = sunday + ' ' + endTime
+
+        sunday = moment(sunday).add(this.intervalTime, 'minutes').format('YYYY/MM/DD HH:mm:ss')
+
+        // 结束时间 如果没找到最近的结束时间,说明可预约到本周最后的一天
+        if (this.furtherLimit && !closestDate) {
+          // this.endEndDate = closestDate ? closestDate.start : sunday;
+          this.endEndDate = this.furtherLimit
+        } else {
+          this.endEndDate = closestDate ? closestDate.start : sunday
+        }
+        // // 获取范围内全部日期
+        this.endDateRange = this.getDatesInRange(this.endStartDate, this.endEndDate)
+
+        let intervalTimeRange = this.splitTimeInterval(this.endStartDate.split(' ')[1], this.endDateRange.length > 1 ? endTime : this.endEndDate.split(' ')[1])
+        if (this.furtherLimit && this.furtherLimit.split(' ')[0] == moment(this.endDateRange[0]).format('YYYY/MM/DD')) {
+          const limtTime = moment(this.furtherLimit).format('HH:mm:ss')
+          intervalTimeRange = intervalTimeRange.filter((item) => {
+            return new Date('1900/01/01 ' + item).getTime() <= new Date('1900/01/01 ' + limtTime).getTime()
+          })
+        }
+        // 初始化的结束picker数据
+        this.endDateColumns = [[...this.endDateRange], [...intervalTimeRange]]
+      },
+
+      // 找出指定范围内的日期
+      getDatesInRange(start, end) {
+        const startDate = moment(start, 'YYYY/MM/DD')
+        const endDate = moment(end, 'YYYY/MM/DD')
+        let dates = []
+        let cursor = moment(start, 'YYYY/MM/DD')
+        while (cursor <= endDate) {
+          dates.push(cursor.format('YYYY/MM/DD'))
+          cursor = moment(cursor).add(1, 'days')
+        }
+        if (this.furtherLimit) {
+          dates = dates.filter((item) => {
+            return new Date(item).getTime() <= new Date(this.furtherLimit.split(' ')[0]).getTime()
+          })
+        }
+        return dates
+      },
+
+      confirmEndColumn(e) {
+        const [dateIndex, timeIndex] = e.detail.value
+        const [date, time] = [this.endDateColumns[0][dateIndex], this.endDateColumns[1][timeIndex]]
+        this.selectedEndDate = `${date} ${time}`
+      },
+
+      // 切换结束picker的左槽
+      changeEndDateColumn(event) {
+        const { column, value } = event.detail
+        const [startTime, endTime] = this.instrBusinessTime.split('-')
+        let timeRange = []
+
+        if (column != 0) return
+        if (value == 0) {
+          timeRange = this.splitTimeInterval(this.endStartDate.split(' ')[1], endTime)
+        } else if (value == this.endDateRange.length - 1) {
+          timeRange = this.splitTimeInterval(startTime, this.endEndDate.split(' ')[1])
+        } else {
+          timeRange = this.splitTimeInterval(startTime, endTime)
+        }
+        this.endDateColumns = [this.endDateRange, [...timeRange]]
+      },
+
+      // 按照规定时间段分割
+      splitTimeInterval(start, end) {
+        if (new Date(`1900/01/01 ${start}`) > new Date(`1900/01/01 ${end}`)) {
+          return [end]
+        }
+        const interval = this.intervalTime || 30
+        const times = []
+        let current = new Date('2020/01/01 ' + start)
+        while (current <= new Date('2020/01/01 ' + end)) {
+          times.push(moment(current).format('HH:mm:ss'))
+          current = new Date(current.getTime() + interval * 60000)
+        }
+        return times
+      },
+
+      cancel() {
+        this.$emit('closePage')
+      },
+
+      /** 提交按钮 */
+      async nextSubmit() {
+        console.log(this.instrInfo)
+        if (!this.selectedStartDate) {
+          this.$refs.uNotify.show({
+            type: 'warning',
+            message: '请选择预约起始时间',
+            duration: 1000 * 3
+          })
+          return false
+        }
+        if (!this.selectedEndDate) {
+          this.$refs.uNotify.show({
+            type: 'warning',
+            message: '请选择预约终止时间',
+            duration: 1000 * 3
+          })
+          return false
+        }
+        if (this.selectedEndDate == this.selectedStartDate) {
+          this.$refs.uNotify.show({
+            type: 'warning',
+            message: '开始时间不能等于终止时间',
+            duration: 1000 * 3
+          })
+          return false
+        }
+        // 从延长上机时间来的直接跳转
+        if (this.appointId != 0) {
+          this.$refs.subAppointRef.open(this.instrInfo, this.selectedStartDate, this.selectedEndDate, this.appointId)
+        } else {
+          // 检查是否可以提示延期
+          const params = {
+            startTime: this.selectedStartDate,
+            instId: this.instrInfo.id * 1
+          }
+          const [err, res] = await to(instAppointApi.getoffDelayAlert(params))
+          if (err) return
+          if (res.code == 200 && res.data) {
+            wx.showModal({
+              title: '提示',
+              content: '当前设备存在正在上机预约,是否使用延期功能?',
+              success: (sm) => {
+                if (sm.confirm) {
+                  this.cancel()
+                  uni.navigateTo({
+                    url: `/pages/appointList/index?type=inProgress`
+                  })
+                } else if (sm.cancel) {
+                  this.$refs.subAppointRef.open(this.instrInfo, this.selectedStartDate, this.selectedEndDate, this.appointId)
+                }
+              }
+            })
+          } else {
+            this.$refs.subAppointRef.open(this.instrInfo, this.selectedStartDate, this.selectedEndDate, this.appointId)
+          }
+        }
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .time-select-container {
+    width: 100%;
+    height: 100vh;
+    position: fixed;
+    left: 0;
+    top: 0;
+    background: rgba(0, 0, 0, 0.6);
+    z-index: 10;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    .time-box {
+      width: 320px;
+      height: 225px;
+      background: #fff;
+      border-radius: 5px;
+      display: flex;
+      flex-direction: column;
+      .content {
+        flex: 1;
+        .at-list__item-extra {
+          min-width: 200px;
+        }
+        .tit {
+          padding: 20px 0 10px 0;
+          text-align: center;
+          border-bottom: 1px solid #e4e4e4;
+        }
+      }
+      .footer {
+        display: flex;
+        justify-content: space-around;
+        padding: 10px;
+        align-items: center;
+        .btn {
+          width: 100px;
+          height: 35px;
+          border-radius: 10px;
+          text-align: center;
+          line-height: 35px;
+        }
+        .cancel {
+          border: 1px solid rgba(78, 110, 242, 1);
+          color: rgba(78, 110, 242, 1);
+        }
+        .sub {
+          background: rgba(78, 110, 242, 1);
+          color: #fff;
+        }
+      }
+    }
+  }
+</style>

+ 536 - 0
src/view/instr/appoint/components/subAppoint.vue

@@ -0,0 +1,536 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-12 11:57:48
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-09-21 18:26:35
+ * @Description: file content
+ * @FilePath: \labsop小程序\pages\message\appoint\components\subAppoint.vue
+-->
+<template>
+  <div>
+    <van-popup v-model:show="popupShow" round :closeable="true">
+      <div class="pupop-wrap">
+        <span style="margin-bottom: 10px" class="primary-color fontSize16 bold">提交预约</span>
+        <div class="main">
+          <van-form ref="addForm" label-width="0">
+            <van-cell-group inset>
+              <van-field
+                v-model="addForm.startTime"
+                is-link
+                readonly
+                label="所在时间"
+                placeholder="所在时间"
+                :rules="[{ required: true, message: '开始时间不能为空' }]"
+              />
+              <van-field
+                v-model="addForm.endTime"
+                is-link
+                readonly
+                label="结束时间"
+                placeholder="结束时间"
+                :rules="[{ required: true, message: '结束时间不能为空' }]"
+              />
+            </van-cell-group>
+            <div v-if="appointId == 0">
+              <van-cell-group inset style="padding: 40rpx 0 30rpx" v-if="isActiveService">
+                <van-field name="radio" label="课题/服务">
+                  <template #input>
+                    <van-radio-group v-model="addForm.projectType" direction="horizontal" @change="changeProjectType">
+                      <van-radio style="margin-right: 20px" name="project">课题</van-radio>
+                      <van-radio name="service">服务</van-radio>
+                    </van-radio-group>
+                  </template>
+                </van-field>
+              </van-cell-group>
+              <van-cell-group inset style="padding: 40rpx 0 30rpx" v-if="addForm.projectType == 'project'">
+                <van-field label="课题" @click="showProject = true" v-model="addForm.projectName"> </van-field>
+              </van-cell-group>
+              <van-cell-group inset style="padding: 40rpx 0 30rpx" v-if="addForm.projectType == 'service'">
+                <van-field label="服务" @click="shwoService = true" v-model="addForm.serviceName"> </van-field>
+              </van-cell-group>
+              <!-- <van-cell-group
+                inset
+                v-if="addForm.projectType == 'project'"
+                prop="expenseCardId"
+                borderBottom
+                style="padding: 40rpx 0 30rpx"
+                @click="showExpenseCard = true"
+              >
+                <div class="form-label flex_l">
+                  <div class="label-tag"></div>
+                  经费卡
+                </div>
+                <u-input
+                  :readonly="true"
+                  placeholder="请选择经费卡"
+                  v-model="addForm.expenseCardName"
+                  border="none"
+                  suffixIcon="arrow-down"
+                  suffixIconStyle="color:#CDCDCD"
+                  clearable
+                  style="padding: 0 30rpx 0 12rpx"
+                ></u-input>
+              </van-cell-group> -->
+            </div>
+            <!-- <van-cell-group inset prop="expenseCardId" borderBottom style="padding: 40rpx 0 30rpx" @click="openSelectUser">
+              <div class="form-label flex_l">预约人</div>
+              <u-input
+                :readonly="true"
+                placeholder="请选择预约人"
+                v-model="addForm.nickName"
+                border="none"
+                suffixIcon="arrow-down"
+                suffixIconStyle="color:#CDCDCD"
+                clearable
+                style="padding: 0 30rpx 0 12rpx"
+              ></u-input>
+            </van-cell-group>
+            <van-cell-group inset prop="userContact" borderBottom style="padding: 40rpx 0 30rpx">
+              <div class="form-label flex_l">联系电话</div>
+              <u-input
+                placeholder="输入联系电话"
+                v-model="addForm.userContact"
+                :disabled="appointId != 0"
+                border="none"
+                clearable
+                style="padding: 0 30rpx 0 12rpx"
+              ></u-input>
+            </van-cell-group> -->
+            <!-- <div v-if="appointId == 0">
+              <van-cell-group inset prop="assistEnable" borderBottom style="padding: 40rpx 0 30rpx">
+                <div class="form-label flex_l">辅助上机</div>
+                <u-radio-group v-model="addForm.assistEnable" placement="row">
+                  <u-radio style="margin-right: 20px" label="是" :name="true" :value="true"></u-radio>
+                  <u-radio label="否" :name="false" :value="false"></u-radio>
+                </u-radio-group>
+              </van-cell-group>
+              <van-cell-group inset prop="remark" borderBottom style="padding: 40rpx 0 30rpx">
+                <div class="form-label flex_l">备注</div>
+                <spanarea placeholder="请输入备注" v-model="addForm.remark" clearable style="padding: 0 30rpx 0 12rpx"></spanarea>
+              </van-cell-group>
+            </div> -->
+          </van-form>
+          <!-- 自定义表单 -->
+          <CustomForm :formData="addForm.createForm" ref="customFormRef"></CustomForm>
+          <!-- END -->
+        </div>
+        <div class="save" v-if="appointId != 0" @click="handleDelayAdd" :class="!flag ? 'disabledBtn' : ''">提 交</div>
+        <div class="save" v-else @click="handleAdd" :class="!flag ? 'disabledBtn' : ''">提 交</div>
+        <!-- 选择服务 -->
+        <van-popup v-model:show="shwoService" position="bottom">
+          <van-picker :columns="serviceList" :columns-field-names="{ text: 'name', value: 'id' }" @confirm="pickService" @cancel="shwoService = false" />
+        </van-popup>
+        <!-- 选择课题 -->
+        <van-popup v-model:show="showProject" position="bottom">
+          <van-picker
+            :columns="projectList"
+            :columns-field-names="{ text: 'projectName', value: 'projectId' }"
+            @confirm="pickProject"
+            @cancel="showProject = false"
+          />
+        </van-popup>
+        <!-- 选择经费卡 -->
+        <u-picker :show="showExpenseCard" :columns="fundsList" keyName="finAccount" @cancel="showExpenseCard = false" @confirm="pickExpenseCard"></u-picker>
+        <!-- 选择预约人 -->
+        <u-picker :show="showAppointUser" :columns="userList" keyName="nickName" @cancel="showAppointUser = false" @confirm="pickAppointUser"></u-picker>
+        <u-notify ref="uNotify"></u-notify>
+        <u-toast ref="uToast"></u-toast>
+      </div>
+    </van-popup>
+  </div>
+</template>
+<script>
+  import { useProApi } from '/@/api/project/index'
+  import { useInstrApi } from '/@/api/instr'
+  import technicalApi from '/@/api/technical/index'
+  import { useUserApi } from '/@/api/system/user'
+  import instAppoint from '/@/api/instr/instAppoint'
+  import CustomForm from '/@/components/CustomForm'
+
+  const projApi = useProApi()
+  const instApi = useInstrApi()
+  const systemApi = useUserApi()
+  export default {
+    components: { CustomForm },
+    data() {
+      return {
+        appointId: 0,
+        isInstrHead: false,
+        flag: true,
+        showExpenseCard: false, //经费卡
+        fundsList: [], //经费卡列表
+        shwoService: false, //服务
+        serviceList: [], //列表
+        showProject: false, //课题
+        projectList: [], //列表
+        showAppointUser: false, //选择预约人
+        userList: [], //用户列表
+        InstCfgCharge: false,
+        isActiveService: false,
+        addForm: {
+          instId: 0,
+          startTime: '',
+          endTime: null,
+          projectName: null,
+          projectId: null,
+          serviceId: null,
+          serviceName: null,
+          expenseCardId: 0,
+          expenseCardName: '',
+          userContact: '',
+          userId: 0,
+          nickName: '',
+          projectType: '',
+          assistEnable: false,
+          createForm: [],
+          remark: ''
+        },
+        rules: {
+          startTime: {
+            type: 'string',
+            required: true,
+            message: '请选择开始时间',
+            trigger: ['change']
+          },
+          endTime: [
+            {
+              type: 'string',
+              required: true,
+              message: '请选择结束时间',
+              trigger: ['change']
+            } // 正则判断为字母或数字
+          ],
+          projectType: {
+            type: 'string',
+            required: true,
+            message: '请选择课题或服务',
+            trigger: ['change']
+          }
+          // serviceId: {
+          //   type: 'string',
+          //   required: true,
+          //   message: '请选择服务',
+          //   trigger: ['change', 'blur'],
+          // },
+          // projectId: {
+          //   type: 'string',
+          //   required: true,
+          //   message: '请选择课题',
+          //   trigger: ['change', 'blur'],
+          // },
+        },
+        popupShow: false,
+        instDetail: {}
+      }
+    },
+    methods: {
+      open(row, startTime, endTime, appointId) {
+        this.popupShow = true
+        //延长预约会传一个预约id 有预约id 获取预约详情
+        const activateService = JSON.parse(uni.getStorageSync('instr_is_activate_service') || '{}')
+        this.isActiveService = activateService == '10' ? true : false
+        this.addForm.instId = row.id * 1
+        this.addForm.startTime = startTime
+        this.addForm.endTime = endTime
+        this.addForm.projectType = 'project'
+        this.addForm.expenseCardId = null
+        this.addForm.expenseCardName = ''
+        const userInfo = uni.getStorageSync('userInfos')
+        this.addForm.userContact = userInfo.phone || ''
+        this.addForm.userId = userInfo.id || 0
+        this.addForm.nickName = userInfo.nickName || ''
+        if (appointId) {
+          this.appointId = appointId
+          this.getAppointDetails(row.id * 1)
+          return
+        }
+        this.getChargeConfig()
+        this.getAppointConfig()
+        this.getMyProjectInfo()
+        this.getUserService()
+        this.getUserList()
+        this.getInstrDetails()
+      },
+      async getAppointDetails(instId) {
+        const [err, res] = await to(instApi.getDetail({ id: instId }))
+        if (err) return
+        if (res.code == 200) {
+        }
+      },
+      // 预约配置信息
+      async getAppointConfig() {
+        const params = {
+          instId: this.addForm.instId,
+          code: 'InstCfgAppoint'
+        }
+        const [err, res] = await to(instApi.getSettingDetail({ ...params }))
+        if (err) return
+        this.addForm.createForm = res?.data?.config.createForm ? JSON.parse(res.data.config.createForm) : []
+        console.log(this.addForm.createForm)
+      },
+      async getInstrDetails() {
+        const [err, res] = await to(instApi.getDetail({ id: this.addForm.instId }))
+        if (err) return
+        if (res?.code === 200) {
+          this.instDetail = res.data
+          const userInfo = uni.getStorageSync('userInfos')
+          this.isInstrHead = userInfo.id ? res.data.instHeadId.split(',').includes('' + userInfo?.id) : false
+        }
+      },
+      async getChargeConfig() {
+        const params = {
+          instId: this.addForm.instId,
+          code: 'InstCfgCharge'
+        }
+        const [err, res] = await to(instApi.getSettingDetail({ ...params }))
+        if (err) return
+        this.InstCfgCharge = res?.data.config.enable
+      },
+      // 获取用户相关的课题组
+      async getMyProjectInfo(id) {
+        let params = {}
+        if (id) {
+          params = { id }
+        } else {
+          params = {}
+        }
+        const [err, res] = await to(projApi.getMySelfProjectGroup(params))
+        if (err) return
+        // this.addForm.projectName = res?.data.pgName || ''
+        // this.addForm.projectId = res?.data.id || null
+        this.projectList = [{ projectName: res?.data.pgName || '', projectId: res?.data.id || null }]
+        if (this.addForm.projectType == 'project') {
+          this.addForm.projectId = res?.data.id || null
+          this.addForm.projectName = res?.data.pgName || ''
+          this.getFundsData()
+        }
+      },
+      // 获取用户下的服务
+      async getUserService() {
+        const [err, res] = await to(technicalApi.getList({ noPage: true }))
+        if (err) return
+        this.serviceList = [res?.data.list]
+        if (this.addForm.projectType == 'service') {
+          this.addForm.serviceId = res?.data.list[0].id || 0
+          this.addForm.serviceName = res?.data.list[0].name || ''
+        }
+      },
+      // 获取用户列表
+      async getUserList() {
+        const [err, res] = await to(systemApi.getUserList({ noPage: true }))
+        if (err) return
+        this.userList = [res?.data.list]
+      },
+      // 选择课题还是服务
+      changeProjectType() {
+        this.addForm.serviceId = 0
+        this.addForm.serviceName = ''
+        this.addForm.projectId = 0
+        this.addForm.projectName = ''
+        this.addForm.expenseCardId = 0
+        this.addForm.expenseCardName = ''
+        this.fundsList = []
+      },
+      async getFundsData() {
+        const [err, res] = await to(projApi.getFinanceAccountList({ projId: this.addForm.projectId }))
+        if (err) return
+        this.fundsList = res?.data.list ? [res?.data.list] : []
+        if (this.fundsList && this.fundsList.length > 0 && this.fundsList[0].length > 0) {
+          this.addForm.expenseCardId = this.fundsList[0][0].id
+          this.addForm.expenseCardName = this.fundsList[0][0].finAccount
+        }
+      },
+      // 经费卡选择
+      pickExpenseCard(e) {
+        this.addForm.expenseCardId = e.value[0].id
+        this.addForm.expenseCardName = e.value[0].finAccount
+        this.showExpenseCard = false
+      },
+      // 选择预约人
+      openSelectUser() {
+        if (!this.isInstrHead) return
+        this.showAppointUser = true
+      },
+      // 预约人选择
+      pickAppointUser(e) {
+        this.addForm.nickName = e.value[0].nickName
+        this.addForm.userId = e.value[0].id
+        this.addForm.userContact = e.value[0].phone
+        this.showAppointUser = false
+        this.addForm.serviceId = 0
+        this.addForm.serviceName = ''
+        this.addForm.projectId = 0
+        this.addForm.projectName = ''
+        this.addForm.expenseCardId = 0
+        this.addForm.expenseCardName = ''
+        this.projectType = ''
+        this.fundsList = []
+        this.getMyProjectInfo(this.addForm.userId)
+      },
+      // 选择服务
+      pickService(selectedOptions) {
+        this.addForm.serviceId = selectedOptions[0].id
+        this.addForm.serviceName = selectedOptions[0].name
+        this.shwoService = false
+      },
+      // 选择课题
+      pickProject(selectedOptions) {
+        this.addForm.projectId = selectedOptions[0].projectId
+        this.addForm.projectName = selectedOptions[0].projectName
+        this.showProject = false
+        this.getFundsData()
+      },
+      async handleDelayAdd() {
+        if (!this.flag) return
+        this.flag = false
+        const params = {
+          id: this.appointId,
+          endTime: this.addForm.endTime
+        }
+        const [err, res] = await to(instAppoint.delayAdd(params))
+        this.flag = true
+        if (err) return
+        if (res && res.code == 200) {
+          this.$refs.uToast.show({
+            type: 'success',
+            message: '预约延迟成功',
+            complete: () => {
+              uni.navigateBack({
+                url: `/pages/appointList/index?type=inProgress`
+              })
+            }
+          })
+        }
+      },
+      handleAdd() {
+        if (!this.flag) return
+        console.log(this.addForm)
+        this.$refs.addForm
+          .validate()
+          .then(async () => {
+            if (this.addForm.projectType == 'project' && !this.addForm.projectId) {
+              return this.$refs.uNotify.show({
+                type: 'warning',
+                message: '请选择课题',
+                duration: 1000 * 3
+              })
+            }
+            if (this.addForm.projectType == 'service' && !this.addForm.serviceId) {
+              return this.$refs.uNotify.show({
+                type: 'warning',
+                message: '请选择服务',
+                duration: 1000 * 3
+              })
+            }
+            if (this.addForm.projectType == 'project' && this.InstCfgCharge && !this.addForm.expenseCardId) {
+              this.$refs.uNotify.show({
+                type: 'warning',
+                message: '请选择经费卡',
+                duration: 1000 * 3
+              })
+              return
+            }
+            const params = Object.assign({ ...this.addForm })
+            // 获取自定义表单数据
+            const formData = this.$refs.customFormRef.getFormData()
+            if (this.addForm.createForm.length > 0 && !formData) {
+              return
+            } else {
+              params.createForm = JSON.stringify(formData)
+            }
+            params.userName = params.nickName
+            this.flag = false
+            const [err, res] = await to(instAppoint.add(params))
+            this.flag = true
+            if (err) return
+            if (res && res.code == 200) {
+              this.$refs.uToast.show({
+                type: 'success',
+                message: '创建成功',
+                complete: () => {
+                  if (this.instDetail.appointJump && this.instDetail.appointJumpLink) {
+                    uni.showModal({
+                      title: '提示',
+                      content:
+                        '除了在当前系统预约,此仪器也必须在其他仪器共享平台进行预约,仪器在其他仪器共享平台编号为:' +
+                        this.instDetail.assetNumber +
+                        ',平台链接为' +
+                        this.instDetail.appointJumpLink,
+                      success(res) {
+                        if (res.confirm) {
+                          this.$emit('isSubmitted')
+                          console.log('用户点击确定')
+                        } else if (res.cancel) {
+                          console.log('用户点击取消')
+                        }
+                      }
+                    })
+                  } else {
+                    this.$emit('isSubmitted')
+                  }
+                }
+              })
+            }
+          })
+          .catch((err) => {
+            this.$refs.uNotify.show({
+              type: 'warning',
+              message: err[0].message,
+              duration: 1000 * 3
+            })
+          })
+      }
+    }
+  }
+</script>
+<style>
+  page {
+    background: #f2f3f5;
+  }
+</style>
+<style lang="scss" scoped>
+  .pupop-wrap {
+    width: 90vw;
+    height: 90vh;
+    background: #fff;
+    border-radius: 10px;
+    padding: 15px;
+  }
+  .main {
+    width: 100%;
+    height: calc(100% - 88px);
+    background: #ffffff;
+    box-shadow: 0 6rpx 19rpx 2rpx rgba(0, 45, 132, 0.15);
+    border-radius: 31rpx 31rpx 0 0;
+    padding: 0 32rpx;
+    overflow: auto;
+    padding-bottom: 64rpx;
+
+    .form-label {
+      font-size: 32rpx;
+      font-weight: bold;
+      color: #323232;
+      padding-bottom: 18rpx;
+
+      .label-tag {
+        width: 15rpx;
+        height: 15rpx;
+        background: #ff4d4f;
+        border-radius: 50%;
+        margin-right: 10rpx;
+      }
+    }
+  }
+
+  .save {
+    width: 569rpx;
+    height: 92rpx;
+    background: #3e7ef8;
+    border-radius: 31rpx;
+    margin: 10px auto 0;
+    font-size: 32rpx;
+    color: #ffffff;
+    text-align: center;
+    line-height: 92rpx;
+  }
+</style>

+ 483 - 0
src/view/instr/appoint/components/timeInfo.vue

@@ -0,0 +1,483 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-04-18 15:16:21
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-09-21 17:20:25
+ * @Description: file content
+ * @FilePath: \labsop小程序\pages\message\components\TimeInfo.vue
+-->
+<template>
+  <div class="time-info-container">
+    <!-- <div class="time-split"></div> -->
+    <div v-for="(v, i) in timeList" v-show="isInBusinessRange(instrBusinessTime, v.time)" :key="i" :class="['time-item', v.time]" style="height: 32px">
+      <div class="left-time">
+        <div v-if="v.isWholeHour">
+          {{ v.time }}
+        </div>
+      </div>
+      <div
+        class="right-appoint"
+        :class="v.time"
+        :style="{
+          borderTop: v.isWholeHour ? '1px solid #F2F2F2' : '',
+          background:
+            curTimeLineDate(currentDate, v.time) < new Date() || (furtherLimit && new Date(`${currentDate} ${v.time}:59`) > new Date(furtherLimit))
+              ? '#ececec'
+              : ''
+        }"
+        @click="handleClickTime(v)"
+      >
+        <div
+          v-if="showEvent(v).findAppoint"
+          class="event-block"
+          :style="{
+            height: showEvent(v).cellCount * 32 + 'px'
+          }"
+        >
+          <div
+            class="event-inner"
+            :class="showEvent(v).findAppoint.appointStatus ? 'appointTimeEvent' : 'disabledTimeEvent'"
+            :style="{
+              lineHeight: '40px',
+              background: showEvent(v).findAppoint.appointStatus ? ' #e8effc' : '#ccc'
+            }"
+            @click="openAppointDetails(showEvent(v).findAppoint)"
+          >
+            {{ showEvent(v).findAppoint.range }}
+            {{ showEvent(v).findAppoint.appointStatus ? showEvent(v).findAppoint.userName : '不可预约时段' }}
+            {{ showEvent(v).findAppoint.userContact || '' }}
+          </div>
+          <div
+            v-if="showEvent(v).showContinueAppoint"
+            class="select-btn"
+            :style="{
+              height: '26px',
+              bottom: '-29px',
+              lineHeight: '26px'
+            }"
+            @click="handleSelectTime(v)"
+          >
+            选择时间
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- 预约信息 -->
+    <div class="header-info">
+      <div>正在发起对{{ instrInfo.name }}的预约</div>
+    </div>
+    <select-time
+      v-if="showSelect"
+      :show-cancel="showCancel"
+      :appoint-id="appointId"
+      :instr-info="instrInfo"
+      :interval-time="intervalTime"
+      :current-week-appoint-list="currentWeekAppointList"
+      :instr-business-time="instrBusinessTime"
+      :further-limit="furtherLimit"
+      :current-select-appoint-end-date="currentSelectPointStartDate"
+      @closePage="closePage"
+      @backInstAppoint="backInstList()"
+    />
+    <u-modal :show="showAppointFlag" title="预约详情" :content="curAppointDetails" @confirm="showAppointFlag = false">
+      <div class="slot-content">
+        <div class="mb10">
+          <span class="mr20">预约时间段:</span>
+          {{ curAppointDetails.range || '' }}
+        </div>
+        <div class="mb10" v-if="curAppointDetails.projectName">
+          <span class="mr20">课题组:</span>
+          {{ curAppointDetails.projectName || '' }}
+        </div>
+        <div class="mb10" v-if="curAppointDetails.serviceName">
+          <span class="mr20">服务:</span>
+          {{ curAppointDetails.serviceName || '' }}
+        </div>
+
+        <div class="mb10">
+          <span class="mr20">预约人:</span>
+          {{ curAppointDetails.userName || '' }}
+        </div>
+        <div class="mb10">
+          <span class="mr20">联系电话:</span>
+          {{ curAppointDetails.userContact || '' }}
+        </div>
+      </div>
+    </u-modal>
+  </div>
+</template>
+
+<script>
+  import SelectTime from '/@/view/instr/appoint/components/selectTime.vue'
+  export default {
+    name: 'FrontendMobileTimeInfo',
+    components: {
+      SelectTime
+    },
+    props: {
+      // 提前预约时间
+      furtherLimit: {
+        type: String,
+        default: () => null,
+        required: true
+      },
+      // 时间轴
+      timeList: {
+        type: Array,
+        default: () => [],
+        required: true
+      },
+      // 间隔
+      intervalTime: {
+        type: Number,
+        default: () => 0,
+        required: true
+      },
+      // 设备工作时间
+      instrBusinessTime: {
+        type: String,
+        default: () => '',
+        required: true
+      },
+      // 当前天涉及的预约时间段
+      currentDayAppointList: {
+        type: Array,
+        default: () => [],
+        required: true
+      },
+      currentWeekAppointList: {
+        type: Array,
+        default: () => [],
+        required: true
+      },
+      // 当前日期
+      currentDate: {
+        type: String,
+        default: () => ''
+      },
+      instrInfo: {
+        type: Object,
+        default: () => {}
+      }
+    },
+    data() {
+      return {
+        appointId: 0, //预约id
+        showAppointFlag: false, //预约详情
+        currentSelectPointStartDate: '',
+        showSelect: false, //选择日期组件
+        // 处理之后的当前时间列表(只保留了当天的预约没有其他天的数据)
+        handleCurDayAppointList: [],
+        curAppointDetails: {},
+        showCancel: true
+      }
+    },
+    computed: {
+      instrBusinessStart() {
+        return this.instrBusinessTime.split('-')[0]
+      },
+      instrBusinessEnd() {
+        return this.instrBusinessTime.split('-')[1]
+      }
+    },
+    watch: {
+      currentDayAppointList(list) {
+        this.handleCurDayAppointList = []
+        list.map((item) => {
+          // 跨天的数据处理
+          if (this.isCrossDay(item.start, item.end)) {
+            let date = this.splitDateRange(item)
+            if (!date) return
+            this.handleCurDayAppointList.push(date)
+          } else {
+            // 不跨天的
+            const date = {
+              start: item.start,
+              end: item.end,
+              projectName: item.projectName,
+              appointStatus: item.appointStatus,
+              userContact: item.userContact,
+              userName: item.userName,
+              userDeptName: item.userDeptName,
+              serviceName: item.serviceName || '',
+              range: `${item.start.substr(11, 5)}-${item.end.substr(11, 5)}`
+            }
+            this.handleCurDayAppointList.push({ ...date })
+          }
+        })
+      }
+    },
+
+    mounted() {},
+    methods: {
+      openAppointDetails(item) {
+        if (item.appointStatus) {
+          this.showAppointFlag = true
+          this.curAppointDetails = item
+        }
+      },
+      // 格式化时间线对应的时间
+      curTimeLineDate(currentDate, time) {
+        const date = new Date(currentDate + ' ' + time)
+        return date
+      },
+      backInstList() {
+        this.showSelect = false
+        this.$emit('submitFresh')
+      },
+      // 选择时间
+      handleSelectTime(timeLineRow) {
+        const end = this.showEvent(timeLineRow).findAppoint.end
+        this.currentSelectPointStartDate = end
+        this.showSelect = true
+      },
+      // 固定预约开始时间、选择结束时间
+      handleAutoSelectTime(appointId, startTime) {
+        setTimeout(() => {
+          this.currentSelectPointStartDate = startTime
+          this.appointId = appointId * 1
+          this.showSelect = true
+          this.showCancel = false
+        })
+      },
+      // 点击任意时间节点判断是否可以选择时间
+      handleClickTime(timeLineRow) {
+        const show = this.showEvent(timeLineRow).findAppoint
+        if (show) return
+        if (
+          new Date(`${this.currentDate.replace(/\-/g, '/')} ${timeLineRow.time}:59`) < new Date() ||
+          (this.furtherLimit && new Date(`${this.currentDate.replace(/\-/g, '/')} ${timeLineRow.time}:59`) > new Date(this.furtherLimit))
+        ) {
+          return
+        }
+        this.currentSelectPointStartDate = `${this.currentDate} ${timeLineRow.time}`
+        this.showSelect = true
+      },
+      // 是否跨天
+      isCrossDay(start, end) {
+        const startDate = new Date(start.replace(/\-/g, '/'))
+        const endDate = new Date(end.replace(/\-/g, '/'))
+        const startDay = startDate.getDate()
+        const endDay = endDate.getDate()
+        return startDay !== endDay
+      },
+      // 跨天的数据分割出当前天的
+      splitDateRange(item) {
+        // 当前设备的开始工作和结束工作时间
+        const startBusinessTime = this.instrBusinessTime.split('-')[0]
+        let result = {}
+        if (item.start.split(' ')[0] == this.currentDate) {
+          result = {
+            start: item.start,
+            end: `${this.currentDate} 23:59:59`,
+            projectName: item.projectName,
+            userContact: item.userContact,
+            appointStatus: item.appointStatus,
+            userName: item.userName,
+            serviceName: item.serviceName || '',
+            userDeptName: item.userDeptName,
+            range: `${item.start.substr(5, 11)}-${item.end.substr(5, 11)}`
+          }
+        } else if (item.end.split(' ')[0] == this.currentDate) {
+          if (new Date(item.end.replace(/\-/g, '/')) < new Date(`${this.currentDate.replace(/\-/g, '/')} ${startBusinessTime}`)) {
+            result = null
+          } else {
+            result = {
+              start: `${this.currentDate} ${startBusinessTime}`,
+              end: item.end,
+              projectName: item.projectName,
+              userContact: item.userContact,
+              appointStatus: item.appointStatus,
+              userName: item.userName,
+              serviceName: item.serviceName || '',
+              userDeptName: item.userDeptName,
+              range: `${item.start.substr(5, 11)}-${item.end.substr(5, 11)}`
+            }
+          }
+        } else {
+          result = {
+            start: `${this.currentDate} ${startBusinessTime}`,
+            end: `${this.currentDate} 23:59:59`,
+            projectName: item.projectName,
+            appointStatus: item.appointStatus,
+            userContact: item.userContact,
+            userName: item.userName,
+            serviceName: item.serviceName || '',
+            userDeptName: item.userDeptName,
+            range: `${item.start.substr(5, 11)}-${item.end.substr(5, 11)}`
+          }
+        }
+        return result
+      },
+      // 显示预约情况
+      showEvent(v) {
+        // 间隔数量
+        let intervalCount = 0
+        let showContinueAppoint = false
+        let findItem = this.handleCurDayAppointList.find((item) => item.start.split(' ')[1] == v.time + ':00' && item.start != item.end)
+        if (findItem) {
+          const startTime = findItem.start // 开始时间
+          const endTime = findItem.end // 结束时间
+          const interval = this.intervalTime // 时间间隔,单位为分钟
+          // 将时间字符串转换为分钟数
+          const [startHour, startMinute] = startTime.split(' ')[1].split(':').map(Number)
+          const [endHour, endMinute] = endTime.split(' ')[1].split(':').map(Number)
+          const startMinutes = startHour * 60 + startMinute
+          const endMinutes = endHour * 60 + endMinute
+
+          // 计算时间间隔个数
+          const diffMinutes = endMinutes - startMinutes
+          intervalCount = Math.ceil(diffMinutes / interval)
+          // 判断是否显示继续预约
+          showContinueAppoint = this.checkContinueAppoint(endTime, this.currentDate + ' ' + this.instrBusinessEnd) && findItem.appointStatus
+        }
+        return {
+          findAppoint: findItem,
+          cellCount: intervalCount || 0,
+          showContinueAppoint
+        }
+      },
+      // 判断是否在工作时间
+      isInBusinessRange(range, time) {
+        let [workStart, workEnd] = range.split('-')
+        let nowTime = new Date('1970/01/01 ' + time)
+        let workStartTime = new Date('1970/01/01 ' + workStart)
+        let workEndTime = new Date('1970/01/01 ' + workEnd)
+        if (nowTime >= workStartTime && nowTime <= workEndTime) {
+          return true
+        } else {
+          return false
+        }
+      },
+      // 判断是否显示继续预约
+      checkContinueAppoint(endTime, instrBusinessEnd) {
+        // 当前时间
+        var currentDate = new Date()
+
+        const findEndDateInRange = this.handleCurDayAppointList.find((item) => {
+          return this.isTimeInRange(endTime.split(' ')[1], item.start.split(' ')[1], item.end.split(' ')[1])
+        })
+
+        // 判断结束时间是否大于当前时间  大于继续预约
+        if (new Date(endTime.replace(/\-/g, '/')).getTime() <= currentDate.getTime() || findEndDateInRange) {
+          return false
+        }
+        // // 判断结束时间是否大于等于当天设备工作的结束时间 大约 就不允许预约
+        if (new Date(endTime.replace(/\-/g, '/')).getTime() > new Date(instrBusinessEnd.replace(/\-/g, '/')).getTime()) {
+          return false
+        }
+        // 判断当前的结束是不是 是不是其他预约的开始时间
+        if (this.handleCurDayAppointList.find((item) => item.start == endTime)) {
+          return false
+        }
+
+        return true
+      },
+      isTimeInRange(time, startTime, endTime) {
+        // 创建 Date 对象
+        const date = new Date(`1970-01-01T${time}Z`)
+        const startDate = new Date(`1970-01-01T${startTime}Z`)
+        const endDate = new Date(`1970-01-01T${endTime}Z`)
+
+        // 获取时间戳
+        const timestamp1 = date.getTime()
+        const timestamp2 = startDate.getTime()
+        const timestamp3 = endDate.getTime()
+
+        // 确定哪个是开始时间,哪个是结束时间
+        const startTime1 = Math.min(timestamp2, timestamp3)
+        const endTime1 = Math.max(timestamp2, timestamp3)
+
+        // 判断 time1 是否在 time2 和 time3 之间
+        return timestamp1 > startTime1 && timestamp1 < endTime1
+      },
+      closePage() {
+        this.showSelect = false
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .time-info-container {
+    background: #ffffff;
+    padding-bottom: 140px;
+    overflow: hidden;
+    .time-split {
+      width: 100%;
+      height: 40px;
+      // border-right: 1px solid #e4e4e4;
+      background: #ffffff;
+    }
+    .header-info {
+      width: 100%;
+      height: 50px;
+      line-height: 50px;
+      position: fixed;
+      bottom: 0;
+      left: 0;
+      background: #fff;
+      display: flex;
+      justify-content: center;
+      box-sizing: border-box;
+      // padding-top: 40px;
+      border-top: 1px solid #e4e4e4;
+      z-index: 10;
+      font-weight: bold;
+    }
+    .time-item {
+      width: 100%;
+      display: flex;
+      .left-time {
+        width: 100px;
+        border-right: 1px solid #e4e4e4;
+        span-align: center;
+        height: 100%;
+        background: #fafafa;
+      }
+      .right-appoint {
+        flex: 1;
+        height: 100%;
+        position: relative;
+        box-sizing: border-box;
+        border-bottom: 1px dashed #e4e4e4;
+        .event-block {
+          // width: calc(100% - 10px);
+          width: 100%;
+          position: absolute;
+          top: 0;
+          left: 0;
+          // z-index: 9;
+          font-size: 24px;
+          padding: 8px;
+          box-sizing: border-box;
+          .event-inner {
+            font-size: 22px;
+            border-radius: 8px;
+            width: calc(100% - 16px);
+            height: calc(100% - 16px);
+            display: flex;
+            align-items: flex-end;
+            justify-content: center;
+          }
+        }
+        .select-btn {
+          position: absolute;
+          width: 50%;
+          background: #3788d8;
+          border-radius: 8px;
+          color: #fff;
+          span-align: center;
+        }
+      }
+    }
+  }
+  .appointTimeEvent {
+    position: absolute;
+    z-index: 10;
+  }
+  .disabledTimeEvent {
+    position: absolute;
+    z-index: 9;
+  }
+</style>

+ 287 - 0
src/view/instr/appoint/index.vue

@@ -0,0 +1,287 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-04-12 11:16:26
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-09-21 16:39:27
+ * @Description: file content
+ * @FilePath: \labsop小程序\pages\message\appoint.vue
+-->
+<template>
+  <div class="appoint-container">
+    <!-- 日期选择 -->
+    <div v-if="weekRange" class="date-container">
+      <div class="date-btn" @tap="goPrevDay(prevDisabled)">
+        <van-button type="primary" size="small" :disabled="prevDisabled" icon="arrow-left"> 前一天 </van-button>
+      </div>
+      <div class="date">
+        {{ currentShowDate }}
+      </div>
+      <div class="date-btn" @tap="goNextDay(nextDisabled)">
+        <van-button type="primary" size="small" :disabled="nextDisabled" icon="arrow-right"> 后一天 </van-button>
+      </div>
+    </div>
+    <!-- 日期时间展示 -->
+    <div class="dateBox calendar-container">
+      <time-info
+        ref="time"
+        :time-list="timeList"
+        :interval-time="intervalTime"
+        :instr-business-time="instrBusinessTime"
+        :current-day-appoint-list="currentDayAppointList"
+        :current-date="paramsDate"
+        :instr-info="instInfo"
+        :further-limit="furtherLimit"
+        :current-week-appoint-list="currentWeekAppointList"
+        @submitFresh="getAppointTimeInfo()"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+  import TimeInfo from '/@/view/instr/appoint/components/timeInfo.vue'
+  import { useInstrApi } from '/@/api/instr'
+  import to from 'await-to-js'
+  import moment from 'moment'
+  const instApi = useInstrApi()
+  export default {
+    components: { TimeInfo },
+    data() {
+      return {
+        currentDate: new Date(),
+        dayNames: ['日', '一', '二', '三', '四', '五', '六'],
+        weekRange: null,
+        intervalTime: 0, //时间间隔
+        instInfo: {}, //设备信息
+        timeList: [], //时间轴数据
+        instrBusinessTime: '', //当前设备的工作时间
+        currentDayAppointList: [], //从接口得到的预约信息
+        currentWeekAppointList: [], //当前周预约信息
+        further_range: 0,
+        furtherLimit: '',
+        begin_at: '00:00:00',
+        ent_at: '23:50:00'
+      }
+    },
+    computed: {
+      // 计算当前日期的字符串格式
+      currentShowDate() {
+        const year = this.currentDate.getFullYear()
+        const month = this.currentDate.getMonth() + 1
+        const day = this.currentDate.getDate()
+        const dayName = this.dayNames[this.currentDate.getDay()]
+        const today = moment().format('YYYY-MM-DD')
+        const today2 = `${year}-${month < 10 ? '0' + month : month}-${day < 10 ? '0' + day : day}`
+        if (today == today2) {
+          return `${year}年${month}月${day}日 周${dayName}(今天)`
+        } else {
+          return `${year}年${month}月${day}日 周${dayName}`
+        }
+      },
+      prevDisabled() {
+        const start = this.weekRange?.start || null
+        return new Date(start) >= new Date(this.paramsDate)
+      },
+      nextDisabled() {
+        const end = this.weekRange?.end || null
+        return new Date(end) <= new Date(this.paramsDate)
+      },
+      // 计算当前日期的传参格式
+      paramsDate() {
+        const year = this.currentDate.getFullYear()
+        const month = this.currentDate.getMonth() + 1
+        const day = this.currentDate.getDate()
+        return `${year}-${month < 10 ? '0' + month : month}-${day < 10 ? '0' + day : day}`
+      }
+    },
+    async mounted() {
+      this.instInfo = { name: decodeURIComponent(this.$route.query.name), id: Number(this.$route.query.id) }
+      await this.getTimeSplit()
+      // this.initTimeLine()
+    },
+    methods: {
+      getAppointTimeInfo() {
+        this.appointTimeInfo()
+      },
+      // 获取系统设置时间间隔
+      async getTimeSplit() {
+        const [err, res] = await to(
+          instApi.getSettingDetail({
+            instId: Number(this.instInfo.id),
+            code: 'InstCfgAppoint'
+          })
+        )
+        if (err) return
+        if (res.code == 200) {
+          this.intervalTime = res.data?.config?.timeSplit
+          this.begin_at = res.data?.config?.timeRange[0].start
+          this.ent_at = res.data?.config?.timeRange[0].end
+          this.initTimeLine(this.intervalTime)
+          this.appointTimeInfo()
+        }
+      },
+      // 生成时间线
+      initTimeLine(interval) {
+        let arr = []
+        for (let i = 0; i < 24 * 60; i += interval) {
+          let hour = Math.floor(i / 60)
+          hour = hour < 10 ? '0' + hour : hour
+          let minute = i % 60
+          minute = minute < 10 ? '0' + minute : minute
+          let time = `${hour}:${minute}`
+          let obj = {
+            time
+          }
+          // 添加标识,如果是整点时间
+          if (minute === '00') {
+            obj.isWholeHour = true
+          }
+          arr.push(obj)
+        }
+        this.timeList = arr
+      },
+      // 获取预约信息
+      async appointTimeInfo() {
+        let params = {
+          instId: this.instInfo.id * 1,
+          date: this.paramsDate,
+          dateType: 'week'
+        }
+        const [err, res] = await to(instApi.getAppointInfo({ ...params }))
+        if (err) return
+        if (res.code == 200) {
+          const { appoint, unavailable, furtherLimit } = res.data
+          let allDate = [...(appoint || []), ...(unavailable || [])].map((item) => ({
+            ...item,
+            start: item.startTime ? item.startTime : item.start,
+            end: item.endTime ? item.endTime : item.end
+          }))
+          let arr = []
+          this.further_range = 99999 //未来可跳转多少周的数据
+          this.weekRange = this.getTwoWeekTime()
+          this.currentWeekAppointList = allDate
+          this.furtherLimit = furtherLimit ? this.nearFurtherLimit(furtherLimit, this.intervalTime) : ''
+          // 把涉及当前天预约的数据获取出来(会有跨天的数据)
+          allDate.forEach((item) => {
+            if (this.isInDateRange(this.paramsDate, item.start.split(' ')[0], item.end.split(' ')[0])) {
+              arr.push(item)
+            }
+          })
+          this.currentDayAppointList = arr
+          // 设备的工作时间
+          this.instrBusinessTime = `${this.begin_at}-${this.ent_at}`
+          // 接口获取完成打开预约时间
+          if (this.instInfo && this.instInfo.appointId && this.$refs.time && this.timeList.length > 0) {
+            this.$refs.time.handleAutoSelectTime(this.instInfo.appointId, this.instInfo.endTime)
+          }
+        }
+      },
+      nearFurtherLimit(time, split) {
+        // 将目标时间转换为Moment对象
+        const targetMoment = moment(time)
+
+        // 计算距离目标时间最近的能够被时间间隔整除的时间
+        const remainder = targetMoment.minute() % split
+        const divisibleTime = targetMoment.clone().subtract(remainder, 'minutes')
+        const nearestAvailableTime = divisibleTime.format('YYYY/MM/DD HH:mm') + ':00'
+        return nearestAvailableTime
+      },
+      // 获取符合日期的预约数据
+      isInDateRange(date, beginDateStr, endDateStr) {
+        var curDate = new Date(date),
+          beginDate = new Date(beginDateStr),
+          endDate = new Date(endDateStr)
+        if (curDate >= beginDate && curDate <= endDate) {
+          return true
+        }
+        return false
+      },
+
+      // 跳转到前一天
+      goPrevDay(disable) {
+        if (disable) return
+        const prevDate = new Date(this.currentDate.getTime() - 24 * 60 * 60 * 1000)
+        this.currentDate = prevDate
+        this.appointTimeInfo()
+      },
+      // 跳转到后一天
+      goNextDay(disable) {
+        if (disable) return
+        const nextDate = new Date(this.currentDate.getTime() + 24 * 60 * 60 * 1000)
+        this.currentDate = nextDate
+        this.appointTimeInfo()
+      },
+      /**
+       * 判断给定日期是否在可切换日期范围内(当前周和下一周)
+       * @param {Date} date 给定日期
+       * @return {boolean} 是否在可切换日期范围内
+       */
+      checkDateInRange(past) {
+        const pastTime = new Date(past).getTime()
+        const today = new Date(new Date().toLocaleDateString())
+        let day = today.getDay()
+        day = day == 0 ? 7 : day
+        const oneDayTime = 60 * 60 * 24 * 1000
+        const monday = new Date(today.getTime() - oneDayTime * (day - 1))
+        const nextMonday = new Date(today.getTime() + oneDayTime * (8 - day))
+        if (monday.getTime() <= pastTime && nextMonday.getTime() > pastTime) {
+          return true
+        } else {
+          return false
+        }
+      },
+      getTwoWeekTime() {
+        // 获取当前日期
+        const today = new Date()
+        // 获取当前周开始日期(周一)
+        let start = moment().isoWeekday(1).format('YYYY-MM-DD') //本周一
+
+        // 计算距离本周日还有多少天
+        const daysUntilSunday = 7 - today.getDay()
+
+        // 计算下一周日的日期
+        const nextSunday = new Date(
+          today.getFullYear(),
+          today.getMonth(),
+          today.getDate() + daysUntilSunday + (this.further_range > 0 ? this.further_range : 9999) * 7
+        )
+        const end = moment(nextSunday).format('YYYY-MM-DD') //本周一
+
+        return {
+          start,
+          end
+        }
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .appoint-container {
+    background: #f7f7f7;
+    height: 100vh;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    .date-container {
+      background: #fff;
+      margin: 0 0 10px 0;
+      display: flex;
+      padding: 10px;
+      .date {
+        flex: 1;
+        text-align: center;
+        font-size: 12px;
+        line-height: 25px;
+      }
+      .date-btn {
+        font-size: 14px;
+        line-height: 25px;
+      }
+    }
+    .calendar-container {
+      flex: 1;
+      overflow: auto;
+    }
+  }
+</style>

+ 245 - 0
src/view/instr/appointList/inProgress/index.vue

@@ -0,0 +1,245 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-12 11:57:48
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-09-22 14:38:38
+ * @Description: file content
+ * @FilePath: \labsop小程序\pages\schedule\myAppoint\index.vue
+-->
+<template>
+  <div class="panel-wrap">
+    <van-empty v-if="appointList.length == 0" mode="list" description="暂无正在上机"></van-empty>
+    <van-list v-else v-model:loading="listloading" class="data-list" :finished="finished" finished-text="没有更多了" @load="onLoad">
+      <div class="inst-item mb40" v-for="(v, index) in appointList" :key="index" @click="toDetail(v)">
+        <div class="flex flex-between mb20">
+          <div>
+            <div class="mr10">
+              <span class="fontSize14 primary-color bold">{{ v.instName }}</span>
+            </div>
+          </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" v-if="v.serviceName">
+          <div class="equ-tit">
+            <span class="fontSize14 bold">服务:</span>
+          </div>
+          <div>
+            <span class="fontSize14">{{ v.serviceName }}</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">
+          <van-button
+            style="width: 70px; height: 30px; margin: 0; font-size: 14px"
+            class="scan-txt"
+            type="danger"
+            size="small"
+            @click.native.stop="handleIsGetOff(v)"
+          >
+            下机
+          </van-button>
+        </div>
+      </div>
+    </van-list>
+    <!-- <blue-tooth ref="bluetoothRef" @getOff="callbackOff"></blue-tooth> -->
+  </div>
+</template>
+
+<script>
+  // import BlueTooth from '../../../components/BlueTooth'
+  import { useMyAppointApi } from '/@/api/appoint'
+  const myAppointApi = useMyAppointApi()
+  import { useInstrApi } from '/@/api/instr'
+  const instApi = useInstrApi()
+  import instAppointApi from '/@/api/instr/instAppoint'
+  import to from 'await-to-js'
+  import { showConfirmDialog, showNotify } from 'vant'
+  export default {
+    // components: { BlueTooth },
+    data() {
+      return {
+        appointList: [],
+        queryForm: {
+          pageNum: 1,
+          pageSize: 10
+        },
+        curAppointInfo: 0,
+        total: 0,
+        listloading: false,
+        finished: false
+      }
+    },
+    created() {},
+    mounted() {
+      this.queryForm.pageNum = 1
+      this.getInstList()
+    },
+    methods: {
+      // 重新加载
+      onLoad() {
+        this.queryForm.pageNum++
+        this.getInstList()
+      },
+      // 查询列表
+      async getInstList() {
+        this.listloading = true
+        const [err, res] = await to(myAppointApi.inProgressList(this.queryForm))
+        this.listloading = false
+        if (err) return
+        if (res?.code === 200) {
+          this.appointList = this.queryForm.pageNum == 1 ? [...(res?.data?.list || [])] : [...this.appointList, ...(res?.data?.list || [])]
+          this.total = res?.data?.total
+          if (this.queryForm.pageNum * this.queryForm.pageSize >= res.data.total) {
+            this.finished = true
+          }
+        }
+      },
+      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.$refs.bluetoothRef.initBlue('close', row)
+            } else {
+              this.curAppointInfo = row
+              showConfirmDialog({
+                title: '提示',
+                message: `本次上机时长共计${useTime}分钟,确定要下机吗?`
+              })
+                .then((e) => {
+                  console.log(e)
+                  this.getOff()
+                })
+                .catch(() => {
+                  console.log('ssss')
+                })
+            }
+          } else {
+            if (row.controlMode === '30') {
+              this.$refs.bluetoothRef.initBlue('close', row)
+            } else {
+              this.curAppointInfo = row
+              showConfirmDialog({
+                title: '提示',
+                message: `本次上机时长共计${useTime}分钟,不足${shortWraningTime}分钟,确定要下机吗?`
+              })
+                .then((e) => {
+                  console.log(e)
+                  this.getOff()
+                })
+                .catch(() => {
+                  console.log('ssss')
+                })
+            }
+          }
+        } else {
+          if (row.controlMode === '30') {
+            this.$refs.bluetoothRef.initBlue('close', row)
+          } else {
+            this.curAppointInfo = row
+            showConfirmDialog({
+              title: '提示',
+              message: `本次上机时长共计${useTime}分钟,确定要下机吗?`
+            })
+              .then((e) => {
+                console.log(e)
+                this.getOff()
+              })
+              .catch(() => {
+                console.log('ssss')
+              })
+          }
+        }
+      },
+      // 计算时间转分钟
+      getShortWraningTime(time, unit) {
+        if (unit == 'hour') {
+          return time * 60
+        } else if (unit == 'minute') {
+          return time
+        }
+      },
+      calcTimeDiff(date) {
+        var new_date = new Date() //新建一个日期对象,默认现在的时间
+        var old_date = new Date(date) //设置过去的一个时间点,"yyyy-MM-dd HH:mm:ss"格式化日期
+        var difftime = new_date - old_date //计算时间差
+        return Math.round(difftime / 60000)
+      },
+      async getOff() {
+        const params = { appointId: this.curAppointInfo.id }
+        const [err, res] = await to(instAppointApi.getOff({ ...params }))
+        if (err) return
+        if (res.code == 200) {
+          this.queryForm.pageNum = 1
+          this.getInstList()
+          showNotify({ type: 'success', message: '下机成功' })
+        }
+      },
+      callbackOff() {
+        this.queryForm.pageNum = 1
+        this.getInstList()
+      },
+      toDetail(v) {
+        this.$router.push(`/onlineInfo?appointId=${v.id}`)
+      }
+    }
+  }
+</script>
+<style lang="scss" scoped>
+  * {
+    box-sizing: border-box;
+  }
+  .panel-wrap {
+    height: 100%;
+    .data-list {
+      height: 100%;
+      padding: 20px;
+      overflow: auto;
+      .inst-item {
+        border-radius: 10px;
+        padding: 15px;
+        box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
+        margin-bottom: 20px;
+        .equ-tit {
+          width: 74px;
+        }
+      }
+    }
+  }
+</style>

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

@@ -0,0 +1,138 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-12 11:57:48
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-01-16 13:56:57
+ * @Description: file content
+ * @FilePath: \opms\pages\schedule\index.vue
+-->
+<template>
+  <!-- 页面内容 -->
+  <div class="home">
+    <!-- <van-pull-refresh v-model="loading" @refresh="onRefresh"> -->
+    <van-tabs v-model:active="active" type="card" @click-tab="onClickTab">
+      <van-tab title="即将上机">
+        <soon-geton v-if="active === 0" ref="soonGetonRef" />
+      </van-tab>
+      <van-tab title="正在上机">
+        <in-progress v-if="active === 1" ref="inProgressRef" />
+      </van-tab>
+      <van-tab title="等待审核">
+        <my-appoint v-if="active === 2" ref="myAppointRef" />
+      </van-tab>
+    </van-tabs>
+    <!-- </van-pull-refresh> -->
+    <van-tabbar route :placeholder="true">
+      <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>
+  </div>
+</template>
+<script>
+  import SoonGeton from './soonGeton/index.vue'
+  import InProgress from './inProgress/index.vue'
+  import MyAppoint from './myAppoint/index.vue'
+  export default {
+    name: 'appointList',
+    components: { SoonGeton, InProgress, MyAppoint },
+    data() {
+      return {
+        loading: false,
+        active: 0
+      }
+    },
+    // onLoad(option) {
+    //   console.log(option)
+    //   if (option.type && option.type === 'inProgress') {
+    //     this.curInstTabIndex = 1
+    //   }
+    //   console.log('onLoad')
+    //   setTimeout(() => {
+    //     this.showTemp = true
+    //     this.initList()
+    //   }, 500)
+    // },
+
+    // onShow() {
+    //   setTimeout(() => {
+    //     if (!this.$refs.soonGetonRef) {
+    //       return
+    //     }
+    //     switch (this.curInstTabIndex) {
+    //       case 0:
+    //         this.$refs.soonGetonRef.onRefresh()
+    //         break
+    //       case 1:
+    //         this.$refs.inProgressRef.onRefresh()
+    //         break
+    //       case 2:
+    //         this.$refs.myAppointRef.onRefresh()
+    //         break
+    //     }
+    //   })
+    // },
+
+    methods: {
+      // 下拉刷新被触发
+      onRefresh() {
+        // this.initList()
+        this.loading = false
+      },
+      onClickTab(e) {
+        this.active = e.name
+      }
+      // initList() {
+      //   if (this.curAppointTypeIndex == 0) {
+      //     switch (this.curInstTabIndex) {
+      //       case 0:
+      //         if (!this.$refs.soonGetonRef) return
+      //         this.$refs.soonGetonRef.queryForm.pageNum = 1
+      //         this.$refs.soonGetonRef.appointList = []
+      //         this.$refs.soonGetonRef.getInstList()
+      //         break
+      //       case 1:
+      //         if (!this.$refs.inProgressRef) return
+      //         this.$refs.inProgressRef.queryForm.pageNum = 1
+      //         this.$refs.inProgressRef.appointList = []
+      //         this.$refs.inProgressRef.getInstList()
+      //         break
+      //       case 2:
+      //         if (!this.$refs.myAppointRef) return
+      //         this.$refs.myAppointRef.queryForm.pageNum = 1
+      //         this.$refs.myAppointRef.appointList = []
+      //         this.$refs.myAppointRef.getInstList()
+      //         break
+      //     }
+      //   } else if (this.curAppointTypeIndex == 1) {
+      //     this.$refs.sampleDeliveryRef.queryForm.pageNum = 1
+      //     this.$refs.sampleDeliveryRef.getSampleAppointList()
+      //   }
+      // },
+    }
+  }
+</script>
+<style lang="scss" scoped>
+  * {
+    box-sizing: border-box;
+  }
+  .home {
+    height: 100%;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    padding-top: 10px;
+    .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>

+ 169 - 0
src/view/instr/appointList/myAppoint/index.vue

@@ -0,0 +1,169 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-12 11:57:48
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-09-22 14:38:38
+ * @Description: file content
+ * @FilePath: \labsop小程序\pages\schedule\myAppoint\index.vue
+-->
+<template>
+  <div class="panel-wrap">
+    <van-empty v-if="appointList.length == 0" mode="list" description="暂无待审核预约"></van-empty>
+    <van-list v-else v-model:loading="listloading" class="data-list" :finished="finished" finished-text="没有更多了" @load="onLoad">
+      <div class="inst-item mb40" v-for="(v, index) in appointList" :key="index">
+        <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.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" v-if="v.serviceName">
+          <div class="equ-tit">
+            <span class="fontSize14 bold">服务:</span>
+          </div>
+          <div>
+            <span class="fontSize14">{{ v.serviceName }}</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>
+          <div>
+            <span class="fontSize14 bold primary-color" @click="handleCancelAppoint(v)">取消预约</span>
+          </div>
+        </div>
+      </div>
+    </van-list>
+  </div>
+</template>
+
+<script>
+  import { useMyAppointApi } from '/@/api/appoint'
+  const myAppointApi = useMyAppointApi()
+  import instAppointApi from '/@/api/instr/instAppoint'
+  import to from 'await-to-js'
+  import { showConfirmDialog, showNotify } from 'vant'
+  export default {
+    data() {
+      return {
+        instStatus: {
+          10: '正常',
+          20: '故障',
+          30: '报废'
+        },
+        appointList: [],
+        current: 1,
+        queryForm: {
+          pageNum: 1,
+          pageSize: 10
+        },
+        total: 0,
+        listloading: false,
+        finished: false
+      }
+    },
+    mounted() {
+      this.queryForm.pageNum = 1
+      this.getInstList()
+    },
+    methods: {
+      onLoad() {
+        this.queryForm.pageNum++
+        this.getInstList()
+      },
+      handleCancelAppoint(row) {
+        showConfirmDialog({
+          title: '提示',
+          message: '确认取消预约?'
+        })
+          .then((e) => {
+            this.cancelAppoint(row.id)
+          })
+          .catch(() => {
+            console.log('ssss')
+          })
+      },
+      async cancelAppoint(id) {
+        const params = { id }
+        const [err, res] = await to(instAppointApi.userCancelAppoint(params))
+        this.modalVisible = false
+        if (err) return
+        if (res?.code === 200) {
+          this.queryForm.pageNum = 1
+          this.appointList = []
+          this.getInstList()
+          showNotify({ type: 'success', message: '取消成功' })
+        }
+      },
+
+      // 查询列表
+      async getInstList() {
+        this.listloading = true
+        const [err, res] = await to(myAppointApi.myAppoint(this.queryForm))
+        this.listloading = false
+        if (err) return
+        if (res?.code === 200) {
+          this.appointList = this.queryForm.pageNum == 1 ? [...(res?.data?.list || [])] : [...this.appointList, ...(res?.data?.list || [])]
+          this.total = res?.data?.total
+          if (this.queryForm.pageNum * this.queryForm.pageSize >= res.data.total) {
+            this.finished = true
+          }
+        }
+      }
+    }
+  }
+</script>
+<style lang="scss" scoped>
+  * {
+    box-sizing: border-box;
+  }
+  .panel-wrap {
+    height: 100%;
+    .data-list {
+      height: 100%;
+      padding: 20px;
+      overflow: auto;
+      .inst-item {
+        border-radius: 10px;
+        padding: 15px;
+        box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
+        margin-bottom: 20px;
+        .equ-tit {
+          width: 74px;
+        }
+      }
+    }
+  }
+</style>

+ 282 - 0
src/view/instr/appointList/onlineInfo/index.vue

@@ -0,0 +1,282 @@
+<template>
+  <div class="equ-container">
+    <!-- 实验员信息 -->
+    <div class="inst-item mb40">
+      <div class="flex flex-between mb20">
+        <div class="ml10"><span class="fontSize14 bold primary-color">实验员信息</span></div>
+        <div class="flex">
+          <van-button
+            v-if="showOffBtn"
+            style="width: 60px; height: 30px; margin: 0 20px 0 0; font-size: 14px"
+            class="scan-txt"
+            type="danger"
+            size="small"
+            :disabled="!details?.instId"
+            @click="handleIsGetOff"
+          >
+            下机
+          </van-button>
+          <van-button style="width: 60px; height: 30px; margin: 0; font-size: 14px" class="scan-txt" type="primary" size="small" @click="backPrePage">
+            返回
+          </van-button>
+        </div>
+      </div>
+      <div class="flex mb20">
+        <div class="equ-tit">
+          <span class="fontSize14 bold">姓名:</span>
+        </div>
+        <div>
+          <span class="fontSize14">{{ details.userName }}</span>
+        </div>
+      </div>
+      <div class="flex mb20">
+        <div class="equ-tit">
+          <span class="fontSize14 bold">部门:</span>
+        </div>
+        <div>
+          <!-- <span class="fontSize14">{{ details?.deptName }}</span> -->
+        </div>
+      </div>
+      <div class="flex mb20">
+        <div class="equ-tit">
+          <span class="fontSize14 bold">电话:</span>
+        </div>
+        <div>
+          <span class="fontSize14">{{ details?.userContact }}</span>
+        </div>
+      </div>
+      <div class="flex mb20">
+        <div class="equ-tit">
+          <span class="fontSize14 bold">预计开始时间:</span>
+        </div>
+        <div>
+          <span class="fontSize14">{{ details?.startTime }}</span>
+        </div>
+      </div>
+      <div class="flex mb20">
+        <div class="equ-tit">
+          <span class="fontSize14 bold">预计结束时间:</span>
+        </div>
+        <div>
+          <span class="fontSize14">{{ details?.endTime }}</span>
+        </div>
+      </div>
+      <div class="flex mb20">
+        <div class="equ-tit">
+          <span class="fontSize14 bold">预计时长:</span>
+        </div>
+        <div>
+          <span class="fontSize14">{{ diffTime(details?.startTime, details?.endTime) + '分钟' }}</span>
+        </div>
+      </div>
+    </div>
+    <!-- end -->
+    <!-- 设备信息 -->
+    <div class="inst-item mt20">
+      <div class="flex mb20">
+        <div class="ml10"><span class="fontSize14 bold primary-color" text="">设备信息</span></div>
+      </div>
+      <div class="flex mb20">
+        <div class="equ-tit">
+          <span class="fontSize14 bold">设备名称:</span>
+        </div>
+        <div>
+          <span class="fontSize14">{{ details?.instName }}</span>
+        </div>
+      </div>
+      <div class="flex mb20">
+        <div class="equ-tit">
+          <span class="fontSize14 bold">设备型号:</span>
+        </div>
+        <div>
+          <span class="fontSize14">{{ details?.instNameEn }}</span>
+        </div>
+      </div>
+      <div class="flex mb20">
+        <div class="equ-tit">
+          <span class="fontSize14 bold">所在位置:</span>
+        </div>
+        <div>
+          <span class="fontSize14">{{ details?.placeAddress + '(' + (details?.laboratoryName || '') + ')' }}</span>
+        </div>
+      </div>
+      <div class="flex mb20">
+        <div class="equ-tit">
+          <span class="fontSize14 bold">下一个预约人:</span>
+        </div>
+        <div>
+          <span class="fontSize14">{{ details?.next?.userName || '-' }}</span>
+        </div>
+      </div>
+      <div class="flex mb20">
+        <div class="equ-tit">
+          <span class="fontSize14 bold">下一个预约时间:</span>
+        </div>
+        <div>
+          <span class="fontSize14">{{ details?.next?.startTime || '-' }}</span>
+        </div>
+      </div>
+    </div>
+    <u-modal :show="modalVisible" :content="offLineContent" showCancelButton @cancel="close" @confirm="getOff" ref="uModal" :asyncClose="true"></u-modal>
+    <u-notify ref="uNotify"></u-notify>
+    <!-- <blue-tooth ref="bluetoothRef" @getOff="callbackOff"></blue-tooth> -->
+  </div>
+</template>
+
+<script>
+  // import BlueTooth from '../../../components/BlueTooth'
+  import { useMyAppointApi } from '/@/api/appoint'
+  const myAppointApi = useMyAppointApi()
+  import instAppointApi from '/@/api/instr/instAppoint'
+  import { useInstrApi } from '/@/api/instr'
+  const instApi = useInstrApi()
+  import to from 'await-to-js'
+  import moment from 'moment'
+  import { showConfirmDialog, showNotify } from 'vant'
+  export default {
+    name: 'LabsopIndex',
+    // components: { BlueTooth },
+    data() {
+      return {
+        offLineContent: '',
+        showOffBtn: true,
+        appointId: 0,
+        details: {},
+        modalVisible: false
+      }
+    },
+    mounted() {
+      this.appointId = Number(this.$route.query.appointId)
+      this.getOnlineInfo()
+    },
+    methods: {
+      diffTime(start, end) {
+        const startTime = moment(start)
+        const endTime = moment(end)
+        // 计算两个时间的分钟差异
+        return endTime.diff(startTime, 'minutes')
+      },
+      // 下机的时候提示使用时长
+      calcTimeDiff(date) {
+        var new_date = new Date() //新建一个日期对象,默认现在的时间
+        var old_date = new Date(date) //设置过去的一个时间点,"yyyy-MM-dd HH:mm:ss"格式化日期
+        var difftime = new_date - old_date //计算时间差
+        return Math.round(difftime / 60000)
+      },
+      async getOnlineInfo() {
+        const params = { appointId: this.appointId }
+        const [err, res] = await to(myAppointApi.appointDetails({ ...params }))
+        if (err) return
+        if (res.code == 200 && res.data) {
+          this.details = res.data
+        }
+        console.log(this.details)
+      },
+      // 返回上机信息页面
+      backPrePage() {
+        this.$router.push('/instr-appoint-record')
+      },
+      async handleIsGetOff() {
+        const [err, res] = await to(instApi.getSettingDetail({ instId: this.details.instId, code: 'InstCfgUse' }))
+        if (err) return
+        // 上机时间
+        const useTime = this.calcTimeDiff(this.details.usedStartTime)
+        // 是否进行上机时间太短的提示
+        // 需要
+        if (res?.data?.config.shortWarningEnable) {
+          const shortWraningTime = this.getShortWraningTime(res?.data?.config.shortWarning, res?.data?.config.shortWarningUnit)
+          if (useTime > shortWraningTime) {
+            if (this.details.controlMode === '30') {
+              this.$refs.bluetoothRef.initBlue('close', this.details)
+            } else {
+              this.curAppointInfo = this.details
+              showConfirmDialog({
+                title: '提示',
+                message: `本次上机时长共计${useTime}分钟,确定要下机吗?`
+              })
+                .then((e) => {
+                  console.log(e)
+                  this.getOff()
+                })
+                .catch(() => {
+                  console.log('ssss')
+                })
+            }
+          } else {
+            if (this.details.controlMode === '30') {
+              this.$refs.bluetoothRef.initBlue('close', this.details)
+            } else {
+              this.curAppointInfo = this.details
+              showConfirmDialog({
+                title: '提示',
+                message: `本次上机时长共计${useTime}分钟,不足${shortWraningTime}分钟,确定要下机吗?`
+              })
+                .then((e) => {
+                  console.log(e)
+                  this.getOff()
+                })
+                .catch(() => {
+                  console.log('ssss')
+                })
+            }
+          }
+        } else {
+          if (this.details.controlMode === '30') {
+            this.$refs.bluetoothRef.initBlue('close', this.details)
+          } else {
+            this.curAppointInfo = this.details
+            showConfirmDialog({
+              title: '提示',
+              message: `本次上机时长共计${useTime}分钟,确定要下机吗?`
+            })
+              .then((e) => {
+                console.log(e)
+                this.getOff()
+              })
+              .catch(() => {
+                console.log('ssss')
+              })
+          }
+        }
+      },
+      // 计算时间转分钟
+      getShortWraningTime(time, unit) {
+        if (unit == 'hour') {
+          return time * 60
+        } else if (unit == 'minute') {
+          return time
+        }
+      },
+      close() {
+        this.modalVisible = false
+      },
+      async getOff() {
+        const params = { appointId: this.curAppointInfo.id }
+        const [err, res] = await to(instAppointApi.getOff({ ...params }))
+        if (err) return
+        if (res.code == 200) {
+          showNotify({ type: 'success', message: '下机成功' })
+          this.callbackOff()
+        }
+      },
+      callbackOff() {
+        this.$router.push('/instr-appoint-record')
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .equ-container {
+    height: 100vh;
+    padding: 20px;
+    .inst-item {
+      border-radius: 10px;
+      padding: 15px;
+      box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
+      // .equ-tit {
+      //   width: 74px;
+      // }
+    }
+  }
+</style>

+ 288 - 0
src/view/instr/appointList/soonGeton/index.vue

@@ -0,0 +1,288 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-12 11:57:48
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-09-26 18:13:20
+ * @Description: file content
+ * @FilePath: \labsop小程序\pages\appointList\soonGeton\index.vue
+-->
+<template>
+  <div class="panel-wrap">
+    <!-- <van-pull-refresh v-model="refreshing" @refresh="onRefresh"> -->
+    <van-empty v-if="appointList.length == 0" mode="list" description="暂无即将上机"></van-empty>
+    <van-list v-else v-model:loading="listloading" class="data-list" :finished="finished" finished-text="没有更多了" @load="onLoad">
+      <div class="inst-item mb40" v-for="(v, index) in appointList" :key="index">
+        <div class="flex flex-between mb20">
+          <div>
+            <div class="mr10">
+              <span class="primary-color bold fontSize14">{{ v.instName }}</span>
+            </div>
+          </div>
+          <div class="flex">
+            <van-button
+              style="width: 80px; height: 30px; margin: 0; font-size: 14px"
+              class="scan-txt"
+              type="primary"
+              size="small"
+              :disabled="loading"
+              v-if="['10', '20', '30', '50'].includes(v.controlMode)"
+              @click="handleGetOn(v)"
+            >
+              {{ v.controlMode == '10' ? '上机' : '扫码上机' }}
+            </van-button>
+            <van-button
+              style="width: 60px; height: 30px; margin: 0 0 0 10px; font-size: 14px"
+              class="scan-txt"
+              type="warning"
+              size="small"
+              :disabled="loading"
+              @click="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.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" v-if="v.serviceName">
+          <div class="equ-tit">
+            <span class="fontSize14 bold">服务:</span>
+          </div>
+          <div>
+            <span class="fontSize14">{{ v.serviceName }}</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 }}
+              <!-- {{ v.startTime ? formatDates(new Date(v.startTime), 'YYYY-mm-dd HH:MM') : '' }} -->
+            </span>
+          </div>
+        </div>
+      </div>
+    </van-list>
+    <!-- </van-pull-refresh> -->
+    <!-- <blue-tooth ref="bluetoothRef"></blue-tooth> -->
+  </div>
+</template>
+
+<script>
+  // import BlueTooth from '../../../components/BlueTooth'
+  // import { formatDate } from '/@/utils/formatTime'
+  import { useUserInfo } from '/@/stores/userInfo'
+  import { useMyAppointApi } from '/@/api/appoint'
+  const myAppointApi = useMyAppointApi()
+  import { useInstrApi } from '/@/api/instr'
+  const instApi = useInstrApi()
+  import instAppointApi from '/@/api/instr/instAppoint'
+  import to from 'await-to-js'
+  import { showConfirmDialog, showNotify } from 'vant'
+  export default {
+    // components: { BlueTooth },
+    data() {
+      return {
+        loading: false,
+        curAppointInfo: {},
+        appointList: [],
+        queryForm: {
+          pageNum: 1,
+          pageSize: 10
+        },
+        total: 0,
+        listloading: false,
+        finished: false
+      }
+    },
+    mounted() {
+      this.queryForm.pageNum = 1
+      this.getInstList()
+    },
+    methods: {
+      // 重新加载
+      onLoad() {
+        this.queryForm.pageNum++
+        this.getInstList()
+      },
+      // 查询列表
+      async getInstList() {
+        this.listloading = true
+        const [err, res] = await to(myAppointApi.toGetonList(this.queryForm))
+        this.listloading = false
+        if (err) return
+        if (res?.code === 200) {
+          this.appointList = this.queryForm.pageNum == 1 ? [...(res?.data?.list || [])] : [...this.appointList, ...(res?.data?.list || [])]
+          this.total = res?.data?.total
+          if (this.queryForm.pageNum * this.queryForm.pageSize >= res.data.total) {
+            this.finished = true
+          }
+        }
+      },
+      /**
+       * 触发上机之前的订阅
+       *
+       * */
+      handleGetOn(row) {
+        this.handleScanCode(row)
+      },
+      /**
+       * 调起二维码扫码
+       * controlMode (10 不控制 20 电源控制(wifi) 30 电源控制(蓝牙) 50 电脑控制)
+       */
+      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') {
+          // wifi控制 和 电脑控制
+          const that = this
+          // 调起条码扫描
+          const res = await useUserInfo().scanCode()
+          that.handleDeCode(res, row)
+        } else if (row.controlMode == '30') {
+          // 蓝牙
+          // this.$refs.bluetoothRef.initBlue('open', row)
+        }
+      },
+      /**
+       *  解码
+       *  code 二维码内容
+       *  row 当前预约信息
+       * */
+      async handleDeCode(code, row) {
+        if (this.loading) return
+        this.loading = true
+        const params = { content: code }
+        const [err, res] = await to(instApi.decode({ ...params }))
+        if (err) return (this.loading = false)
+        if (res.code == 200) {
+          this.handleCodeInfo(res.data.content, row)
+        } else {
+          this.loading = false
+        }
+      },
+      /**
+       * 解码后的信息判断扫码仪器是否是当前预约仪器
+       * content 解码后的内容 两种 带id(仪器id)和不带id
+       * appointRow 当前预约信息
+       */
+      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 {
+          // 拿到解码信息(仪器编码)获取仪器id
+          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 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((e) => {
+            console.log(e)
+            this.cancelAppoint(row.id)
+          })
+          .catch(() => {
+            console.log('ssss')
+          })
+      },
+      // 取消预约
+      async cancelAppoint(id) {
+        const params = { id }
+        const [err, res] = await to(instAppointApi.userCancelAppoint(params))
+        this.modalVisible = false
+        if (err) return
+        if (res?.code === 200) {
+          this.queryForm.pageNum = 1
+          this.appointList = []
+          this.getInstList()
+          showNotify({ type: 'success', message: '取消成功' })
+        }
+      }
+    }
+  }
+</script>
+<style lang="scss" scoped>
+  * {
+    box-sizing: border-box;
+  }
+  .panel-wrap {
+    height: 100%;
+    .data-list {
+      height: 100%;
+      padding: 20px;
+      overflow: auto;
+      .inst-item {
+        border-radius: 10px;
+        padding: 15px;
+        box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
+        margin-bottom: 20px;
+        .equ-tit {
+          width: 74px;
+        }
+      }
+    }
+  }
+</style>

+ 462 - 0
src/view/instr/list.vue

@@ -0,0 +1,462 @@
+<template>
+  <div class="home">
+    <div class="search-wrap">
+      <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>
+      </div>
+      <div>
+        <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-wrap">
+        <div @click="openSelectLaboratory">
+          <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>
+    </div>
+    <div class="inst-list">
+      <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">
+            <div class="header flex_between">
+              <div class="flex">
+                <div class="flex">
+                  <img @click="handleFollowInst(v)" v-if="v.following" class="follow-icon mr10" src="../../assets/img/follow.png" />
+                  <img @click="handleFollowInst(v)" v-else class="follow-icon mr10" src="../../assets/img/unfollow.png" />
+                  <div class="primary-color instr-status-btn">{{ v.instName }}</div>
+                </div>
+              </div>
+              <div style="width: 50px">
+                <div class="success-color instr-status-btn" v-if="v.instStatus == '10'">正常</div>
+                <div class="warning-color instr-status-btn" v-if="v.instStatus == '20'">故障</div>
+                <div class="danger-color instr-status-btn" v-if="v.instStatus == '30'">报废</div>
+              </div>
+            </div>
+            <div class="inst-info mt10 mb10">
+              <div class="i-left" @click="openDetail(v)">
+                <img :showLoading="true" :src="v.instPicture" width="122px" height="83px" />
+              </div>
+              <div class="i-right ml10">
+                <div @click="openDetail(v)">
+                  <div class="flex flex-top mb10 ml2">
+                    <div class="detailTxt">{{ v.instCode }}</div>
+                  </div>
+                  <div class="flex flex-top mb10">
+                    <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 mb10">
+                    <img class="i-r-icon" src="../../assets/img/address.png" v-if="v.placeAddress" />
+                    <div class="detailTxt">{{ v.placeAddress + setLaboratoryName(v.laboratoryName) }}</div>
+                  </div>
+                </div>
+                <div class="flex flex-between" style="padding-right: 10px">
+                  <van-button
+                    type="primary"
+                    style="width: 70px; height: 31px; margin: 0"
+                    v-if="v.instStatus == '10' && v.isAppointment == '10'"
+                    @click="openAppoint(v)"
+                    size="small"
+                  >
+                    预约
+                  </van-button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </van-list>
+      </div>
+    </div>
+    <van-tabbar route :placeholder="true">
+      <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">
+      <div class="detail-box">
+        <div class="instNameTxt">{{ state.instDetail.instName }}</div>
+        <div v-if="state.instDetail.instName" class="dc-content">
+          <div class="flex">
+            <div class="label-item">仪器型号:</div>
+            <div class="detailTxt">{{ state.instDetail.instNameEn }}</div>
+          </div>
+          <div class="flex">
+            <div class="label-item">当前状态:</div>
+            <div class="detailTxt">{{ state.instStatus[state.instDetail.instStatus] }}</div>
+          </div>
+          <div class="flex">
+            <div class="label-item">所属组织:</div>
+            <div class="detailTxt">{{ state.instDetail.belongOrgName }}</div>
+          </div>
+          <div class="flex">
+            <div class="label-item">位置:</div>
+            <div class="detailTxt">{{ state.instDetail.placeAddress + '(' + state.instDetail.laboratoryName + ')' }}</div>
+          </div>
+          <!--<div>
+           <div class="label-item">开放时段:</div>
+            <u-text :text="state.instDetail.instName" size="14"></u-text>
+          </div> -->
+          <div class="flex">
+            <div class="label-item">负责人:</div>
+            <div class="detailTxt">{{ state.instDetail.instHeadName }}</div>
+          </div>
+          <div class="flex">
+            <div class="label-item">联系方式:</div>
+            <div class="detailTxt">{{ state.instDetail.instHeadTel }}</div>
+          </div>
+          <u-divider :dashed="true"></u-divider>
+          <div class="mt10 mb10">
+            <div class="labelTit">主要规格及技术指标</div>
+            <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>
+          <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>
+        </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>
+    <!-- <van-picker :show="showLab" :columns="laboratoryColumns" keyName="name" @cancel="showLab = false" @confirm="pickLab"></van-picker> -->
+    <!-- <shutDown ref="shutDownRef"></shutDown> -->
+  </div>
+</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'
+
+  const router = useRouter()
+  const instApi = useInstrApi()
+  const blacklistApi = useBlackApi()
+  const positionApi = usePositionApi()
+
+  const state = reactive({
+    showLab: false,
+    laboratoryColumns: [],
+    filterVisible: false,
+    instStatus: {
+      10: '正常',
+      20: '故障',
+      30: '报废'
+    },
+    instList: [],
+    current: 1,
+    queryForm: {
+      laboratoryId: 0,
+      laboratoryName: '',
+      pageNum: 1,
+      pageSize: 10,
+      searchText: ''
+    },
+    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
+      }
+    })
+  }
+  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) => {
+    state.popupShow = true
+    getDetail(v.id)
+  }
+  const onLoad = () => {
+    state.queryForm.pageNum++
+    getInstList()
+  }
+  // 查询列表
+  const getInstList = async () => {
+    state.loading = true
+    const [err, res]: ToResponse = await to(instApi.getList(state.queryForm))
+    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
+      }
+    }
+    state.filterVisible = false
+  }
+  const onSearch = () => {
+    state.queryForm.pageNum = 1
+    getInstList()
+  }
+  // 重置
+  const reset = () => {
+    state.queryForm.laboratoryId = 0
+    state.queryForm.laboratoryName = ''
+  }
+  // 确认
+  const confirmFilter = () => {
+    state.filterVisible = false
+    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()
+  })
+</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;
+    }
+    .inst-list {
+      flex: 1;
+      height: 0;
+      // padding-bottom: 20px;
+      // padding: 0 15px;
+      // height: calc(100% - 60px);
+      .inst-wrap {
+        height: 100%;
+        overflow: auto;
+        padding: 20px 20px 0 20px;
+        // padding: 10px 10px 0 10px;
+      }
+      .inst-item {
+        padding: 10px;
+        box-shadow: -2px 0px 9px rgba(0, 0, 0, 0.12);
+        margin-bottom: 14px;
+        border-radius: 6px;
+        .follow-icon {
+          width: 24px;
+          height: 24px;
+        }
+        .inst-info {
+          display: flex;
+        }
+        .i-right {
+          flex: 1;
+          font-size: 14px;
+          .i-r-icon {
+            width: 15px;
+            height: 15px;
+            margin-right: 10px;
+          }
+        }
+      }
+    }
+    .detail-box {
+      height: 80vh;
+      background: #fff;
+      border-radius: 10px;
+      padding: 15px;
+      .dc-content {
+        width: 100%;
+        height: calc(100% - 30px);
+        overflow: auto;
+        .label-item {
+          width: 90px;
+          font-size: 14px;
+          padding: 6px;
+        }
+        .detailTxt {
+          flex: 1;
+        }
+      }
+    }
+    .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;
+
+        .filter-item {
+          padding-bottom: 14px;
+
+          .tit {
+            font-size: 14px;
+            color: #323232;
+            font-weight: bold;
+            padding-bottom: 10px;
+          }
+
+          .menu-list {
+            display: flex;
+            flex-wrap: wrap;
+
+            .menu-item {
+              margin-right: 20px;
+              margin-bottom: 10px;
+            }
+          }
+        }
+      }
+
+      .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;
+  }
+  .detailTxt {
+    font-size: 14px;
+    color: #333333;
+  }
+  .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;
+  }
+</style>

+ 2 - 1
tsconfig.json

@@ -14,12 +14,13 @@
     "lib": ["ESNext", "DOM"],
     "skipLibCheck": true,
     "types": ["vite/client"],
+    "allowJs": true,
     "paths": {
       "/@/*": [
         "src/*"
       ]
     }
   },
-  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/utils/errorCode.js", "src/utils/micro_request.js", "src/types/*.d.ts", "src/types/**/*.ts"],
+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/utils/errorCode.js", "src/utils/micro_request.js", "src/types/*.d.ts", "src/types/**/*.ts", "src/api/appoint/index.ts"],
   "references": [{ "path": "./tsconfig.node.json" }]
 }