BindLabModal.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723
  1. <!-- 绑定实验室弹窗 -->
  2. <template>
  3. <view class="bind-lab-modal" v-if="visible">
  4. <!-- 遮罩层点击不关闭(强制绑定模式),offType=true时可关闭 -->
  5. <view class="bind-lab-overlay" v-if="offType" @tap="handleClose"></view>
  6. <view class="bind-lab-overlay no-close" v-else></view>
  7. <!-- 主弹窗 -->
  8. <view class="bind-lab-content" @tap.stop>
  9. <!-- 标题栏 -->
  10. <view class="modal-header">
  11. <text class="modal-title">绑定实验室</text>
  12. <text v-if="offType" class="modal-close" @tap="handleClose">✕</text>
  13. </view>
  14. <scroll-view scroll-y class="modal-scroll" :style="scrollStyle">
  15. <view class="modal-body">
  16. <!-- 无需进入实验室 勾选区 -->
  17. <view class="no-lab-box">
  18. <view class="checkbox-row" @tap="toggleNoLab">
  19. <view class="custom-checkbox" :class="{ checked: isEnter }"></view>
  20. <text class="no-lab-title">无需进入实验室</text>
  21. </view>
  22. <view class="no-lab-desc">
  23. <text>人文、体育等相关学院无需进入实验室的新研究生可选择此项,选择后学时要求为4小时,需通过应知应会考试。</text>
  24. </view>
  25. </view>
  26. <!-- 选择实验室(未勾选"无需进入"时显示) -->
  27. <view v-if="!isEnter">
  28. <!-- 选择实验室按钮 -->
  29. <view class="select-lab-btn" @tap="openSelectLab">
  30. <text v-if="!selectedLab" class="select-lab-placeholder">点击选择实验室</text>
  31. <text v-else class="select-lab-value">{{ selectedLabLabel }}</text>
  32. <text class="select-lab-arrow">›</text>
  33. </view>
  34. <text class="search-tip">此处请选择需要后续进入的科研实验室的名称,输入实验室名称、房号或楼栋关键词,再从检索结果中选择目标实验室。</text>
  35. <!-- 实验室信息表 -->
  36. <view class="lab-info-table" v-if="selectedLab">
  37. <view class="info-row">
  38. <view class="info-cell label-cell">实验室名称</view>
  39. <view class="info-cell value-cell">{{ selectedLab.subName }}{{ selectedLab.roomNum ? '(' + selectedLab.roomNum + ')' : '' }}</view>
  40. </view>
  41. <view class="info-row">
  42. <view class="info-cell label-cell">实验室类型</view>
  43. <view class="info-cell value-cell">{{ selectedLab.moldName }}</view>
  44. </view>
  45. <view class="info-row">
  46. <view class="info-cell label-cell">负责人</view>
  47. <view class="info-cell value-cell">{{ selectedLab.adminName }}</view>
  48. </view>
  49. <view class="info-row">
  50. <view class="info-cell label-cell">实验室分级</view>
  51. <view class="info-cell value-cell" :style="'color:' + (selectedLab.classLevelColor || '#333')">{{ selectedLab.classLevelName }}</view>
  52. </view>
  53. <view class="info-row">
  54. <view class="info-cell label-cell">所属学院</view>
  55. <view class="info-cell value-cell">{{ selectedLab.deptName }}</view>
  56. </view>
  57. <view class="info-row">
  58. <view class="info-cell label-cell">实验室分类</view>
  59. <view class="info-cell value-cell">{{ selectedLab.classTypeNames }}</view>
  60. </view>
  61. <view class="info-row no-border">
  62. <view class="info-cell label-cell">所在位置</view>
  63. <view class="info-cell value-cell">{{ selectedLab.buildName }}{{ selectedLab.floorName }}</view>
  64. </view>
  65. </view>
  66. <!-- 本科生:进入原因(educationType=2) -->
  67. <view class="reason-box" v-if="educationType == 2&&!isEnter">
  68. <textarea
  69. class="reason-input"
  70. v-model="reason"
  71. placeholder="请填写进入实验室原因(必填)"
  72. maxlength="100"
  73. :auto-height="false"
  74. ></textarea>
  75. <text class="reason-tip">毕业论文或毕业设计,科创等相关需要在具体的实验室开展业务的情况可申请绑定实验室,绑定完毕后需要完成对应的分级考试。</text>
  76. </view>
  77. </view>
  78. <!-- 协议勾选 -->
  79. <view class="agree-row" @tap="onAgreeTap">
  80. <view class="custom-checkbox" :class="{ checked: checkType }"></view>
  81. <view class="agree-text">
  82. <text>我已阅读并同意</text>
  83. <text class="agree-link" @tap.stop="openNotice">《新生绑定实验室须知及责任说明》</text>
  84. </view>
  85. </view>
  86. <!-- 底部按钮 -->
  87. <view class="modal-footer">
  88. <view v-if="offType" class="btn-cancel" @tap="handleClose">取消</view>
  89. <view class="btn-confirm" @tap="handleConfirm">确认绑定</view>
  90. </view>
  91. </view>
  92. </scroll-view>
  93. </view>
  94. <!-- 须知弹窗 -->
  95. <view class="notice-modal" v-if="showNotice" @tap.stop>
  96. <view class="notice-content">
  97. <view class="notice-header">
  98. <text class="notice-title">新生绑定实验室须知及责任说明</text>
  99. </view>
  100. <scroll-view scroll-y class="notice-scroll">
  101. <view class="notice-body">
  102. <text class="notice-p">1. 保证进入的实验室为导师实际使用实验室或属于当前实验室所属课题组。</text>
  103. <text class="notice-p">2. 研究生期间涉及到操作实验室内重要危险源,必须绑定实验室。</text>
  104. </view>
  105. </scroll-view>
  106. <view class="notice-footer">
  107. <view class="btn-cancel" @tap="cancelNotice">取消</view>
  108. <view class="btn-confirm" @tap="confirmNotice">我已知晓</view>
  109. </view>
  110. </view>
  111. </view>
  112. <!-- 选择实验室弹窗 -->
  113. <view class="select-lab-modal" v-if="showSelectLab" @tap="closeSelectLab">
  114. <view class="select-lab-content" @tap.stop>
  115. <view class="select-lab-header">
  116. <text class="select-lab-title">选择实验室</text>
  117. <text class="modal-close" @tap="closeSelectLab">✕</text>
  118. </view>
  119. <!-- 搜索框 -->
  120. <view class="search-bar">
  121. <input
  122. class="search-input"
  123. type="text"
  124. v-model="searchKeyword"
  125. placeholder="输入实验室名称、房间或楼栋关键词搜索"
  126. @input="onSearchInput"
  127. />
  128. </view>
  129. <!-- 列表 -->
  130. <scroll-view
  131. scroll-y
  132. class="lab-list-scroll"
  133. :style="labListScrollStyle"
  134. @scrolltolower="onScrollLower"
  135. >
  136. <view v-if="labList.length === 0 && !labLoading" class="lab-list-empty">暂无数据</view>
  137. <view
  138. v-for="(lab, idx) in labList"
  139. :key="idx"
  140. class="lab-list-item"
  141. :data-idx="idx"
  142. @tap="onLabItemTap"
  143. >
  144. <text class="lab-item-name">{{ lab.subName }}{{ lab.roomNum ? '(' + lab.roomNum + ')' : '' }}</text>
  145. <text class="lab-item-sub">{{ lab.deptName }} · {{ lab.buildName }}{{ lab.floorName }}</text>
  146. </view>
  147. <view v-if="labLoading" class="lab-list-loading">加载中...</view>
  148. <view v-if="labNoMore" class="lab-list-loading">没有更多了</view>
  149. </scroll-view>
  150. </view>
  151. </view>
  152. </view>
  153. </template>
  154. <script>
  155. import {
  156. examElLabEduRelationLabLists,
  157. examElLabEduRelationBind,
  158. } from '@/pages_safetyEducationExamination/api/index.js'
  159. export default {
  160. name: 'BindLabModal',
  161. props: {
  162. visible: {
  163. type: Boolean,
  164. default: false
  165. },
  166. // 是否可关闭(true=去绑定时点击可关闭,false=强制绑定)
  167. offType: {
  168. type: Boolean,
  169. default: true
  170. },
  171. // 用户教育类型:1=本科生,2=研究生(来自 bindStatus 接口)
  172. educationType: {
  173. type: [Number, String],
  174. default: 1
  175. },
  176. },
  177. data() {
  178. return {
  179. // 表单
  180. isEnter: false, // 无需进入实验室
  181. checkType: false, // 已同意协议
  182. reason: '', // 进入原因(本科生必填)
  183. selectedLab: null, // 选中的实验室对象
  184. // 须知弹窗
  185. showNotice: false,
  186. // 选择实验室弹窗
  187. showSelectLab: false,
  188. searchKeyword: '',
  189. searchTimer: null,
  190. labList: [],
  191. labLoading: false,
  192. labNoMore: false,
  193. labPage: 1,
  194. labPageSize: 15,
  195. labTotal: 0,
  196. }
  197. },
  198. computed: {
  199. selectedLabLabel() {
  200. if (!this.selectedLab) return '';
  201. const lab = this.selectedLab;
  202. return (lab.deptName || '') + ' - ' + (lab.subName || '') +
  203. (lab.roomNum ? '(' + lab.roomNum + ')' : '') +
  204. ' - ' + (lab.buildName || '') + (lab.floorName || '');
  205. },
  206. scrollStyle() {
  207. const sys = uni.getSystemInfoSync();
  208. const h = sys.windowHeight - 100;
  209. return 'height:' + (h > 300 ? h : 300) + 'px;';
  210. },
  211. labListScrollStyle() {
  212. const sys = uni.getSystemInfoSync();
  213. const h = sys.windowHeight * 0.5;
  214. return 'height:' + h + 'px;';
  215. },
  216. },
  217. watch: {
  218. visible(val) {
  219. if (val) this.resetState();
  220. }
  221. },
  222. methods: {
  223. resetState() {
  224. this.isEnter = false;
  225. this.checkType = false;
  226. this.reason = '';
  227. this.selectedLab = null;
  228. this.showNotice = false;
  229. this.showSelectLab = false;
  230. this.searchKeyword = '';
  231. this.labList = [];
  232. this.labPage = 1;
  233. this.labTotal = 0;
  234. this.labNoMore = false;
  235. },
  236. handleClose() {
  237. if (!this.offType) return;
  238. this.$emit('close');
  239. },
  240. toggleNoLab() {
  241. this.isEnter = !this.isEnter;
  242. },
  243. onAgreeTap() {
  244. // 点击文字区域时切换,但点链接时单独处理
  245. this.checkType = !this.checkType;
  246. },
  247. // ---- 须知弹窗 ----
  248. openNotice() {
  249. this.showNotice = true;
  250. },
  251. cancelNotice() {
  252. this.checkType = false;
  253. this.showNotice = false;
  254. },
  255. confirmNotice() {
  256. this.checkType = true;
  257. this.showNotice = false;
  258. },
  259. // ---- 选择实验室弹窗 ----
  260. openSelectLab() {
  261. this.showSelectLab = true;
  262. this.searchKeyword = '';
  263. this.labList = [];
  264. this.labPage = 1;
  265. this.labTotal = 0;
  266. this.labNoMore = false;
  267. this.fetchLabList(true);
  268. },
  269. closeSelectLab() {
  270. this.showSelectLab = false;
  271. clearTimeout(this.searchTimer);
  272. },
  273. onSearchInput() {
  274. clearTimeout(this.searchTimer);
  275. this.searchTimer = setTimeout(() => {
  276. this.labPage = 1;
  277. this.labList = [];
  278. this.labNoMore = false;
  279. this.fetchLabList(true);
  280. }, 300);
  281. },
  282. onScrollLower() {
  283. if (this.labNoMore || this.labLoading) return;
  284. this.labPage++;
  285. this.fetchLabList(false);
  286. },
  287. fetchLabList(reset) {
  288. if (this.labLoading) return;
  289. this.labLoading = true;
  290. examElLabEduRelationLabLists({
  291. searchValue: this.searchKeyword.trim(),
  292. page: this.labPage,
  293. pageSize: this.labPageSize,
  294. }).then(res => {
  295. const resp = res.data || {};
  296. if (resp.code === 200) {
  297. const records = Array.isArray(resp.data) ? resp.data : ((resp.data || {}).records || []);
  298. const total = typeof resp.data === 'object' && !Array.isArray(resp.data) ? (resp.data.total || records.length) : records.length;
  299. if (reset) {
  300. this.labList = records;
  301. } else {
  302. this.labList = [...this.labList, ...records];
  303. }
  304. this.labTotal = total;
  305. this.labNoMore = this.labList.length >= this.labTotal;
  306. }
  307. }).catch(() => {}).finally(() => {
  308. this.labLoading = false;
  309. });
  310. },
  311. onLabItemTap(e) {
  312. const idx = Number(e.currentTarget.dataset.idx);
  313. const lab = this.labList[idx];
  314. if (!lab) return;
  315. this.selectedLab = lab;
  316. this.showSelectLab = false;
  317. },
  318. // ---- 提交 ----
  319. handleConfirm() {
  320. if (!this.isEnter && !this.selectedLab) {
  321. return uni.showToast({ title: '请选择实验室或勾选无需进入', icon: 'none' });
  322. }
  323. if (this.educationType == 2 && !this.isEnter && !this.reason.trim()) {
  324. return uni.showToast({ title: '请填写进入实验室原因', icon: 'none' });
  325. }
  326. if (!this.checkType) {
  327. return uni.showToast({ title: '请阅读并确认勾选《新生绑定实验室须知及责任说明》', icon: 'none' });
  328. }
  329. uni.showLoading({ title: '提交中...' });
  330. const obj = {
  331. subId: this.isEnter ? '' : (this.selectedLab ? this.selectedLab.subId : ''),
  332. reason: this.reason || '',
  333. isEnter: this.isEnter ? '0' : '1',
  334. };
  335. examElLabEduRelationBind(obj).then(res => {
  336. uni.hideLoading();
  337. const resp = res.data || {};
  338. if (resp.code === 200) {
  339. uni.showToast({ title: resp.message || '绑定成功', icon: 'success' });
  340. this.$emit('bind-success');
  341. this.$emit('close');
  342. } else {
  343. uni.showToast({ title: resp.message || '绑定失败', icon: 'none' });
  344. }
  345. }).catch(() => {
  346. uni.hideLoading();
  347. uni.showToast({ title: '绑定失败', icon: 'none' });
  348. });
  349. },
  350. }
  351. }
  352. </script>
  353. <style lang="stylus" scoped>
  354. .bind-lab-modal
  355. position fixed
  356. top 0
  357. left 0
  358. right 0
  359. bottom 0
  360. z-index 800
  361. display flex
  362. align-items center
  363. justify-content center
  364. .bind-lab-overlay
  365. position absolute
  366. top 0
  367. left 0
  368. right 0
  369. bottom 0
  370. background rgba(0,0,0,0.5)
  371. .no-close
  372. pointer-events none
  373. .bind-lab-content
  374. position relative
  375. z-index 1
  376. background #fff
  377. border-radius 24rpx
  378. width 90%
  379. max-width 680rpx
  380. box-shadow 0 16rpx 64rpx rgba(0,0,0,0.2)
  381. overflow hidden
  382. .modal-header
  383. display flex
  384. align-items center
  385. justify-content space-between
  386. padding 32rpx 40rpx 24rpx
  387. border-bottom 1rpx solid #f0f0f0
  388. flex-shrink 0
  389. .modal-title
  390. font-size 32rpx
  391. font-weight 700
  392. color #1a1a1a
  393. .modal-close
  394. font-size 36rpx
  395. color #999
  396. padding 8rpx
  397. .modal-scroll
  398. overflow hidden
  399. .modal-body
  400. padding 28rpx 36rpx 32rpx
  401. // 无需进入实验室
  402. .no-lab-box
  403. background #f6f8ff
  404. border-radius 16rpx
  405. padding 24rpx 28rpx
  406. margin-bottom 28rpx
  407. .checkbox-row
  408. display flex
  409. align-items center
  410. gap 16rpx
  411. .custom-checkbox
  412. width 36rpx
  413. height 36rpx
  414. border-radius 6rpx
  415. border 2rpx solid #ccc
  416. flex-shrink 0
  417. box-sizing border-box
  418. position relative
  419. .custom-checkbox.checked
  420. background #1677ff
  421. border-color #1677ff
  422. .custom-checkbox.checked::after
  423. content ''
  424. position absolute
  425. top 44%
  426. left 20%
  427. width 10rpx
  428. height 20rpx
  429. border solid #fff
  430. border-width 0 3rpx 3rpx 0
  431. transform rotate(45deg) translate(-50%,-50%)
  432. .no-lab-title
  433. font-size 28rpx
  434. font-weight 600
  435. color #1a1a1a
  436. .no-lab-desc
  437. font-size 22rpx
  438. color #888
  439. margin-top 10rpx
  440. line-height 1.6
  441. padding-left 52rpx
  442. // 选择实验室按钮
  443. .select-lab-btn
  444. display flex
  445. align-items center
  446. border 1rpx solid #d9d9d9
  447. border-radius 12rpx
  448. padding 0 24rpx
  449. height 80rpx
  450. background #fafafa
  451. margin-bottom 12rpx
  452. .select-lab-placeholder
  453. flex 1
  454. font-size 26rpx
  455. color #bbb
  456. .select-lab-value
  457. flex 1
  458. font-size 26rpx
  459. color #333
  460. .select-lab-arrow
  461. font-size 36rpx
  462. color #bbb
  463. margin-left 8rpx
  464. .search-tip
  465. font-size 22rpx
  466. color #1677ff
  467. line-height 1.5
  468. margin-bottom 20rpx
  469. display block
  470. // 实验室信息表
  471. .lab-info-table
  472. border 1rpx solid #e8e8e8
  473. border-radius 12rpx
  474. overflow hidden
  475. margin-bottom 24rpx
  476. .info-row
  477. display flex
  478. border-bottom 1rpx solid #f0f0f0
  479. .info-row.no-border
  480. border-bottom none
  481. .info-cell
  482. padding 18rpx 20rpx
  483. font-size 24rpx
  484. .label-cell
  485. width 160rpx
  486. color #888
  487. background #fafafa
  488. flex-shrink 0
  489. .value-cell
  490. flex 1
  491. color #333
  492. // 原因输入框(本科生)
  493. .reason-box
  494. margin-bottom 20rpx
  495. .reason-input
  496. width 100%
  497. box-sizing border-box
  498. height 120rpx
  499. border 1rpx solid #d9d9d9
  500. border-radius 12rpx
  501. padding 16rpx 20rpx
  502. font-size 26rpx
  503. color #333
  504. background #fafafa
  505. .reason-tip
  506. font-size 22rpx
  507. color #9ba7ba
  508. line-height 1.5
  509. margin-top 8rpx
  510. display block
  511. // 协议
  512. .agree-row
  513. display flex
  514. align-items flex-start
  515. gap 16rpx
  516. margin-bottom 32rpx
  517. margin-top 16rpx
  518. .agree-text
  519. display flex
  520. flex-wrap wrap
  521. font-size 24rpx
  522. color #555
  523. line-height 1.6
  524. .agree-link
  525. color #1677ff
  526. // 底部按钮
  527. .modal-footer
  528. display flex
  529. gap 20rpx
  530. justify-content flex-end
  531. .btn-cancel
  532. padding 16rpx 44rpx
  533. border-radius 12rpx
  534. font-size 26rpx
  535. border 1rpx solid #d9d9d9
  536. background #fff
  537. color #555
  538. .btn-confirm
  539. padding 16rpx 44rpx
  540. border-radius 12rpx
  541. font-size 26rpx
  542. background #1677ff
  543. color #fff
  544. font-weight 600
  545. // 须知弹窗
  546. .notice-modal
  547. position fixed
  548. top 0
  549. left 0
  550. right 0
  551. bottom 0
  552. z-index 900
  553. background rgba(0,0,0,0.5)
  554. display flex
  555. align-items center
  556. justify-content center
  557. .notice-content
  558. background #fff
  559. border-radius 20rpx
  560. width 85%
  561. max-width 640rpx
  562. overflow hidden
  563. .notice-header
  564. padding 28rpx 36rpx
  565. border-bottom 1rpx solid #f0f0f0
  566. .notice-title
  567. font-size 30rpx
  568. font-weight 700
  569. color #1a1a1a
  570. .notice-scroll
  571. height 300rpx
  572. .notice-body
  573. padding 24rpx 36rpx
  574. .notice-p
  575. display block
  576. font-size 26rpx
  577. color #333
  578. line-height 1.8
  579. margin-bottom 12rpx
  580. .notice-footer
  581. display flex
  582. gap 20rpx
  583. justify-content flex-end
  584. padding 20rpx 36rpx
  585. border-top 1rpx solid #f0f0f0
  586. // 选择实验室弹窗
  587. .select-lab-modal
  588. position fixed
  589. top 0
  590. left 0
  591. right 0
  592. bottom 0
  593. z-index 900
  594. background rgba(0,0,0,0.5)
  595. display flex
  596. align-items flex-end
  597. .select-lab-content
  598. background #fff
  599. border-radius 32rpx 32rpx 0 0
  600. width 100%
  601. overflow hidden
  602. .select-lab-header
  603. display flex
  604. align-items center
  605. justify-content space-between
  606. padding 28rpx 36rpx 20rpx
  607. border-bottom 1rpx solid #f0f0f0
  608. .select-lab-title
  609. font-size 30rpx
  610. font-weight 700
  611. color #1a1a1a
  612. .search-bar
  613. padding 16rpx 28rpx
  614. background #fff
  615. .search-input
  616. width 100%
  617. box-sizing border-box
  618. height 76rpx
  619. border 1rpx solid #e0e0e0
  620. border-radius 38rpx
  621. padding 0 28rpx
  622. font-size 26rpx
  623. color #333
  624. background #f5f5f5
  625. .lab-list-scroll
  626. overflow hidden
  627. .lab-list-item
  628. padding 24rpx 36rpx
  629. border-bottom 1rpx solid #f5f5f5
  630. .lab-item-name
  631. display block
  632. font-size 28rpx
  633. color #222
  634. font-weight 600
  635. margin-bottom 6rpx
  636. .lab-item-sub
  637. display block
  638. font-size 22rpx
  639. color #999
  640. .lab-list-empty
  641. text-align center
  642. padding 60rpx 0
  643. color #bbb
  644. font-size 26rpx
  645. .lab-list-loading
  646. text-align center
  647. padding 24rpx 0
  648. color #bbb
  649. font-size 24rpx
  650. </style>