综合办公系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

uv-vtabs.vue 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. <template>
  2. <view
  3. class="uv-vtabs"
  4. :style="[vtabsStyle]"
  5. >
  6. <scroll-view
  7. class="uv-vtabs__bar"
  8. ref="uv-vtabs__bar"
  9. :style="[getBarStyle]"
  10. :scroll-y="barScrollable"
  11. :scroll-x="scrollX"
  12. :show-scrollbar="false"
  13. :scroll-with-animation="true"
  14. :scroll-top="barScrollTop"
  15. :scroll-into-view="barScrollToView"
  16. >
  17. <view
  18. :class="[
  19. 'uv-vtabs__bar-item',
  20. `uv-vtabs__bar-item--${index}`,
  21. index == activeIndex && 'uv-vtabs__bar-item-active'
  22. ]"
  23. :ref="`uv-vtabs__bar-item--${index}`"
  24. v-for="(item,index) in list"
  25. :key="index"
  26. :id="`bar_${index}`"
  27. :style="[itemStyle(index)]"
  28. @tap.stop="clickHandler(index)"
  29. >
  30. <view
  31. class="uv-vtabs__bar-item--line"
  32. v-if="index == activeIndex"
  33. :style="[$uv.addStyle(barItemActiveLineStyle)]"
  34. ></view>
  35. <text
  36. :class="[
  37. 'uv-vtabs__bar-item--value',
  38. index == activeIndex && 'uv-vtabs__bar-item-active--value'
  39. ]"
  40. :style="[itemStyle(index),textStyle(index)]"
  41. >{{item[keyName]}}</text>
  42. <view
  43. class="uv-vtabs__bar-item--badge"
  44. :style="[$uv.addStyle(barItemBadgeStyle)]"
  45. v-if="!!(item.badge && (item.badge.show || item.badge.isDot || item.badge.value))"
  46. >
  47. <uv-badge
  48. :show="!!(item.badge && (item.badge.show || item.badge.isDot || item.badge.value))"
  49. :isDot="item.badge && item.badge.isDot || propsBadge.isDot"
  50. :value="item.badge && item.badge.value || propsBadge.value"
  51. :max="item.badge && item.badge.max || propsBadge.max"
  52. :type="item.badge && item.badge.type || propsBadge.type"
  53. :showZero="item.badge && item.badge.showZero || propsBadge.showZero"
  54. :bgColor="item.badge && item.badge.bgColor || propsBadge.bgColor"
  55. :color="item.badge && item.badge.color || propsBadge.color"
  56. :shape="item.badge && item.badge.shape || propsBadge.shape"
  57. :numberType="item.badge && item.badge.numberType || propsBadge.numberType"
  58. :inverted="item.badge && item.badge.inverted || propsBadge.inverted"
  59. ></uv-badge>
  60. </view>
  61. </view>
  62. </scroll-view>
  63. <scroll-view
  64. class="uv-vtabs__content"
  65. :style="[getContentStyle,$uv.addStyle(contentStyle)]"
  66. :scroll-y="true"
  67. :scroll-x="scrollX"
  68. :show-scrollbar="false"
  69. :scroll-top="contentScrollTop"
  70. :scroll-into-view="contentScrollTo"
  71. :scroll-with-animation="true"
  72. @scroll="scrollHandler"
  73. @scrolltolower="scrolltolower"
  74. v-if="chain"
  75. >
  76. <slot />
  77. </scroll-view>
  78. <scroll-view
  79. v-else
  80. class="uv-vtabs__content"
  81. :style="[getContentStyle,$uv.addStyle(contentStyle)]"
  82. :scroll-y="true"
  83. :scroll-x="scrollX"
  84. :show-scrollbar="false"
  85. :scroll-top="contentScrollTop2"
  86. @scrolltolower="scrolltolower"
  87. >
  88. <slot />
  89. </scroll-view>
  90. </view>
  91. </template>
  92. <script>
  93. import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
  94. import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
  95. import debounce from '@/uni_modules/uv-ui-tools/libs/function/debounce.js'
  96. import throttle from '@/uni_modules/uv-ui-tools/libs/function/throttle.js'
  97. import uvBadgeProps from '@/uni_modules/uv-badge/components/uv-badge/props.js'
  98. import props from './props.js';
  99. // #ifdef APP-NVUE
  100. const dom = uni.requireNativePlugin('dom')
  101. // #endif
  102. /**
  103. * 垂直选项卡
  104. * @description 该组件兼容所有端,提供了分类展示和联动等功能
  105. * @tutorial https://www.uvui.cn/components/vtabs.html
  106. * @property {Array} list 选项数组,元素为对象,如[{name:'uv-ui'}](默认 [] )
  107. * @property {String} keyName 从list元素对象中读取的键名(默认 name )
  108. * @property {Number} current 当前选中项,从0开始(默认 0 )
  109. * @property {Number | String} hdHeight 头部内容的高度,头部有内容必传,否则会有联动误差(默认 0 )
  110. * @property {Boolean} chain 是否开启联动,开启后右边区域可以滑动查看内容(默认 true )
  111. * @property {Number|String} height 整个列表的高度,默认auto或空则为屏幕高度(默认 auto屏幕高度 )
  112. * @property {Number|String} barWidth 左边选项区域的宽度(默认 180rpx )
  113. * @property {Boolean} barScrollable 左边选项区域是否允许滚动 (默认 true )
  114. * @property {String} barBgColor 左边选项区域的背景颜色(默认$uv-bg-color)
  115. * @property {Object} barStyle 左边选项区域的自定义样式 (默认{})
  116. * @property {Object} barItemStyle 左边选项区域每个选项的自定义样式 (默认{})
  117. * @property {Object} barItemActiveStyle 左边选项区域选中选项的自定义样式 (默认{})
  118. * @property {Object} barItemActiveLineStyle 左边选项区域选中选项竖线条的自定义样式 (默认{})
  119. * @property {Object} barItemBadgeStyle 左边选项区域选中选项徽标的自定义样式,主要用于设置位置 (默认{})
  120. * @property {Object} contentStyle 右边区域自定义样式 (默认{})
  121. * @example <uv-vtabs :list="list"><uv-vtabs-item>...</uv-vtabs-item></uv-vtabs>
  122. */
  123. export default {
  124. name: 'uv-vtabs',
  125. mixins: [mpMixin, mixin, props],
  126. created() {
  127. this.children = []
  128. },
  129. mounted() {
  130. this.$nextTick(()=>{
  131. this.init(this.current);
  132. })
  133. },
  134. data() {
  135. return {
  136. activeIndex: 0,
  137. // 微信小程序下,scroll-view的scroll-into-view属性无法对slot中的内容的id生效,只能通过设置scrollTop的形式去移动滚动条
  138. contentScrollTop: 0,
  139. contentScrollTop2: 0,//针对非联动
  140. contentScrollTo: '',
  141. scrolling: false,
  142. barScrolling: false,
  143. touching: false,
  144. hasHeight: 0,
  145. scrollViewHeight: 0,
  146. barScrollTop: 0,
  147. barScrollToView: '',
  148. timer2: 0
  149. }
  150. },
  151. computed: {
  152. scrollX(){
  153. // #ifdef APP-NVUE
  154. return true;
  155. // #endif
  156. return false;
  157. },
  158. vtabsStyle() {
  159. const style = {};
  160. style.height = this.getHeight();
  161. return this.$uv.deepMerge(style, this.$uv.addStyle(this.customStyle));
  162. },
  163. getBarStyle() {
  164. const style = {};
  165. style.width = this.$uv.getPx(this.barWidth, true);
  166. style.background = this.barBgColor;
  167. style.height = this.getHeight();
  168. return this.$uv.deepMerge(style, this.$uv.addStyle(this.barStyle));
  169. },
  170. itemStyle(){
  171. return index =>{
  172. const style = {};
  173. let barItemInitStyle = this.barItemStyle;
  174. // 避免在nvue模式下,切换时候上一个选中颜色不变
  175. if(this.barItemStyle && !this.barItemStyle?.background) {
  176. barItemInitStyle.background = 'transparent';
  177. }
  178. // 是否激活的样式
  179. const customeStyle = index === this.activeIndex ? this.$uv.addStyle(this.barItemActiveStyle) : this.$uv.addStyle(barItemInitStyle);
  180. if (this.list[index].disabled) {
  181. style.color = '#c8c9cc'
  182. }
  183. return this.$uv.deepMerge(style, customeStyle);
  184. }
  185. },
  186. // nvue设置字体样式必须要text标签上进行
  187. textStyle(){
  188. return index=>{
  189. const style = {};
  190. style.width = this.$uv.getPx(this.barWidth, true);
  191. return style;
  192. }
  193. },
  194. getContentStyle() {
  195. const style = {};
  196. style.height = this.getHeight();
  197. return style;
  198. },
  199. propsBadge() {
  200. return uvBadgeProps
  201. }
  202. },
  203. watch: {
  204. current(newVal){
  205. if(!this.touching)
  206. this.$nextTick(()=>{
  207. this.init(newVal?newVal:0);
  208. })
  209. },
  210. list(newVal) {
  211. if (newVal.length) {
  212. this.$uv.sleep(30).then(res => {
  213. this.resize();
  214. })
  215. }
  216. },
  217. activeIndex(newVal){
  218. if(!this.chain) {// 解决:非联动,内容过多的情况,滚动一段距离,再切换未滚动到顶部的BUG
  219. this.contentScrollTop2 = 0 - Math.random() * 4 - 4;
  220. }
  221. this.$emit('change',newVal);
  222. }
  223. },
  224. methods: {
  225. init(index){
  226. let num = 0;
  227. clearInterval(this.timer2);
  228. this.timer2 = setInterval(async ()=>{
  229. num++;
  230. if(num>50) clearInterval(this.timer2);
  231. if(this.children.length) {
  232. clearInterval(this.timer2);
  233. await this.$uv.sleep(300);
  234. this.clickHandler(index);
  235. }
  236. },100)
  237. },
  238. // 内容滚动到底部触发
  239. scrolltolower(){
  240. this.$emit('scrolltolower',this.activeIndex);
  241. },
  242. async resize() {
  243. // 如果list数组长度为0就不处理 || 选中目标未变则不处理
  244. if (this.list.length == 0 || !this.barScrollable) return;
  245. // 避免滑太快,修复位置
  246. Promise.all([this.getTabsRect(), this.getAllItemRect()]).then(([tabsRect, itemRect = []]) => {
  247. this.tabsRect = tabsRect;
  248. this.scrollViewHeight = 0
  249. itemRect.map((item, index) => {
  250. this.scrollViewHeight += item.height;
  251. this.list[index].rect = item;
  252. })
  253. this.setBarScrollTop();
  254. })
  255. },
  256. // 设置左边菜单滚动条的位置,目标:将当前的选项移到中间位置
  257. setBarScrollTop() {
  258. const tabRect = this.list[this.activeIndex];
  259. const offsetTop = this.list
  260. .slice(0, this.activeIndex)
  261. .reduce((total, item) => {
  262. return total + item.rect.height;
  263. }, 0);
  264. const scrollViewHeight = this.$uv.getPx(this.getHeight());
  265. let barScrollTop = tabRect.rect.height / 2 + offsetTop - scrollViewHeight / 2;
  266. // 先给一点随机值,避免出现不能滚动的BUG
  267. barScrollTop = Math.min(barScrollTop, this.scrollViewHeight - this.tabsRect.height);
  268. this.barScrollTop = Math.max(0, barScrollTop);
  269. // 已经不能滚动的时候,就使用scroll-into-view的方式进行定位,避免失效
  270. if(barScrollTop>=(this.scrollViewHeight - this.tabsRect.height)) {
  271. this.timer && clearTimeout(this.timer);
  272. this.timer = setTimeout(()=>{
  273. this.barScrollToView = `bar_${this.activeIndex}`;
  274. },400)
  275. }
  276. },
  277. // 左边菜单点击
  278. async clickHandler(currentIndex) {
  279. if (currentIndex == this.activeIndex) return;
  280. this.touching = true;
  281. this.activeIndex = currentIndex;
  282. if(this.chain) {
  283. // 给一点随机值,避免出现不能滚动的BUG。微信端必须用此方法
  284. this.contentScrollTop = this.children[currentIndex].top - this.$uv.getPx(this.hdHeight) - Math.random() * 4 - 4;
  285. // #ifndef MP-WEIXIN
  286. this.contentScrollTo = `content_${currentIndex}`;
  287. // #endif
  288. }
  289. this.timer && clearTimeout(this.timer);
  290. throttle(()=>{
  291. this.resize();
  292. },300,false)
  293. debounce(() => {
  294. this.touching = false;
  295. }, 900)
  296. },
  297. // 内容滚动
  298. scrollHandler(e) {
  299. if (this.touching || this.scrolling) return;
  300. // 每过一定时间取样一次,减少资源损耗以及可能带来的卡顿
  301. this.scrolling = true;
  302. this.$uv.sleep(80).then(() => {
  303. this.scrolling = false;
  304. })
  305. const scrollTop = e.detail.scrollTop;
  306. let children = this.children;
  307. const len = children.length;
  308. let top = 0;
  309. let activeIndex = 0;
  310. children = this.children.map((item, index) => {
  311. if (item.height > 0) this.hasHeight = item.height;
  312. item.height = item.height > 0 ? item.height : this.hasHeight;
  313. const child = {
  314. height: item.height,
  315. top
  316. }
  317. // 进行累加,给下一个item提供计算依据
  318. top += item.height;
  319. return child;
  320. })
  321. for (let i = 0; i < len; i++) {
  322. const item = children[i];
  323. const nextItem = children[i + 1];
  324. // 如果滚动条高度小于第一个item的top值,此时无需设置任意字母为高亮
  325. if (scrollTop <= children[0].top) {
  326. activeIndex = 0;
  327. break
  328. } else if (!nextItem) {
  329. // 当不存在下一个item时,意味着历遍到了最后一个
  330. activeIndex = len - 1;
  331. break
  332. } else if (scrollTop > item.top && scrollTop < nextItem.top) {
  333. activeIndex = i;
  334. break
  335. }
  336. }
  337. this.activeIndex = activeIndex;
  338. // 当前选中项索引必然来源于前后两个索引,满足才执行,避免闪烁的bug
  339. this.timer4 && clearTimeout(this.timer4);
  340. this.timer4 = setTimeout(()=>{
  341. this.resize();
  342. },100)
  343. },
  344. // 设置高度
  345. getHeight() {
  346. let height = 0;
  347. const isEmpty = this.$uv.test.empty(this.height);
  348. if (isEmpty || this.height=='auto') height = this.$uv.addUnit(this.$uv.sys().windowHeight);
  349. else height = this.$uv.getPx(this.height, true);
  350. return height;
  351. },
  352. // 获取导航菜单的尺寸
  353. getTabsRect() {
  354. return new Promise(resolve => {
  355. this.queryRect('uv-vtabs__bar').then(size => resolve(size))
  356. })
  357. },
  358. // 获取所有标签的尺寸
  359. getAllItemRect() {
  360. return new Promise(resolve => {
  361. const promiseAllArr = this.list.map((item, index) => this.queryRect(
  362. `uv-vtabs__bar-item--${index}`, true))
  363. Promise.all(promiseAllArr).then(sizes => resolve(sizes))
  364. })
  365. },
  366. // 获取各个标签的尺寸
  367. queryRect(el, item) {
  368. // #ifndef APP-NVUE
  369. // $uvGetRect为uv-ui自带的节点查询简化方法,详见文档介绍:https://www.uvui.cn/js/getRect.html
  370. // 组件内部一般用this.$uvGetRect,对外的为getRect,二者功能一致,名称不同
  371. return new Promise(resolve => {
  372. this.$uvGetRect(`.${el}`).then(size => {
  373. resolve(size)
  374. })
  375. })
  376. // #endif
  377. // #ifdef APP-NVUE
  378. // nvue下,使用dom模块查询元素高度
  379. // 返回一个promise,让调用此方法的主体能使用then回调
  380. return new Promise(resolve => {
  381. dom.getComponentRect(item ? this.$refs[el][0] : this.$refs[el], res => {
  382. resolve(res.size)
  383. })
  384. })
  385. // #endif
  386. }
  387. }
  388. }
  389. </script>
  390. <style scoped lang="scss">
  391. @import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
  392. @import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
  393. .uv-vtabs {
  394. @include flex;
  395. &__bar {
  396. background: $uv-bg-color;
  397. &-item {
  398. position: relative;
  399. @include flex;
  400. align-items: center;
  401. justify-content: center;
  402. padding: 35rpx 12rpx 35rpx 20rpx;
  403. &--value {
  404. /* #ifdef APP-NVUE */
  405. padding: 0 12rpx;
  406. /* #endif */
  407. font-size: 14px;
  408. color: $uv-content-color;
  409. }
  410. &-active {
  411. background: #fff;
  412. &--value {
  413. color: $uv-primary;
  414. }
  415. }
  416. &--line {
  417. position: absolute;
  418. width: 2px;
  419. left: 0;
  420. top: 0;
  421. bottom: 0;
  422. z-index: 1;
  423. background-color: $uv-primary;
  424. }
  425. &--badge {
  426. position: absolute;
  427. top: 4px;
  428. right: 10px;
  429. z-index: 1;
  430. }
  431. }
  432. }
  433. &__content {
  434. flex: 1;
  435. background: #fff;
  436. }
  437. }
  438. </style>