Class.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. <template>
  2. <view>
  3. <view class="u-wrap">
  4. <view class="message flex_c_c">提示:仅可选择已资质认证通过的服务</view>
  5. <view class="u-menu-wrap">
  6. <scroll-view scroll-y scroll-with-animation class="u-tab-view menu-scroll-view" :scroll-top="scrollTop"
  7. :scroll-into-view="itemId">
  8. <view v-for="(item, index) in tabbar" :key="index" class="u-tab-item"
  9. :class="[current == index ? 'u-tab-item-active' : '']" @tap.stop="swichMenu(index)">
  10. <text class="u-line-1">{{ item.businessName }}</text>
  11. <view v-if="activesParentIds[item.id]" class="u-tab-item-radio flex_c_c">{{
  12. activesParentIds[item.id].length }}</view>
  13. </view>
  14. </scroll-view>
  15. <scroll-view :scroll-top="scrollRightTop" scroll-y scroll-with-animation class="right-box"
  16. @scroll="rightScroll">
  17. <view class="page-view">
  18. <view class="class-item" :id="'item' + index" v-for="(item, index) in tabbar" :key="index">
  19. <view class="item-title">
  20. <text class="item-businessName">{{ item.businessName }}</text>
  21. <text class="item-title-text">{{ item.businessDescribe }}</text>
  22. </view>
  23. <!-- thumb-box-active -->
  24. <view class="item-container">
  25. <view v-for="(item1, index1) in (item.children ? item.children : [item])" :key="index1"
  26. :class="[activesIds.includes(item1.id) ? `thumb-box ${item1.id === activeItem.id && 'thumb-box-active'}` : 'thumb-box-disable']"
  27. @click="clickMenu(item1)">
  28. <image class="item-menu-image" :src="item1.businessIcon"></image>
  29. <view class="item-menu-name">{{ item1.businessName }}</view>
  30. </view>
  31. </view>
  32. </view>
  33. </view>
  34. </scroll-view>
  35. </view>
  36. </view>
  37. </view>
  38. </template>
  39. <script>
  40. import { getTreeList } from '@/api/volunteer'
  41. import { volunteerSeachgetTreeList } from "@/api/volunteerDetailsApi/details.js"
  42. import { getVolunteerInfo } from '@/api/volunteer.js'
  43. import CustomTabBar from '@/components/CustomTabBar/index.vue'
  44. import { computed } from 'vue'
  45. export default {
  46. components: { CustomTabBar },
  47. data() {
  48. return {
  49. scrollTop: 0, //tab标题的滚动条位置
  50. oldScrollTop: 0,
  51. current: 0, // 预设当前项的值
  52. menuHeight: 0, // 左边菜单的高度
  53. menuItemHeight: 0, // 左边菜单item的高度
  54. itemId: '', // 栏目右边scroll-view用于滚动的id
  55. tabbar: [],
  56. arr: [],
  57. scrollRightTop: 0, // 右边栏目scroll-view的滚动条高度
  58. timer: null, // 定时器
  59. userType: uni.getStorageSync('userType') || 1,
  60. activeList: [
  61. {
  62. id: '13',
  63. businessName: '居家陪伴',
  64. parentId: '1',
  65. isFiles: true,//上传文件
  66. },
  67. {
  68. id: '14',
  69. businessName: '出行陪伴',
  70. parentId: '1',
  71. isFiles: false,//上传文件
  72. }
  73. ],
  74. activeItem: {
  75. id:null
  76. },
  77. //选中的项
  78. }
  79. },
  80. computed: {
  81. activesIds() {
  82. return this.activeList.map(item => item.id);
  83. },
  84. activesParentIds() {
  85. const result = this.activeList.reduce((acc, item) => {
  86. const { parentId } = item;
  87. if (!acc[parentId]) {
  88. acc[parentId] = [];
  89. }
  90. acc[parentId].push(item.id); // 推入整个对象
  91. return acc;
  92. }, {});
  93. return result;
  94. }
  95. },
  96. onReady() {
  97. },
  98. onShow() {
  99. this.getData();
  100. this.getMenuItemTop()
  101. },
  102. onLoad(options) {
  103. try {
  104. const data = decodeURIComponent(options.data)
  105. this.activeList = data ? JSON.parse(data) : [];
  106. if(options.value){
  107. const isValue = decodeURIComponent(options.value)
  108. this.activeItem = isValue ? JSON.parse(isValue) : {id:null};
  109. }
  110. } catch (error) {
  111. console.log("TCL: onLoad -> error", error)
  112. }
  113. },
  114. methods: {
  115. deleteActiveList(id) {
  116. this.activeList = this.activeList.filter(item => item.id !== id);
  117. },
  118. getData() {
  119. getTreeList({ parentId: '0' }).then(res => {
  120. this.tabbar = res.data;
  121. })
  122. },
  123. // 点击左边的栏目切换
  124. async swichMenu(index) {
  125. if (this.arr.length == 0) {
  126. await this.getMenuItemTop();
  127. }
  128. if (index == this.current) return;
  129. this.scrollRightTop = this.oldScrollTop;
  130. this.$nextTick(() => {
  131. this.scrollRightTop = this.arr[index];
  132. this.current = index;
  133. this.leftMenuStatus(index);
  134. })
  135. },
  136. // 获取一个目标元素的高度
  137. getElRect(elClass, dataVal) {
  138. new Promise((resolve, reject) => {
  139. const query = uni.createSelectorQuery().in(this);
  140. query.select('.' + elClass).fields({
  141. size: true
  142. }, res => {
  143. // 如果节点尚未生成,res值为null,循环调用执行
  144. if (!res) {
  145. setTimeout(() => {
  146. this.getElRect(elClass);
  147. }, 10);
  148. return;
  149. }
  150. this[dataVal] = res.height;
  151. resolve();
  152. }).exec();
  153. })
  154. },
  155. // 观测元素相交状态
  156. async observer() {
  157. this.tabbar.map((val, index) => {
  158. let observer = uni.createIntersectionObserver(this);
  159. // 检测右边scroll-view的id为itemxx的元素与right-box的相交状态
  160. // 如果跟.right-box底部相交,就动态设置左边栏目的活动状态
  161. observer.relativeTo('.right-box', {
  162. top: 0
  163. }).observe('#item' + index, res => {
  164. if (res.intersectionRatio > 0) {
  165. let id = res.id.substring(4);
  166. this.leftMenuStatus(id);
  167. }
  168. })
  169. })
  170. },
  171. // 设置左边菜单的滚动状态
  172. async leftMenuStatus(index) {
  173. this.current = index;
  174. // 如果为0,意味着尚未初始化
  175. if (this.menuHeight == 0 || this.menuItemHeight == 0) {
  176. await this.getElRect('menu-scroll-view', 'menuHeight');
  177. await this.getElRect('u-tab-item', 'menuItemHeight');
  178. }
  179. // 将菜单活动item垂直居中
  180. this.scrollTop = index * this.menuItemHeight + this.menuItemHeight / 2 - this.menuHeight / 2;
  181. },
  182. // 获取右边菜单每个item到顶部的距离
  183. getMenuItemTop() {
  184. new Promise(resolve => {
  185. let selectorQuery = uni.createSelectorQuery();
  186. selectorQuery.selectAll('.class-item').boundingClientRect((rects) => {
  187. // 如果节点尚未生成,rects值为[](因为用selectAll,所以返回的是数组),循环调用执行
  188. if (!rects.length) {
  189. setTimeout(() => {
  190. this.getMenuItemTop();
  191. }, 10);
  192. return;
  193. }
  194. rects.forEach((rect) => {
  195. // 这里减去rects[0].top,是因为第一项顶部可能不是贴到导航栏(比如有个搜索框的情况)
  196. this.arr.push(rect.top - rects[0].top);
  197. resolve();
  198. })
  199. }).exec()
  200. })
  201. },
  202. // 右边菜单滚动
  203. async rightScroll(e) {
  204. this.oldScrollTop = e.detail.scrollTop;
  205. if (this.arr.length == 0) {
  206. await this.getMenuItemTop();
  207. }
  208. if (this.timer) return;
  209. if (!this.menuHeight) {
  210. await this.getElRect('menu-scroll-view', 'menuHeight');
  211. }
  212. setTimeout(() => { // 节流
  213. this.timer = null;
  214. // scrollHeight为右边菜单垂直中点位置
  215. let scrollHeight = e.detail.scrollTop + this.menuHeight / 2;
  216. for (let i = 0; i < this.arr.length; i++) {
  217. let height1 = this.arr[i];
  218. let height2 = this.arr[i + 1];
  219. // 如果不存在height2,意味着数据循环已经到了最后一个,设置左边菜单为最后一项即可
  220. if (!height2 || scrollHeight >= height1 && scrollHeight < height2) {
  221. this.leftMenuStatus(i);
  222. return;
  223. }
  224. }
  225. }, 10)
  226. },
  227. async clickMenu(record) {
  228. try {
  229. if(this.activesIds.includes(record.id)){
  230. this.activeItem = record
  231. this.onSubmit();
  232. return
  233. }
  234. uni.showToast({
  235. title: '仅可选择已资质认证通过的服务',
  236. icon: 'none',
  237. });
  238. } catch (error) {
  239. console.log("TCL: clickMenu -> error", error)
  240. }
  241. },
  242. onSubmit() {
  243. try {
  244. const pages = getCurrentPages();
  245. const prevPage = pages[pages.length - 2];
  246. if (prevPage && prevPage.$vm) {
  247. // 假设上一页有一个方法叫 handleReturnData
  248. prevPage.$vm.activeProjectBack({
  249. serves: encodeURIComponent(JSON.stringify(this.activeItem)),
  250. });
  251. }
  252. uni.navigateBack({
  253. delta: 1
  254. });
  255. } catch (error) {
  256. console.log("TCL: onSubmit -> error", error)
  257. }
  258. }
  259. }
  260. }
  261. </script>
  262. <style lang="scss" scoped>
  263. .u-wrap {
  264. display: flex;
  265. flex-direction: column;
  266. position: fixed;
  267. top: 0px;
  268. left: 0px;
  269. right: 0px;
  270. // bottom: 150rpx;
  271. bottom: 0;
  272. // padding-bottom: 150px;
  273. }
  274. .u-search-box {
  275. padding: 18rpx 30rpx;
  276. }
  277. .u-menu-wrap {
  278. flex: 1;
  279. display: flex;
  280. overflow: hidden;
  281. }
  282. .u-search-inner {
  283. background-color: rgb(234, 234, 234);
  284. border-radius: 100rpx;
  285. display: flex;
  286. align-items: center;
  287. padding: 10rpx 16rpx;
  288. }
  289. .u-search-text {
  290. font-size: 26rpx;
  291. }
  292. .u-tab-view {
  293. width: 250rpx;
  294. height: 100%;
  295. }
  296. .u-tab-item {
  297. height: 100rpx;
  298. background: #f6f6f6;
  299. box-sizing: border-box;
  300. display: flex;
  301. align-items: center;
  302. justify-content: center;
  303. font-family: PingFang SC;
  304. font-size: 26rpx;
  305. font-weight: 500;
  306. line-height: 42rpx;
  307. text-align: center;
  308. letter-spacing: normal;
  309. color: #818181;
  310. border-left: 4rpx solid #fff;
  311. position: relative;
  312. }
  313. .u-tab-item-radio {
  314. width: 32rpx;
  315. height: 32rpx;
  316. padding: 8rpx;
  317. border-radius: 50%;
  318. background: #F53F3F;
  319. position: absolute;
  320. right: 0px;
  321. font-size: 24rpx;
  322. color: #FFFFFF;
  323. }
  324. .u-tab-item-active {
  325. position: relative;
  326. background: #fff;
  327. }
  328. .u-tab-item-active::before {
  329. border-left: 4rpx solid rgba(221, 94, 69, 1) !important;
  330. }
  331. .u-tab-item-active::before {
  332. content: "";
  333. position: absolute;
  334. border-left: 4px solid $u-primary;
  335. height: 100%;
  336. left: -5rpx;
  337. top: 0;
  338. }
  339. .u-tab-view {
  340. height: 100%;
  341. background: #F6F6F6;
  342. }
  343. .right-box {
  344. background-color: rgb(250, 250, 250);
  345. }
  346. .page-view {
  347. // padding: 16rpx;
  348. }
  349. .class-item {
  350. // margin-bottom: 30rpx;
  351. background-color: #fff;
  352. padding: 34rpx;
  353. border-radius: 8rpx;
  354. }
  355. .class-item:last-child {
  356. min-height: 100vh;
  357. }
  358. .item-title {
  359. // font-size: 26rpx;
  360. // color: $u-main-color;
  361. // font-weight: bold;
  362. // text-align: center;
  363. display: flex;
  364. align-items: center;
  365. justify-content: space-between;
  366. border-radius: 14rpx;
  367. background: linear-gradient(180deg, #FFF9F3 0%, rgba(255, 255, 255, 0) 100%);
  368. }
  369. .item-businessName {
  370. font-family: PingFang SC;
  371. font-size: 26rpx;
  372. font-weight: 600;
  373. line-height: 42rpx;
  374. letter-spacing: normal;
  375. color: #262626;
  376. }
  377. .item-title-text {
  378. font-family: Yuppy SC;
  379. font-size: 24rpx;
  380. font-weight: 500;
  381. line-height: 42rpx;
  382. text-align: right;
  383. letter-spacing: normal;
  384. color: #E58182;
  385. }
  386. .item-menu-name {
  387. font-family: PingFang SC;
  388. font-size: 26rpx;
  389. font-weight: 500;
  390. line-height: 42rpx;
  391. text-align: center;
  392. letter-spacing: normal;
  393. color: #5D5D5D;
  394. }
  395. .item-container {
  396. // display: flex;
  397. // flex-wrap: wrap;
  398. display: grid;
  399. grid-template-columns: repeat(3, 1fr);
  400. gap: 12rpx;
  401. margin: 24rpx 0;
  402. }
  403. .thumb-box {
  404. // width: 33.333333%;
  405. display: flex;
  406. align-items: center;
  407. justify-content: center;
  408. flex-direction: column;
  409. padding: 12rpx 0;
  410. }
  411. .thumb-box-disable {
  412. @extend .thumb-box;
  413. .item-menu-image::before {
  414. content: '';
  415. width: 138rpx;
  416. height: 138rpx;
  417. position: absolute;
  418. left: 0px;
  419. top: 0px;
  420. background: rgba(216, 216, 216, 0.6);
  421. z-index: 2;
  422. border-radius: 50%;
  423. }
  424. .item-menu-name {
  425. color: rgba(93, 93, 93, 0.5);
  426. }
  427. }
  428. .thumb-box-active {
  429. .item-menu-image::before {
  430. content: '';
  431. width: 138rpx;
  432. height: 138rpx;
  433. position: absolute;
  434. left: 0px;
  435. top: 0px;
  436. background: rgba(216, 216, 216, 0.6);
  437. z-index: 2;
  438. border-radius: 50%;
  439. }
  440. .item-menu-image::after {
  441. content: '';
  442. width: 138rpx;
  443. height: 138rpx;
  444. position: absolute;
  445. left: 0px;
  446. top: 0px;
  447. z-index: 3;
  448. background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAcCAYAAAAAwr0iAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAKOSURBVEiJxZVPSFRRFId/59zXH8JFS4sIA3MGdCO4KEo4zhC4CqMWLQrcii1ql7ugRS5aNNDCZSJBEdFSwXpeMDFwkYGjDgS1MHHRwoXQVPPuadE8m3nMc3zjON3d3HvP+b53z5l7Cf9hfBE5/pP5IQEF02r4qsgFZZ4mwlUQnfVaBZ4T8U4xPwLhLgAPAFR1kloBXxPpYGNeAdoXzqliJO37E3zY8EI2O8SGP0bg99K+PwEAhyqwlsk8APQNgJMR+JPw96H0QF6kzTM8BWCocl6BXCUcAJreA3mR9iPMs0roqV7Rl6l3czej+5tagoJI2mNejMJVdeFooMO1YpomUBC5DMPzIHREljYCpzfOWVusFdeUEpTh0wDaIktFCtzFLmuX42J3mzAv0naUedRzLhdnmxAOUozsBQfKJciLdHrMS44w/suY+U8iZw4KB3Syy/ef1cvB61kZ9AwvgZAuB/YdM7y0JiKNwkmxUgr0zn4+ggk0jIqLAgAIaCfDs4VsdrRW0KpIT/yXowTnbndbu7Mvgc1Abyn0dXSBAA/Qp4VsZiovsgv6LNLJhmdj4HCKsXp1j3DKL5WhFwS6HrNt2QXBtSKwfYJ58V+5osnow7cg6B+wtpRIIJQ4zTQDomzM3u8Afa18VCKjiMD1pqxd3y+8SgD4+1c0TDNEdClJEgCAYizl++NJw6puwm5rdwKng6q6kCQJKVY2nXucFA7E3ISJT4Jcf+qtfd+IQM23IDwJUqzUS6DQ543CYwVCid/OXYFir6YqmkDvNwrfU6AssVVybiBOQhW589ZuHJpAHYntH84l7vrEAlUSwFY45xS5Xmu3WyIQSiDYlSg65yYOCm9oFETS65nMgY8+HH8AWxQUPX3zeH8AAAAASUVORK5CYII=);
  449. background-position: center;
  450. background-size: 32rpx auto;
  451. background-repeat: no-repeat;
  452. }
  453. .item-menu-name {
  454. color: #FF6E51;
  455. }
  456. }
  457. .item-menu-image {
  458. width: 138rpx;
  459. height: 138rpx;
  460. margin-bottom: 12rpx;
  461. }
  462. .message {
  463. background: #FF7D00;
  464. padding: 16rpx;
  465. font-family: PingFang SC;
  466. font-size: 28rpx;
  467. font-weight: normal;
  468. line-height: 39.2rpx;
  469. text-align: center;
  470. letter-spacing: normal;
  471. color: #FFFFFF;
  472. }
  473. .class-footer {
  474. position: fixed;
  475. bottom: 0rpx;
  476. left: 0px;
  477. right: 0;
  478. z-index: 999;
  479. background: #fff;
  480. padding-bottom: 60rpx;
  481. .footer-title {
  482. padding: 38rpx 28rpx;
  483. }
  484. .footer-btns {
  485. display: grid;
  486. grid-template-columns: repeat(3, 1fr); // 创建 3 列,每列等宽
  487. gap: 10rpx; // 设置列与行之间的间距
  488. padding: 0 26rpx 48rpx;
  489. .footer-btn {
  490. border-radius: 66rpx;
  491. box-sizing: border-box;
  492. border: 2rpx solid #FF6E51;
  493. padding: 16rpx 16rpx 16rpx 36rpx;
  494. font-size: 30rpx;
  495. font-weight: 500;
  496. color: #FF6E51;
  497. display: flex;
  498. align-items: center;
  499. gap: 14rpx;
  500. .footer-class-close {
  501. width: 34rpx;
  502. height: 34rpx;
  503. background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAfCAYAAAAfrhY5AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAALgSURBVEiJ5ZdNS9xAGMfnzRBB0UJhdw/DIriga8B63BRLbz31Yl08iIUc21IQP8F+glLoocehUg8S62cIlcabVMhFVvCwUHbBg16C7M5LL91lk0xi1t1Dwf8pmZknv38yz0yeAeCxCo4awBibLxQK60KIBQgh4pxLwzCuOp3OieM4NxOHM8bM2dnZ90qpnV6v9yxt3NTU1G+M8f7t7e1Xx3Huxoa7rrsthPgipXySxygAAGCMrxFCu/V6/eBB8EajQSzLOux2uxt5oXEZhnEcBMFWo9HgWpNp4Gq16vV6vVcPBQMAgBBiuVQqvaxWq989z5PxfqQLsizrkHP+fBxwX91u94VlWYe6vgTcdd3tcT51ioEN13W34+2ROWeMmdPT039GSa68whhfh2FIh1dB5M1nZmY+pIEhhMC2bUAp1T6cUgps2wYQ6nNYCPF0bm7u3XBbBK6U2klzXqvVQLlc1hrog8vlMqjVammPAEKIt1o4Y2yec76aFthqtYCUEiCEIgb6YIQQkFKCVquVCuecrzLG5hPwQqGwnhr1D+77fsTA2tpaBOz7fiZcKQWLxaKdgAshFrLgOgNLS0u5wX1xzhcTcAihds3rDDSbzUhbs9nMBY5zBhdKqcQOpBOlFFQqlUhbpVJJXQVxDXMGcIzxVR7w8BxfXFxokzBLhJDLBLzT6ZyMAvZ9H5ydnSWSMMsAhFC1220/AXcc54YQcp4F1yVXPAmz4ISQ8+GCg8Q6v3HOP+kCT09PB7B4cvUNUEoH43TCGO8P3/8/e7vjOHcY44+TBgMAAEJoN15aaf8CR0dHPyb5WzUM43hzc/NNwpBucBAEW4SQXxMC/wyCYEvXpy2jPM+TKysr+6VSyRJCLI8BPg6C4HVaDZerepVSfhZCPM0LzVu93ruf1+v1gzAMqWmae1n7AIRQEULOTdPcC8OQ3gcG4IEnlmKxaHPOFyGESCklCSGX7XbbH/XE8nj1FxvKd+/WNl3DAAAAAElFTkSuQmCC);
  504. background-position: center;
  505. background-size: 34rpx auto;
  506. background-repeat: no-repeat;
  507. }
  508. }
  509. }
  510. .footer-submit-box {
  511. .footer-submit {
  512. border-radius: 14rpx;
  513. background: linear-gradient(180deg, #FD8F7C -75%, #FE534B 140%);
  514. width: 580rpx;
  515. height: 80rpx;
  516. font-family: PingFang SC;
  517. font-size: 30rpx;
  518. font-weight: 500;
  519. line-height: 42rpx;
  520. letter-spacing: normal;
  521. color: #FFFFFF;
  522. }
  523. }
  524. }
  525. .footer-text {
  526. font-size: 30rpx;
  527. font-weight: 500;
  528. line-height: 30rpx;
  529. color: rgba(0, 0, 0, 0.9);
  530. }
  531. </style>