cl-tabs.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. <template>
  2. <view class="cl-tabs" :class="[classList]">
  3. <view
  4. class="cl-tabs__header"
  5. :style="{
  6. top: stickyTop,
  7. }"
  8. >
  9. <scroll-view
  10. class="cl-tabs__bar"
  11. scroll-with-animation
  12. scroll-x
  13. :scroll-left="scrollLeft"
  14. :style="[style]"
  15. >
  16. <view
  17. class="cl-tabs__bar-box"
  18. :style="{
  19. 'justify-content': justify,
  20. }"
  21. >
  22. <view
  23. class="cl-tabs__bar-item"
  24. v-for="(item, index) in tabs"
  25. :key="index"
  26. :style="{
  27. color: value === item.name ? color : unColor,
  28. padding: `0 ${gutter}rpx`,
  29. }"
  30. :class="{
  31. 'is-active': value === item.name,
  32. }"
  33. @tap="change(index)"
  34. >
  35. <!-- 前缀图标 -->
  36. <text
  37. v-if="item.prefixIcon"
  38. class="cl-tabs__icon cl-tabs__icon--prefix"
  39. :class="[item.prefixIcon]"
  40. ></text>
  41. <!-- 文本内容 -->
  42. <text class="cl-tabs__label">{{ item.label }}</text>
  43. <!-- 后缀图标 -->
  44. <text
  45. v-if="item.suffixIcon"
  46. class="cl-tabs__icon cl-tabs__icon--suffix"
  47. :class="[item.suffixIcon]"
  48. ></text>
  49. </view>
  50. <!-- 选中样式 -->
  51. <view
  52. class="cl-tabs__line"
  53. v-if="lineLeft > 0"
  54. :style="{
  55. 'background-color': color,
  56. left: lineLeft + 'px',
  57. }"
  58. ></view>
  59. </view>
  60. </scroll-view>
  61. <!-- 下拉图标 -->
  62. <view class="cl-tabs__dropdown" :style="[style]" @tap="onDropdown" v-if="showDropdown">
  63. <cl-icon :name="`${dropdown.visible ? 'arrow-top' : 'arrow-bottom'}`"></cl-icon>
  64. </view>
  65. <!-- 下拉列表 -->
  66. <view
  67. class="cl-tabs__dropdown-box"
  68. :style="{
  69. maxHeight: dropdown.visible ? dropdown.height : '0',
  70. }"
  71. >
  72. <slot name="dropdown"></slot>
  73. </view>
  74. </view>
  75. <view
  76. class="cl-tabs__container"
  77. ref="pane"
  78. @touchstart="onTouchStart"
  79. @touchend="onTouchEnd"
  80. >
  81. <slot></slot>
  82. </view>
  83. </view>
  84. </template>
  85. <script>
  86. import { isNumber } from "../../utils";
  87. /**
  88. * tabs 选项卡
  89. * @description 选项卡,支持滑动,自定义内容
  90. * @tutorial https://docs.cool-js.com/uni/components/nav/tabs.html
  91. * @property {String, Number} value 绑定值
  92. * @property {Array} labels 标签列表
  93. * @property {Boolean} loop 是否循环显示,默认true
  94. * @property {Boolean} swipeable 是否滑动
  95. * @property {Number} swipeThreshold 滑动阈值,默认60
  96. * @property {Boolean} sticky 是否吸顶
  97. * @property {String} stickyTop 吸顶顶部距离
  98. * @property {Boolean} scrollView 是否滚动视图,默认true
  99. * @property {Boolean} fill 标签是否填充
  100. * @property {String} justify 水平布局
  101. * @property {Boolean} border 是否带有下边框,默认true
  102. * @property {Number} gutter 标签间隔,默认20
  103. * @property {String} color 字体及浮标颜色,默认主色
  104. * @property {Boolean} showDropdown 是否显示下拉按钮
  105. * @property {String, Number} height 高度,默认80
  106. * @example 见教程
  107. */
  108. export default {
  109. name: "cl-tabs",
  110. componentName: "ClTabs",
  111. props: {
  112. // 绑定值
  113. value: [String, Number],
  114. // 离开前
  115. beforeLeave: Function,
  116. // 标签列表
  117. labels: {
  118. type: Array,
  119. default: null,
  120. },
  121. // 是否循环显示
  122. loop: {
  123. type: Boolean,
  124. default: true,
  125. },
  126. // 是否滑动
  127. swipeable: Boolean,
  128. // 滑动阈值
  129. swipeThreshold: {
  130. type: Number,
  131. default: 60,
  132. },
  133. // 是否吸顶
  134. sticky: Boolean,
  135. // 吸顶顶部距离
  136. stickyTop: String,
  137. // 是否滚动视图
  138. scrollView: {
  139. type: Boolean,
  140. default: true,
  141. },
  142. // 标签是否填充
  143. fill: Boolean,
  144. // 水平布局
  145. justify: {
  146. type: String,
  147. default: "start",
  148. },
  149. // 是否带有下边框
  150. border: {
  151. type: Boolean,
  152. default: true,
  153. },
  154. // 标签间隔
  155. gutter: {
  156. type: Number,
  157. default: 20,
  158. },
  159. // 字体及浮标颜色,默认主色
  160. color: {
  161. type: String,
  162. default: "",
  163. },
  164. // 未选中字体颜色
  165. unColor: {
  166. type: String,
  167. default: "",
  168. },
  169. // 背景颜色
  170. backgroundColor: {
  171. type: String,
  172. default: "#fff",
  173. },
  174. // 是否显示下拉
  175. showDropdown: Boolean,
  176. // 高度
  177. height: {
  178. type: [String, Number],
  179. default: 80,
  180. },
  181. },
  182. data() {
  183. return {
  184. list: [],
  185. current: 0,
  186. lineLeft: 0,
  187. scrollLeft: 0,
  188. clientX: "",
  189. clientY: "",
  190. offsetLeft: 0,
  191. width: 375,
  192. dropdown: {
  193. visible: false,
  194. height: "200rpx",
  195. timer: null,
  196. },
  197. };
  198. },
  199. watch: {
  200. value: {
  201. immediate: true,
  202. handler(val) {
  203. this.current = val || 0;
  204. },
  205. },
  206. current(val) {
  207. this.onOffset(val);
  208. },
  209. labels() {
  210. this.refresh();
  211. },
  212. },
  213. computed: {
  214. tabs() {
  215. return (this.labels || this.list).map((e, i) => {
  216. e.name = e.name === undefined ? i : e.name;
  217. return e;
  218. });
  219. },
  220. isSticky() {
  221. return this.sticky ? "cl-tabs--sticky" : "";
  222. },
  223. classList() {
  224. let list = [];
  225. if (this.$slots.default || this.$slots.$default) {
  226. list.push("is-content");
  227. }
  228. if (this.sticky) {
  229. list.push("is-sticky");
  230. }
  231. if (this.fill) {
  232. list.push("is-fill");
  233. }
  234. if (this.border) {
  235. list.push("is-border");
  236. }
  237. if (this.showDropdown) {
  238. list.push("is-dropdown");
  239. }
  240. return list.join(" ");
  241. },
  242. style() {
  243. return {
  244. height: isNumber(this.height) ? `${this.height}rpx` : this.height,
  245. backgroundColor: this.backgroundColor,
  246. };
  247. },
  248. },
  249. mounted() {
  250. this.refresh();
  251. },
  252. methods: {
  253. refresh() {
  254. this.$nextTick(() => {
  255. // #ifdef H5
  256. let children = this.$refs.pane.$children;
  257. // #endif
  258. // #ifndef H5
  259. let children = this.$children;
  260. // #endif
  261. this.list = children.map((e, i) => {
  262. return {
  263. label: e.label,
  264. prefixIcon: e.prefixIcon,
  265. suffixIcon: e.suffixIcon,
  266. lazy: e.lazy,
  267. };
  268. });
  269. // 获取选项卡宽度
  270. uni.createSelectorQuery()
  271. // #ifndef MP-ALIPAY
  272. .in(this)
  273. // #endif
  274. .select(".cl-tabs")
  275. .boundingClientRect((d) => {
  276. this.offsetLeft = d.left;
  277. this.width = d.width;
  278. this.getRect();
  279. })
  280. .exec();
  281. });
  282. },
  283. onTouchStart(e) {
  284. this.clientX = e.changedTouches[0].clientX;
  285. this.clientY = e.changedTouches[0].clientY;
  286. },
  287. onTouchEnd(e) {
  288. const subX = e.changedTouches[0].clientX - this.clientX;
  289. const subY = e.changedTouches[0].clientY - this.clientY;
  290. // 判断手势滑动方式
  291. if (this.swipeable) {
  292. if (subY < 50 && subY > -50) {
  293. if (subX > this.swipeThreshold) {
  294. this.prev();
  295. } else if (subX < -this.swipeThreshold) {
  296. this.next();
  297. }
  298. }
  299. }
  300. },
  301. onDropdown() {
  302. this.dropdown.visible = !this.dropdown.visible;
  303. // 清除计时器
  304. clearTimeout(this.dropdown.timer);
  305. if (this.dropdown.visible) {
  306. const fn = () => {
  307. uni.createSelectorQuery()
  308. // #ifndef MP-ALIPAY
  309. .in(this)
  310. // #endif
  311. .select(".cl-tabs__dropdown-box")
  312. .boundingClientRect((res) => {
  313. this.dropdown.height = res.height + "px";
  314. })
  315. .exec();
  316. };
  317. // 获取下拉区域高度
  318. this.dropdown.timer = setTimeout(fn, 300);
  319. }
  320. },
  321. closeDropdown() {
  322. this.dropdown.visible = false;
  323. },
  324. async change(index) {
  325. const { name } = this.tabs[index];
  326. let flag = true;
  327. if (this.beforeLeave) {
  328. const fn = this.beforeLeave(name);
  329. if (!!fn && typeof fn.then === "function") {
  330. flag = !!(await fn.catch(() => null));
  331. } else {
  332. flag = fn;
  333. }
  334. }
  335. if (flag) {
  336. this.$emit("input", name);
  337. this.$emit("tab-change", name);
  338. this.current = name;
  339. }
  340. },
  341. getIndex() {
  342. return this.tabs.findIndex((e) => e.name == this.current);
  343. },
  344. prev() {
  345. let index = this.getIndex();
  346. this.change(index <= 0 ? (this.loop ? this.list.length - 1 : 0) : index - 1);
  347. },
  348. next() {
  349. let index = this.getIndex();
  350. this.change(
  351. index >= this.list.length - 1 ? (this.loop ? 0 : this.list.length - 1) : index + 1
  352. );
  353. },
  354. getRect() {
  355. this.$nextTick(() => {
  356. uni.createSelectorQuery()
  357. .in(this)
  358. .selectAll(".cl-tabs__bar-item")
  359. .fields({ rect: true, size: true })
  360. .exec((d) => {
  361. this.tabRect = d[0];
  362. this.onOffset();
  363. });
  364. });
  365. },
  366. onOffset() {
  367. if (this.tabRect) {
  368. this.$nextTick(() => {
  369. let item = this.tabRect[this.getIndex()];
  370. if (item) {
  371. let scrollLeft =
  372. item.left - (this.width - item.width) / 2 - this.offsetLeft;
  373. if (scrollLeft < 0) {
  374. scrollLeft = 0;
  375. }
  376. this.scrollLeft = scrollLeft;
  377. this.lineLeft = item.left + item.width / 2 - 8 - this.offsetLeft;
  378. }
  379. });
  380. }
  381. },
  382. },
  383. };
  384. </script>