index.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. <template>
  2. <view class="nut-tabs" :class="[direction]" ref="container" id="container">
  3. <template v-if="sticky">
  4. <nut-sticky :top="top" :container="container" @scroll="onStickyScroll">
  5. <view
  6. class="nut-tabs__titles"
  7. :class="{ [type]: type, scrollable: titleScroll, [size]: size }"
  8. :style="tabsNavStyle"
  9. >
  10. <slot v-if="$slots.titles" name="titles"></slot>
  11. <template v-else>
  12. <view
  13. class="nut-tabs__titles-item"
  14. :style="titleStyle"
  15. @click="tabChange(item, index)"
  16. :class="{ active: item.paneKey == modelValue, disabled: item.disabled }"
  17. v-for="(item, index) in titles"
  18. :key="item.paneKey"
  19. >
  20. <view class="nut-tabs__titles-item__line" :style="tabsActiveStyle" v-if="type == 'line'"></view>
  21. <view class="nut-tabs__titles-item__smile" :style="tabsActiveStyle" v-if="type == 'smile'">
  22. <JoySmile :color="color" />
  23. </view>
  24. <view class="nut-tabs__titles-item__text" :class="{ ellipsis: ellipsis }">{{ item.title }} </view>
  25. </view>
  26. </template>
  27. </view>
  28. </nut-sticky>
  29. </template>
  30. <template v-else>
  31. <view
  32. class="nut-tabs__titles"
  33. :class="{ [type]: type, scrollable: titleScroll, [size]: size }"
  34. :style="tabsNavStyle"
  35. >
  36. <slot v-if="$slots.titles" name="titles"></slot>
  37. <template v-else>
  38. <view
  39. class="nut-tabs__titles-item"
  40. :style="titleStyle"
  41. @click="tabChange(item, index)"
  42. :class="{ active: item.paneKey == modelValue, disabled: item.disabled }"
  43. v-for="(item, index) in titles"
  44. :key="item.paneKey"
  45. >
  46. <view class="nut-tabs__titles-item__line" :style="tabsActiveStyle" v-if="type == 'line'"></view>
  47. <view class="nut-tabs__titles-item__smile" :style="tabsActiveStyle" v-if="type == 'smile'">
  48. <JoySmile :color="color" />
  49. </view>
  50. <view class="nut-tabs__titles-item__text" :class="{ ellipsis: ellipsis }">{{ item.title }} </view>
  51. </view>
  52. </template>
  53. </view>
  54. </template>
  55. <view class="nut-tabs__content" :style="contentStyle">
  56. <slot name="default"></slot>
  57. </view>
  58. </view>
  59. </template>
  60. <script lang="ts">
  61. import { createComponent } from '@/packages/utils/create';
  62. import { pxCheck } from '@/packages/utils/pxCheck';
  63. import { TypeOfFun } from '@/packages/utils/util';
  64. import { useRect } from '@/packages/utils/useRect';
  65. import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch } from 'vue';
  66. export class Title {
  67. title: string = '';
  68. titleSlot?: VNode[];
  69. paneKey: string = '';
  70. disabled: boolean = false;
  71. constructor() {}
  72. }
  73. export type TabsSize = 'large' | 'normal' | 'small';
  74. import Sticky from '../sticky/index.vue';
  75. const { create } = createComponent('tabs');
  76. import { JoySmile } from '@nutui/icons-vue';
  77. export default create({
  78. components: { [Sticky.name]: Sticky, JoySmile },
  79. props: {
  80. modelValue: {
  81. type: [String, Number],
  82. default: 0
  83. },
  84. color: {
  85. type: String,
  86. default: ''
  87. },
  88. direction: {
  89. type: String,
  90. default: 'horizontal' //vertical
  91. },
  92. size: {
  93. type: String as import('vue').PropType<TabsSize>,
  94. default: 'normal'
  95. },
  96. type: {
  97. type: String,
  98. default: 'line' //card、line、smile
  99. },
  100. titleScroll: {
  101. type: Boolean,
  102. default: false
  103. },
  104. ellipsis: {
  105. type: Boolean,
  106. default: true
  107. },
  108. autoHeight: {
  109. type: Boolean,
  110. default: false
  111. },
  112. background: {
  113. type: String,
  114. default: ''
  115. },
  116. animatedTime: {
  117. type: [Number, String],
  118. default: 300
  119. },
  120. titleGutter: {
  121. type: [Number, String],
  122. default: 0
  123. },
  124. sticky: {
  125. type: Boolean,
  126. default: false
  127. },
  128. top: {
  129. type: Number,
  130. default: 0
  131. }
  132. },
  133. emits: ['update:modelValue', 'click', 'change'],
  134. setup(props: any, { emit, slots }: any) {
  135. const container = ref(null);
  136. let stickyFixed: boolean;
  137. provide('activeKey', { activeKey: computed(() => props.modelValue) });
  138. provide('autoHeight', { autoHeight: computed(() => props.autoHeight) });
  139. const titles: Ref<Title[]> = ref([]);
  140. const renderTitles = (vnodes: VNode[]) => {
  141. vnodes.forEach((vnode: VNode, index: number) => {
  142. let type = vnode.type;
  143. type = (type as any).name || type;
  144. if (type == 'nut-tab-pane') {
  145. let title = new Title();
  146. if (vnode.props?.title || vnode.props?.['pane-key'] || vnode.props?.['paneKey']) {
  147. let paneKeyType = TypeOfFun(vnode.props?.['pane-key']);
  148. let paneIndex =
  149. paneKeyType == 'number' || paneKeyType == 'string' ? String(vnode.props?.['pane-key']) : null;
  150. let camelPaneKeyType = TypeOfFun(vnode.props?.['paneKey']);
  151. let camelPaneIndex =
  152. camelPaneKeyType == 'number' || camelPaneKeyType == 'string' ? String(vnode.props?.['paneKey']) : null;
  153. title.title = vnode.props?.title;
  154. title.paneKey = paneIndex || camelPaneIndex || String(index);
  155. title.disabled = vnode.props?.disabled;
  156. } else {
  157. // title.titleSlot = vnode.children?.title() as VNode[];
  158. }
  159. titles.value.push(title);
  160. } else {
  161. if (vnode.children == ' ') {
  162. return;
  163. }
  164. renderTitles(vnode.children as VNode[]);
  165. }
  166. });
  167. };
  168. const currentIndex = ref((props.modelValue as number) || 0);
  169. const findTabsIndex = (value: string | number) => {
  170. let index = titles.value.findIndex((item) => item.paneKey == value);
  171. if (titles.value.length == 0) {
  172. console.error('[NutUI] <Tabs> 当前未找到 TabPane 组件元素 , 请检查 .');
  173. } else if (index == -1) {
  174. console.error('[NutUI] <Tabs> 请检查 v-model 值是否为 paneKey ,如 paneKey 未设置,请采用下标控制 .');
  175. } else {
  176. currentIndex.value = index;
  177. }
  178. };
  179. const init = (vnodes: VNode[] = slots.default?.()) => {
  180. titles.value = [];
  181. vnodes = vnodes?.filter((item) => typeof item.children !== 'string');
  182. if (vnodes && vnodes.length) {
  183. renderTitles(vnodes);
  184. }
  185. findTabsIndex(props.modelValue);
  186. };
  187. const onStickyScroll = (params: { top: number; fixed: boolean }) => {
  188. stickyFixed = params.fixed;
  189. };
  190. watch(
  191. () => slots.default?.(),
  192. (vnodes: VNode[]) => {
  193. init(vnodes);
  194. }
  195. );
  196. const getScrollTopRoot = () => {
  197. return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
  198. };
  199. watch(
  200. () => props.modelValue,
  201. (value: string | number) => {
  202. findTabsIndex(value);
  203. if (stickyFixed) {
  204. let top = useRect(container.value!).top + getScrollTopRoot();
  205. let value = Math.ceil(top - props.top);
  206. window.scrollTo({
  207. top: value,
  208. behavior: 'smooth'
  209. });
  210. }
  211. }
  212. );
  213. onMounted(init);
  214. onActivated(init);
  215. const contentStyle = computed(() => {
  216. return {
  217. transform:
  218. props.direction == 'horizontal'
  219. ? `translate3d(-${currentIndex.value * 100}%, 0, 0)`
  220. : `translate3d( 0,-${currentIndex.value * 100}%, 0)`,
  221. transitionDuration: `${props.animatedTime}ms`
  222. };
  223. });
  224. const tabsNavStyle = computed(() => {
  225. return {
  226. background: props.background
  227. };
  228. });
  229. const tabsActiveStyle = computed(() => {
  230. return {
  231. color: props.type == 'smile' ? props.color : '',
  232. background: props.type == 'line' ? props.color : ''
  233. };
  234. });
  235. const titleStyle = computed(() => {
  236. return {
  237. marginLeft: pxCheck(props.titleGutter),
  238. marginRight: pxCheck(props.titleGutter)
  239. };
  240. });
  241. const methods = {
  242. tabChange: (item: Title, index: number) => {
  243. emit('click', item);
  244. if (item.disabled) {
  245. return;
  246. }
  247. currentIndex.value = index;
  248. emit('update:modelValue', item.paneKey);
  249. emit('change', item);
  250. }
  251. };
  252. return {
  253. titles,
  254. contentStyle,
  255. tabsNavStyle,
  256. titleStyle,
  257. tabsActiveStyle,
  258. container,
  259. onStickyScroll,
  260. ...methods
  261. };
  262. }
  263. });
  264. </script>