Browse Source

Merge branch 'v4' of https://github.com/jdf2e/nutui into v4

yangxiaolu3 3 years ago
parent
commit
a57609d15d

+ 26 - 26
src/packages/__VUE/inputnumber/__tests__/index.spec.ts

@@ -1,12 +1,10 @@
 import { mount, config } from '@vue/test-utils';
 import InputNumber from '../index.vue';
-import { nextTick } from 'vue';
-import NutIcon from '../../icon/index.vue';
+import { h, nextTick } from 'vue';
+import { Minus, Plus, Left, Right } from '@nutui/icons-vue';
 
 beforeAll(() => {
-  config.global.components = {
-    NutIcon
-  };
+  config.global.components = { Minus, Plus };
 });
 
 afterAll(() => {
@@ -24,7 +22,7 @@ test('should render modelValue', () => {
   expect(input.value).toBe('12');
 });
 
-test('should add step 2 when trigger click plus button', async () => {
+test('should add step 2 when trigger click right button', async () => {
   const wrapper = mount(InputNumber, {
     props: {
       modelValue: 1,
@@ -32,7 +30,7 @@ test('should add step 2 when trigger click plus button', async () => {
     }
   });
 
-  const iconPlus = wrapper.find('.nut-icon-plus');
+  const iconPlus = wrapper.find('.nut-input-number__right');
   await iconPlus.trigger('click');
 
   expect(wrapper.emitted('overlimit')).toBeFalsy();
@@ -41,7 +39,7 @@ test('should add step 2 when trigger click plus button', async () => {
   expect((wrapper.emitted('update:modelValue')![0] as any[])[0]).toEqual('3');
 });
 
-test('should minis step 2 when trigger click minis button', async () => {
+test('should minis step 2 when trigger click left button', async () => {
   const wrapper = mount(InputNumber, {
     props: {
       modelValue: 3,
@@ -49,7 +47,7 @@ test('should minis step 2 when trigger click minis button', async () => {
     }
   });
 
-  const iconMinus = wrapper.find('.nut-icon-minus');
+  const iconMinus = wrapper.find('.nut-input-number__left');
   await iconMinus.trigger('click');
 
   expect(wrapper.emitted('overlimit')).toBeFalsy();
@@ -67,7 +65,7 @@ test('should render max and min props', async () => {
     }
   });
 
-  const iconPlus = wrapper.find('.nut-icon-plus');
+  const iconPlus = wrapper.find('.nut-input-number__right');
   await iconPlus.trigger('click');
 
   expect(wrapper.emitted('overlimit')).toBeTruthy();
@@ -78,7 +76,7 @@ test('should render max and min props', async () => {
     modelValue: 2
   });
 
-  const iconMinus = wrapper.find('.nut-icon-minus');
+  const iconMinus = wrapper.find('.nut-input-number__left');
   await iconMinus.trigger('click');
   expect(wrapper.emitted('overlimit')).toBeTruthy();
   expect(wrapper.emitted('reduce')).toBeTruthy();
@@ -91,11 +89,11 @@ test('should not trigger click when disabled props to be true', async () => {
     modelValue: 1
   });
 
-  const iconPlus = wrapper.find('.nut-icon-plus');
+  const iconPlus = wrapper.find('.nut-input-number__right');
   await iconPlus.trigger('click');
   expect((wrapper.emitted('update:modelValue')![0] as any[])[0]).toEqual('1');
 
-  const iconMinus = wrapper.find('.nut-icon-minus');
+  const iconMinus = wrapper.find('.nut-input-number__left');
   await iconMinus.trigger('click');
   expect((wrapper.emitted('update:modelValue')![0] as any[])[0]).toEqual('1');
 });
@@ -108,7 +106,7 @@ test('should not focus input when readonly props to be true', async () => {
     }
   });
 
-  const iconMinus = wrapper.find('.nut-icon-minus');
+  const iconMinus = wrapper.find('.nut-input-number__left');
   await iconMinus.trigger('click');
   await nextTick();
 
@@ -126,7 +124,7 @@ test('should render decimal when step props to be 0.2', async () => {
     }
   });
 
-  const iconPlus = wrapper.find('.nut-icon-plus');
+  const iconPlus = wrapper.find('.nut-input-number__right');
   await iconPlus.trigger('click');
 
   expect((wrapper.emitted('change')![0] as any[])[0]).toEqual('2.2');
@@ -141,10 +139,10 @@ test('should render size when buttonSize and inputWidth props setted', async ()
     }
   });
 
-  const iconPlus = wrapper.find('.nut-icon-plus');
+  const iconPlus = wrapper.find('.nut-input-number__right .nut-icon');
   const input = wrapper.find('input').element as HTMLInputElement;
 
-  expect((iconPlus.element as HTMLElement).style.fontSize).toEqual('30px');
+  expect((iconPlus.element as HTMLElement).style.width).toEqual('30px');
   expect(input.style.width).toEqual('120px');
 });
 
@@ -164,18 +162,20 @@ test('should update input value when inputValue overlimit', async () => {
   expect((wrapper.emitted('update:modelValue')![0] as any[])[0]).toEqual('100');
 });
 
-test('should render icon when iconLeft and iconRight props setted', async () => {
+test('should render icon when leftIcon and rightIcon slots setted', async () => {
   const wrapper = mount(InputNumber, {
-    props: {
-      iconLeft: 'left',
-      iconRight: 'right',
-      fontClassName: 'n-nutui-iconfont',
-      classPrefix: 'n-nut-icon'
+    slots: {
+      leftIcon: h(Left, {
+        color: '#123456'
+      }),
+      rightIcon: h(Right, {
+        color: '#654321'
+      })
     }
   });
 
-  const iconList = wrapper.findAll('.nut-icon');
+  const iconList = wrapper.findAll('.nut-input-number__icon');
   expect(iconList.length).toBe(2);
-  expect(iconList[0].html()).toContain('n-nut-icon-left');
-  expect(iconList[1].html()).toContain('n-nut-icon-right');
+  expect(iconList[0].html()).toContain('color="#123456"');
+  expect(iconList[1].html()).toContain('color="#654321"');
 });

+ 10 - 1
src/packages/__VUE/inputnumber/demo.vue

@@ -34,7 +34,14 @@
     </nut-cell>
     <h2>{{ translate('icon') }}</h2>
     <nut-cell>
-      <nut-input-number icon-left="left" icon-right="right" v-model="state.val9" />
+      <nut-input-number v-model="state.val9">
+        <template #leftIcon>
+          <Left />
+        </template>
+        <template #rightIcon>
+          <Right />
+        </template>
+      </nut-input-number>
     </nut-cell>
   </div>
 </template>
@@ -42,6 +49,7 @@
 <script lang="ts">
 import { reactive, getCurrentInstance } from 'vue';
 import { createComponent } from '@/packages/utils/create';
+import { Left, Right } from '@nutui/icons-vue';
 const { createDemo, translate } = createComponent('input-number');
 import { useTranslate } from '@/sites/assets/util/useTranslate';
 const initTranslate = () =>
@@ -74,6 +82,7 @@ const initTranslate = () =>
     }
   });
 export default createDemo({
+  components: { Left, Right },
   props: {},
   setup() {
     initTranslate();

+ 17 - 9
src/packages/__VUE/inputnumber/doc.en-US.md

@@ -9,14 +9,12 @@ Control the number increase or decrease by clicking the button.
 ``` javascript
 import { createApp } from 'vue';
 // vue
-import { InputNumber,Icon } from '@nutui/nutui';
+import { InputNumber } from '@nutui/nutui';
 // taro
-import { InputNumber,Icon } from '@nutui/nutui-taro';
+import { InputNumber } from '@nutui/nutui-taro';
 
 const app = createApp();
 app.use(InputNumber);
-app.use(Icon);
-
 ```
 
 
@@ -223,7 +221,14 @@ Asynchronous modification through `change` event and `model-value`
 
 ```html
 <template>
-  <nut-input-number icon-left="left" icon-right="right" v-model="value" />
+  <nut-input-number v-model="value">
+    <template #leftIcon>
+      <Left />
+    </template>
+    <template #rightIcon>
+      <Right />
+    </template>
+  </nut-input-number>
 </template>
 <script lang="ts">
   import { ref } from 'vue';
@@ -254,10 +259,13 @@ Asynchronous modification through `change` event and `model-value`
 | decimal-places | Set reserved decimal places           | String、Number | `0`        |
 | disabled       | Disable all features               | Boolean        | false      |
 | readonly       | Read only status disables input box operation behavior | Boolean        | false      |
-| icon-left`v3.2.2`  | Left icon name             | String         | `minus`     |
-| icon-right`v3.2.2` | Right icon name             | String         | `plus`      |
-| font-class-name `v3.2.2` | Custom icon font base class name | String   | `nutui-iconfont` |
-| class-prefix `v3.2.2` | Custom icon class name prefix for using custom icons | String   | `nut-icon`  |
+
+### Slots
+
+| Name | Description |
+|-|-|
+| leftIcon | Custom left icon |
+| rightIcon | Custom right icon |
 
 ### Events
 

+ 17 - 9
src/packages/__VUE/inputnumber/doc.md

@@ -9,14 +9,12 @@
 ``` javascript
 import { createApp } from 'vue';
 // vue
-import { InputNumber,Icon } from '@nutui/nutui';
+import { InputNumber } from '@nutui/nutui';
 // taro
-import { InputNumber,Icon } from '@nutui/nutui-taro';
+import { InputNumber } from '@nutui/nutui-taro';
 
 const app = createApp();
 app.use(InputNumber);
-app.use(Icon);
-
 ```
 
 
@@ -223,7 +221,14 @@ app.use(Icon);
 
 ```html
 <template>
-  <nut-input-number icon-left="left" icon-right="right" v-model="value" />
+  <nut-input-number v-model="value">
+    <template #leftIcon>
+      <Left />
+    </template>
+    <template #rightIcon>
+      <Right />
+    </template>
+  </nut-input-number>
 </template>
 <script lang="ts">
   import { ref } from 'vue';
@@ -254,10 +259,13 @@ app.use(Icon);
 | decimal-places | 设置保留的小数位           | String、Number | `0`        |
 | disabled       | 禁用所有功能               | Boolean        | false      |
 | readonly       | 只读状态禁用输入框操作行为 | Boolean        | false      |
-| icon-left `v3.2.2`  | 左侧操作符图标名,同 Icon 组件 name 属性 | String  | `minus` |
-| icon-right `v3.2.2` | 右侧操作符图标名,同 Icon 组件 name 属性 | String  | `plus`  |
-| font-class-name `v3.2.2` | 自定义icon 字体基础类名 | String   | `nutui-iconfont` |
-| class-prefix `v3.2.2` | 自定义icon 类名前缀,用于使用自定义图标 | String   | `nut-icon`  |
+
+### Slots
+
+| 名称 | 说明 |
+|-|-|
+| leftIcon | 自定义左侧按钮 |
+| rightIcon | 自定义右侧按钮 |
 
 ### Events
 

+ 2 - 0
src/packages/__VUE/inputnumber/index.scss

@@ -35,6 +35,8 @@
   }
 
   &__icon {
+    display: flex;
+    align-items: center;
     color: $inputnumber-icon-color;
     font-size: $inputnumber-icon-size;
     cursor: pointer;

+ 18 - 24
src/packages/__VUE/inputnumber/index.taro.vue

@@ -1,21 +1,21 @@
 <template>
-  <view :class="classes" :style="{ height: pxCheck(buttonSize) }">
-    <nut-icon
-      :name="iconLeft"
-      class="nut-input-number__icon"
+  <view :class="classes">
+    <view
+      class="nut-input-number__icon nut-input-number__left"
       :class="{ 'nut-input-number__icon--disabled': !reduceAllow() }"
-      :size="buttonSize"
-      v-bind="$attrs"
       @click="reduce"
     >
-    </nut-icon>
+      <slot name="leftIcon">
+        <Minus :width="pxCheck(buttonSize)" :height="pxCheck(buttonSize)" />
+      </slot>
+    </view>
     <view v-if="readonly" class="nut-input-number__text--readonly">
       {{ modelValue }}
     </view>
     <input
       v-else
-      type="number"
       class="nut-input-number__text--input"
+      type="number"
       :min="min"
       :max="max"
       :style="{ width: pxCheck(inputWidth) }"
@@ -26,23 +26,25 @@
       @blur="blur"
       @focus="focus"
     />
-    <nut-icon
-      :name="iconRight"
-      class="nut-input-number__icon"
+    <view
+      class="nut-input-number__icon nut-input-number__right"
       :class="{ 'nut-input-number__icon--disabled': !addAllow() }"
-      :size="buttonSize"
-      v-bind="$attrs"
       @click="add"
     >
-    </nut-icon>
+      <slot name="rightIcon">
+        <Plus :width="pxCheck(buttonSize)" :height="pxCheck(buttonSize)" />
+      </slot>
+    </view>
   </view>
 </template>
 <script lang="ts">
 import { computed } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 import { pxCheck } from '@/packages/utils/pxCheck';
+import { Minus, Plus } from '@nutui/icons-vue';
 const { componentName, create } = createComponent('input-number');
 export default create({
+  components: { Minus, Plus },
   props: {
     modelValue: {
       type: [Number, String],
@@ -50,11 +52,11 @@ export default create({
     },
     inputWidth: {
       type: [Number, String],
-      default: ''
+      default: '40px'
     },
     buttonSize: {
       type: [Number, String],
-      default: ''
+      default: '20px'
     },
     min: {
       type: [Number, String],
@@ -79,14 +81,6 @@ export default create({
     readonly: {
       type: Boolean,
       default: false
-    },
-    iconLeft: {
-      type: String,
-      default: 'minus'
-    },
-    iconRight: {
-      type: String,
-      default: 'plus'
     }
   },
   emits: ['update:modelValue', 'change', 'blur', 'focus', 'reduce', 'add', 'overlimit'],

+ 17 - 23
src/packages/__VUE/inputnumber/index.vue

@@ -1,14 +1,14 @@
 <template>
-  <view :class="classes" :style="{ height: pxCheck(buttonSize) }">
-    <nut-icon
-      :name="iconLeft"
-      class="nut-input-number__icon"
+  <view :class="classes">
+    <view
+      class="nut-input-number__icon nut-input-number__left"
       :class="{ 'nut-input-number__icon--disabled': !reduceAllow() }"
-      :size="buttonSize"
-      v-bind="$attrs"
       @click="reduce"
     >
-    </nut-icon>
+      <slot name="leftIcon">
+        <Minus :width="pxCheck(buttonSize)" :height="pxCheck(buttonSize)" />
+      </slot>
+    </view>
     <input
       type="number"
       :min="min"
@@ -21,23 +21,25 @@
       @blur="blur"
       @focus="focus"
     />
-    <nut-icon
-      :name="iconRight"
-      class="nut-input-number__icon"
+    <view
+      class="nut-input-number__icon nut-input-number__right"
       :class="{ 'nut-input-number__icon--disabled': !addAllow() }"
-      :size="buttonSize"
-      v-bind="$attrs"
       @click="add"
     >
-    </nut-icon>
+      <slot name="rightIcon">
+        <Plus :width="pxCheck(buttonSize)" :height="pxCheck(buttonSize)" />
+      </slot>
+    </view>
   </view>
 </template>
 <script lang="ts">
 import { computed } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 import { pxCheck } from '@/packages/utils/pxCheck';
+import { Minus, Plus } from '@nutui/icons-vue';
 const { componentName, create } = createComponent('input-number');
 export default create({
+  components: { Minus, Plus },
   props: {
     modelValue: {
       type: [Number, String],
@@ -45,11 +47,11 @@ export default create({
     },
     inputWidth: {
       type: [Number, String],
-      default: ''
+      default: '40px'
     },
     buttonSize: {
       type: [Number, String],
-      default: ''
+      default: '20px'
     },
     min: {
       type: [Number, String],
@@ -74,14 +76,6 @@ export default create({
     readonly: {
       type: Boolean,
       default: false
-    },
-    iconLeft: {
-      type: String,
-      default: 'minus'
-    },
-    iconRight: {
-      type: String,
-      default: 'plus'
     }
   },
   emits: ['update:modelValue', 'change', 'blur', 'focus', 'reduce', 'add', 'overlimit'],

+ 0 - 205
src/packages/__VUE/tabs/common.ts

@@ -1,205 +0,0 @@
-import { pxCheck } from '@/packages/utils/pxCheck';
-import { TypeOfFun } from '@/packages/utils/util';
-import { useRect } from '@/packages/utils/useRect';
-import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch } from 'vue';
-export class Title {
-  title: string = '';
-  titleSlot?: VNode[];
-  paneKey: string = '';
-  disabled: boolean = false;
-  constructor() {}
-}
-export type TabsSize = 'large' | 'normal' | 'small';
-export const component = (components: any) => {
-  return {
-    props: {
-      modelValue: {
-        type: [String, Number],
-        default: 0
-      },
-      color: {
-        type: String,
-        default: ''
-      },
-      direction: {
-        type: String,
-        default: 'horizontal' //vertical
-      },
-      size: {
-        type: String as import('vue').PropType<TabsSize>,
-        default: 'normal'
-      },
-      type: {
-        type: String,
-        default: 'line' //card、line、smile
-      },
-      titleScroll: {
-        type: Boolean,
-        default: false
-      },
-      ellipsis: {
-        type: Boolean,
-        default: true
-      },
-      autoHeight: {
-        type: Boolean,
-        default: false
-      },
-      background: {
-        type: String,
-        default: ''
-      },
-      animatedTime: {
-        type: [Number, String],
-        default: 300
-      },
-      titleGutter: {
-        type: [Number, String],
-        default: 0
-      },
-      sticky: {
-        type: Boolean,
-        default: false
-      },
-      top: {
-        type: Number,
-        default: 0
-      }
-    },
-
-    components,
-    emits: ['update:modelValue', 'click', 'change'],
-
-    setup(props: any, { emit, slots }: any) {
-      const container = ref(null);
-      let stickyFixed: boolean;
-      provide('activeKey', { activeKey: computed(() => props.modelValue) });
-      provide('autoHeight', { autoHeight: computed(() => props.autoHeight) });
-      const titles: Ref<Title[]> = ref([]);
-      const renderTitles = (vnodes: VNode[]) => {
-        vnodes.forEach((vnode: VNode, index: number) => {
-          let type = vnode.type;
-          type = (type as any).name || type;
-          if (type == 'nut-tab-pane') {
-            let title = new Title();
-            if (vnode.props?.title || vnode.props?.['pane-key'] || vnode.props?.['paneKey']) {
-              let paneKeyType = TypeOfFun(vnode.props?.['pane-key']);
-              let paneIndex =
-                paneKeyType == 'number' || paneKeyType == 'string' ? String(vnode.props?.['pane-key']) : null;
-              let camelPaneKeyType = TypeOfFun(vnode.props?.['paneKey']);
-              let camelPaneIndex =
-                camelPaneKeyType == 'number' || camelPaneKeyType == 'string' ? String(vnode.props?.['paneKey']) : null;
-              title.title = vnode.props?.title;
-              title.paneKey = paneIndex || camelPaneIndex || String(index);
-              title.disabled = vnode.props?.disabled;
-            } else {
-              // title.titleSlot = vnode.children?.title() as VNode[];
-            }
-            titles.value.push(title);
-          } else {
-            if (vnode.children == ' ') {
-              return;
-            }
-            renderTitles(vnode.children as VNode[]);
-          }
-        });
-      };
-
-      const currentIndex = ref((props.modelValue as number) || 0);
-      const findTabsIndex = (value: string | number) => {
-        let index = titles.value.findIndex((item) => item.paneKey == value);
-        if (titles.value.length == 0) {
-          console.error('[NutUI] <Tabs> 当前未找到 TabPane 组件元素 , 请检查 .');
-        } else if (index == -1) {
-          console.error('[NutUI] <Tabs> 请检查 v-model 值是否为 paneKey ,如 paneKey 未设置,请采用下标控制 .');
-        } else {
-          currentIndex.value = index;
-        }
-      };
-      const init = (vnodes: VNode[] = slots.default?.()) => {
-        titles.value = [];
-        vnodes = vnodes?.filter((item) => typeof item.children !== 'string');
-        if (vnodes && vnodes.length) {
-          renderTitles(vnodes);
-        }
-        findTabsIndex(props.modelValue);
-      };
-      const onStickyScroll = (params: { top: number; fixed: boolean }) => {
-        stickyFixed = params.fixed;
-      };
-
-      watch(
-        () => slots.default?.(),
-        (vnodes: VNode[]) => {
-          init(vnodes);
-        }
-      );
-      const getScrollTopRoot = () => {
-        return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
-      };
-      watch(
-        () => props.modelValue,
-        (value: string | number) => {
-          findTabsIndex(value);
-          if (stickyFixed) {
-            let top = useRect(container.value!).top + getScrollTopRoot();
-            let value = Math.ceil(top - props.top);
-            window.scrollTo({
-              top: value,
-              behavior: 'smooth'
-            });
-          }
-        }
-      );
-      onMounted(init);
-      onActivated(init);
-      const contentStyle = computed(() => {
-        return {
-          transform:
-            props.direction == 'horizontal'
-              ? `translate3d(-${currentIndex.value * 100}%, 0, 0)`
-              : `translate3d( 0,-${currentIndex.value * 100}%, 0)`,
-          transitionDuration: `${props.animatedTime}ms`
-        };
-      });
-      const tabsNavStyle = computed(() => {
-        return {
-          background: props.background
-        };
-      });
-      const tabsActiveStyle = computed(() => {
-        return {
-          color: props.type == 'smile' ? props.color : '',
-          background: props.type == 'line' ? props.color : ''
-        };
-      });
-      const titleStyle = computed(() => {
-        return {
-          marginLeft: pxCheck(props.titleGutter),
-          marginRight: pxCheck(props.titleGutter)
-        };
-      });
-      const methods = {
-        tabChange: (item: Title, index: number) => {
-          emit('click', item);
-          if (item.disabled) {
-            return;
-          }
-          currentIndex.value = index;
-          emit('update:modelValue', item.paneKey);
-          emit('change', item);
-        }
-      };
-      return {
-        titles,
-        contentStyle,
-        tabsNavStyle,
-        titleStyle,
-        tabsActiveStyle,
-        container,
-        onStickyScroll,
-        ...methods
-      };
-    }
-  };
-};

+ 223 - 10
src/packages/__VUE/tabs/index.taro.vue

@@ -1,14 +1,227 @@
-<template src="./template.html"></template>
+<template>
+  <view class="nut-tabs" :class="[direction]" ref="container" id="container">
+    <view
+      class="nut-tabs__titles"
+      :class="{ [type]: type, scrollable: titleScroll, [size]: size }"
+      :style="tabsNavStyle"
+    >
+      <slot v-if="$slots.titles" name="titles"></slot>
+      <template v-else>
+        <view
+          class="nut-tabs__titles-item"
+          :style="titleStyle"
+          @click="tabChange(item, index)"
+          :class="{ active: item.paneKey == modelValue, disabled: item.disabled }"
+          v-for="(item, index) in titles"
+          :key="item.paneKey"
+        >
+          <view class="nut-tabs__titles-item__line" :style="tabsActiveStyle" v-if="type == 'line'"></view>
+          <view class="nut-tabs__titles-item__smile" :style="tabsActiveStyle" v-if="type == 'smile'">
+            <IconFont name="joy-smile" :color="color" />
+          </view>
+          <view class="nut-tabs__titles-item__text" :class="{ ellipsis: ellipsis }">{{ item.title }} </view>
+        </view>
+      </template>
+    </view>
+    <view class="nut-tabs__content" :style="contentStyle">
+      <slot name="default"></slot>
+    </view>
+  </view>
+</template>
 <script lang="ts">
 import { createComponent } from '@/packages/utils/create';
-import { component } from './common';
-import Sticky from '../sticky/index.taro.vue';
-import { JoySmile } from '@nutui/icons-vue';
+import { IconFont } from '@nutui/icons-vue';
+import { pxCheck } from '@/packages/utils/pxCheck';
+import { TypeOfFun } from '@/packages/utils/util';
+import { useRect } from '@/packages/utils/useRect';
+import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch } from 'vue';
+export class Title {
+  title: string = '';
+  titleSlot?: VNode[];
+  paneKey: string = '';
+  disabled: boolean = false;
+  constructor() {}
+}
+export type TabsSize = 'large' | 'normal' | 'small';
 const { create } = createComponent('tabs');
-export default create(
-  component({
-    [Sticky.name]: Sticky,
-    JoySmile
-  })
-);
+export default create({
+  components: {
+    IconFont
+  },
+  props: {
+    modelValue: {
+      type: [String, Number],
+      default: 0
+    },
+    color: {
+      type: String,
+      default: ''
+    },
+    direction: {
+      type: String,
+      default: 'horizontal' //vertical
+    },
+    size: {
+      type: String as import('vue').PropType<TabsSize>,
+      default: 'normal'
+    },
+    type: {
+      type: String,
+      default: 'line' //card、line、smile
+    },
+    titleScroll: {
+      type: Boolean,
+      default: false
+    },
+    ellipsis: {
+      type: Boolean,
+      default: true
+    },
+    autoHeight: {
+      type: Boolean,
+      default: false
+    },
+    background: {
+      type: String,
+      default: ''
+    },
+    animatedTime: {
+      type: [Number, String],
+      default: 300
+    },
+    titleGutter: {
+      type: [Number, String],
+      default: 0
+    },
+    sticky: {
+      type: Boolean,
+      default: false
+    },
+    top: {
+      type: Number,
+      default: 0
+    }
+  },
+  emits: ['update:modelValue', 'click', 'change'],
+
+  setup(props: any, { emit, slots }: any) {
+    const container = ref(null);
+    let stickyFixed: boolean;
+    provide('activeKey', { activeKey: computed(() => props.modelValue) });
+    provide('autoHeight', { autoHeight: computed(() => props.autoHeight) });
+    const titles: Ref<Title[]> = ref([]);
+    const renderTitles = (vnodes: VNode[]) => {
+      vnodes.forEach((vnode: VNode, index: number) => {
+        let type = vnode.type;
+        type = (type as any).name || type;
+        if (type == 'nut-tab-pane') {
+          let title = new Title();
+          if (vnode.props?.title || vnode.props?.['pane-key'] || vnode.props?.['paneKey']) {
+            let paneKeyType = TypeOfFun(vnode.props?.['pane-key']);
+            let paneIndex =
+              paneKeyType == 'number' || paneKeyType == 'string' ? String(vnode.props?.['pane-key']) : null;
+            let camelPaneKeyType = TypeOfFun(vnode.props?.['paneKey']);
+            let camelPaneIndex =
+              camelPaneKeyType == 'number' || camelPaneKeyType == 'string' ? String(vnode.props?.['paneKey']) : null;
+            title.title = vnode.props?.title;
+            title.paneKey = paneIndex || camelPaneIndex || String(index);
+            title.disabled = vnode.props?.disabled;
+          } else {
+            // title.titleSlot = vnode.children?.title() as VNode[];
+          }
+          titles.value.push(title);
+        } else {
+          if (vnode.children == ' ') {
+            return;
+          }
+          renderTitles(vnode.children as VNode[]);
+        }
+      });
+    };
+
+    const currentIndex = ref((props.modelValue as number) || 0);
+    const findTabsIndex = (value: string | number) => {
+      let index = titles.value.findIndex((item) => item.paneKey == value);
+      if (titles.value.length == 0) {
+        console.error('[NutUI] <Tabs> 当前未找到 TabPane 组件元素 , 请检查 .');
+      } else if (index == -1) {
+        console.error('[NutUI] <Tabs> 请检查 v-model 值是否为 paneKey ,如 paneKey 未设置,请采用下标控制 .');
+      } else {
+        currentIndex.value = index;
+      }
+    };
+    const init = (vnodes: VNode[] = slots.default?.()) => {
+      titles.value = [];
+      vnodes = vnodes?.filter((item) => typeof item.children !== 'string');
+      if (vnodes && vnodes.length) {
+        renderTitles(vnodes);
+      }
+      findTabsIndex(props.modelValue);
+    };
+
+    watch(
+      () => slots.default?.(),
+      (vnodes: VNode[]) => {
+        init(vnodes);
+      }
+    );
+    const getScrollTopRoot = () => {
+      return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
+    };
+    watch(
+      () => props.modelValue,
+      (value: string | number) => {
+        findTabsIndex(value);
+      }
+    );
+    onMounted(init);
+    onActivated(init);
+    const contentStyle = computed(() => {
+      return {
+        transform:
+          props.direction == 'horizontal'
+            ? `translate3d(-${currentIndex.value * 100}%, 0, 0)`
+            : `translate3d( 0,-${currentIndex.value * 100}%, 0)`,
+        transitionDuration: `${props.animatedTime}ms`
+      };
+    });
+    const tabsNavStyle = computed(() => {
+      return {
+        background: props.background
+      };
+    });
+    const tabsActiveStyle = computed(() => {
+      return {
+        color: props.type == 'smile' ? props.color : '',
+        background: props.type == 'line' ? props.color : ''
+      };
+    });
+    const titleStyle = computed(() => {
+      return {
+        marginLeft: pxCheck(props.titleGutter),
+        marginRight: pxCheck(props.titleGutter)
+      };
+    });
+    const methods = {
+      tabChange: (item: Title, index: number) => {
+        emit('click', item);
+        if (item.disabled) {
+          return;
+        }
+        currentIndex.value = index;
+        emit('update:modelValue', item.paneKey);
+        emit('change', item);
+      }
+    };
+    return {
+      titles,
+      contentStyle,
+      tabsNavStyle,
+      titleStyle,
+      tabsActiveStyle,
+      container,
+      ...methods
+    };
+  }
+});
 </script>

+ 262 - 9
src/packages/__VUE/tabs/index.vue

@@ -1,14 +1,267 @@
-<template src="./template.html"></template>
+<template>
+  <view class="nut-tabs" :class="[direction]" ref="container" id="container">
+    <template v-if="sticky">
+      <nut-sticky :top="top" :container="container" @scroll="onStickyScroll">
+        <view
+          class="nut-tabs__titles"
+          :class="{ [type]: type, scrollable: titleScroll, [size]: size }"
+          :style="tabsNavStyle"
+        >
+          <slot v-if="$slots.titles" name="titles"></slot>
+          <template v-else>
+            <view
+              class="nut-tabs__titles-item"
+              :style="titleStyle"
+              @click="tabChange(item, index)"
+              :class="{ active: item.paneKey == modelValue, disabled: item.disabled }"
+              v-for="(item, index) in titles"
+              :key="item.paneKey"
+            >
+              <view class="nut-tabs__titles-item__line" :style="tabsActiveStyle" v-if="type == 'line'"></view>
+              <view class="nut-tabs__titles-item__smile" :style="tabsActiveStyle" v-if="type == 'smile'">
+                <JoySmile :color="color" />
+              </view>
+              <view class="nut-tabs__titles-item__text" :class="{ ellipsis: ellipsis }">{{ item.title }} </view>
+            </view>
+          </template>
+        </view>
+      </nut-sticky>
+    </template>
+    <template v-else>
+      <view
+        class="nut-tabs__titles"
+        :class="{ [type]: type, scrollable: titleScroll, [size]: size }"
+        :style="tabsNavStyle"
+      >
+        <slot v-if="$slots.titles" name="titles"></slot>
+        <template v-else>
+          <view
+            class="nut-tabs__titles-item"
+            :style="titleStyle"
+            @click="tabChange(item, index)"
+            :class="{ active: item.paneKey == modelValue, disabled: item.disabled }"
+            v-for="(item, index) in titles"
+            :key="item.paneKey"
+          >
+            <view class="nut-tabs__titles-item__line" :style="tabsActiveStyle" v-if="type == 'line'"></view>
+            <view class="nut-tabs__titles-item__smile" :style="tabsActiveStyle" v-if="type == 'smile'">
+              <JoySmile :color="color" />
+            </view>
+            <view class="nut-tabs__titles-item__text" :class="{ ellipsis: ellipsis }">{{ item.title }} </view>
+          </view>
+        </template>
+      </view>
+    </template>
+    <view class="nut-tabs__content" :style="contentStyle">
+      <slot name="default"></slot>
+    </view>
+  </view>
+</template>
 <script lang="ts">
 import { createComponent } from '@/packages/utils/create';
-import { component } from './common';
+import { pxCheck } from '@/packages/utils/pxCheck';
+import { TypeOfFun } from '@/packages/utils/util';
+import { useRect } from '@/packages/utils/useRect';
+import { onMounted, provide, VNode, ref, Ref, computed, onActivated, watch } from 'vue';
+export class Title {
+  title: string = '';
+  titleSlot?: VNode[];
+  paneKey: string = '';
+  disabled: boolean = false;
+  constructor() {}
+}
+export type TabsSize = 'large' | 'normal' | 'small';
 import Sticky from '../sticky/index.vue';
-import { JoySmile } from '@nutui/icons-vue';
 const { create } = createComponent('tabs');
-export default create(
-  component({
-    [Sticky.name]: Sticky,
-    JoySmile
-  })
-);
+import { JoySmile } from '@nutui/icons-vue';
+export default create({
+  components: { [Sticky.name]: Sticky, JoySmile },
+  props: {
+    modelValue: {
+      type: [String, Number],
+      default: 0
+    },
+    color: {
+      type: String,
+      default: ''
+    },
+    direction: {
+      type: String,
+      default: 'horizontal' //vertical
+    },
+    size: {
+      type: String as import('vue').PropType<TabsSize>,
+      default: 'normal'
+    },
+    type: {
+      type: String,
+      default: 'line' //card、line、smile
+    },
+    titleScroll: {
+      type: Boolean,
+      default: false
+    },
+    ellipsis: {
+      type: Boolean,
+      default: true
+    },
+    autoHeight: {
+      type: Boolean,
+      default: false
+    },
+    background: {
+      type: String,
+      default: ''
+    },
+    animatedTime: {
+      type: [Number, String],
+      default: 300
+    },
+    titleGutter: {
+      type: [Number, String],
+      default: 0
+    },
+    sticky: {
+      type: Boolean,
+      default: false
+    },
+    top: {
+      type: Number,
+      default: 0
+    }
+  },
+  emits: ['update:modelValue', 'click', 'change'],
+
+  setup(props: any, { emit, slots }: any) {
+    const container = ref(null);
+    let stickyFixed: boolean;
+    provide('activeKey', { activeKey: computed(() => props.modelValue) });
+    provide('autoHeight', { autoHeight: computed(() => props.autoHeight) });
+    const titles: Ref<Title[]> = ref([]);
+    const renderTitles = (vnodes: VNode[]) => {
+      vnodes.forEach((vnode: VNode, index: number) => {
+        let type = vnode.type;
+        type = (type as any).name || type;
+        if (type == 'nut-tab-pane') {
+          let title = new Title();
+          if (vnode.props?.title || vnode.props?.['pane-key'] || vnode.props?.['paneKey']) {
+            let paneKeyType = TypeOfFun(vnode.props?.['pane-key']);
+            let paneIndex =
+              paneKeyType == 'number' || paneKeyType == 'string' ? String(vnode.props?.['pane-key']) : null;
+            let camelPaneKeyType = TypeOfFun(vnode.props?.['paneKey']);
+            let camelPaneIndex =
+              camelPaneKeyType == 'number' || camelPaneKeyType == 'string' ? String(vnode.props?.['paneKey']) : null;
+            title.title = vnode.props?.title;
+            title.paneKey = paneIndex || camelPaneIndex || String(index);
+            title.disabled = vnode.props?.disabled;
+          } else {
+            // title.titleSlot = vnode.children?.title() as VNode[];
+          }
+          titles.value.push(title);
+        } else {
+          if (vnode.children == ' ') {
+            return;
+          }
+          renderTitles(vnode.children as VNode[]);
+        }
+      });
+    };
+
+    const currentIndex = ref((props.modelValue as number) || 0);
+    const findTabsIndex = (value: string | number) => {
+      let index = titles.value.findIndex((item) => item.paneKey == value);
+      if (titles.value.length == 0) {
+        console.error('[NutUI] <Tabs> 当前未找到 TabPane 组件元素 , 请检查 .');
+      } else if (index == -1) {
+        console.error('[NutUI] <Tabs> 请检查 v-model 值是否为 paneKey ,如 paneKey 未设置,请采用下标控制 .');
+      } else {
+        currentIndex.value = index;
+      }
+    };
+    const init = (vnodes: VNode[] = slots.default?.()) => {
+      titles.value = [];
+      vnodes = vnodes?.filter((item) => typeof item.children !== 'string');
+      if (vnodes && vnodes.length) {
+        renderTitles(vnodes);
+      }
+      findTabsIndex(props.modelValue);
+    };
+    const onStickyScroll = (params: { top: number; fixed: boolean }) => {
+      stickyFixed = params.fixed;
+    };
+
+    watch(
+      () => slots.default?.(),
+      (vnodes: VNode[]) => {
+        init(vnodes);
+      }
+    );
+    const getScrollTopRoot = () => {
+      return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
+    };
+    watch(
+      () => props.modelValue,
+      (value: string | number) => {
+        findTabsIndex(value);
+        if (stickyFixed) {
+          let top = useRect(container.value!).top + getScrollTopRoot();
+          let value = Math.ceil(top - props.top);
+          window.scrollTo({
+            top: value,
+            behavior: 'smooth'
+          });
+        }
+      }
+    );
+    onMounted(init);
+    onActivated(init);
+    const contentStyle = computed(() => {
+      return {
+        transform:
+          props.direction == 'horizontal'
+            ? `translate3d(-${currentIndex.value * 100}%, 0, 0)`
+            : `translate3d( 0,-${currentIndex.value * 100}%, 0)`,
+        transitionDuration: `${props.animatedTime}ms`
+      };
+    });
+    const tabsNavStyle = computed(() => {
+      return {
+        background: props.background
+      };
+    });
+    const tabsActiveStyle = computed(() => {
+      return {
+        color: props.type == 'smile' ? props.color : '',
+        background: props.type == 'line' ? props.color : ''
+      };
+    });
+    const titleStyle = computed(() => {
+      return {
+        marginLeft: pxCheck(props.titleGutter),
+        marginRight: pxCheck(props.titleGutter)
+      };
+    });
+    const methods = {
+      tabChange: (item: Title, index: number) => {
+        emit('click', item);
+        if (item.disabled) {
+          return;
+        }
+        currentIndex.value = index;
+        emit('update:modelValue', item.paneKey);
+        emit('change', item);
+      }
+    };
+    return {
+      titles,
+      contentStyle,
+      tabsNavStyle,
+      titleStyle,
+      tabsActiveStyle,
+      container,
+      onStickyScroll,
+      ...methods
+    };
+  }
+});
 </script>

+ 0 - 43
src/packages/__VUE/tabs/template.html

@@ -1,43 +0,0 @@
-<view class="nut-tabs" :class="[direction]" ref="container" id="container">
-  <template v-if="sticky">
-    <nut-sticky :top="top" :container="container" @scroll="onStickyScroll">
-      <view class="nut-tabs__titles" :class="{ [type]: type, scrollable: titleScroll, [size]: size }"
-        :style="tabsNavStyle">
-        <slot v-if="$slots.titles" name="titles"></slot>
-        <template v-else>
-          <view class="nut-tabs__titles-item" :style="titleStyle" @click="tabChange(item, index)"
-            :class="{ active: item.paneKey == modelValue, disabled: item.disabled }" v-for="(item, index) in titles"
-            :key="item.paneKey">
-            <view class="nut-tabs__titles-item__line" :style="tabsActiveStyle" v-if="type == 'line'"></view>
-            <view class="nut-tabs__titles-item__smile" :style="tabsActiveStyle" v-if="type == 'smile'">
-              <JoySmile :color="color" />
-            </view>
-            <view class="nut-tabs__titles-item__text" :class="{ ellipsis: ellipsis }">{{ item.title }}
-            </view>
-          </view>
-        </template>
-      </view>
-    </nut-sticky>
-  </template>
-  <template v-else>
-    <view class="nut-tabs__titles" :class="{ [type]: type, scrollable: titleScroll, [size]: size }"
-      :style="tabsNavStyle">
-      <slot v-if="$slots.titles" name="titles"></slot>
-      <template v-else>
-        <view class="nut-tabs__titles-item" :style="titleStyle" @click="tabChange(item, index)"
-          :class="{ active: item.paneKey == modelValue, disabled: item.disabled }" v-for="(item, index) in titles"
-          :key="item.paneKey">
-          <view class="nut-tabs__titles-item__line" :style="tabsActiveStyle" v-if="type == 'line'"></view>
-          <view class="nut-tabs__titles-item__smile" :style="tabsActiveStyle" v-if="type == 'smile'">
-            <JoySmile :color="color" />
-          </view>
-          <view class="nut-tabs__titles-item__text" :class="{ ellipsis: ellipsis }">{{ item.title }}
-          </view>
-        </view>
-      </template>
-    </view>
-  </template>
-  <view class="nut-tabs__content" :style="contentStyle">
-    <slot name="default"></slot>
-  </view>
-</view>

+ 20 - 15
src/sites/mobile-taro/vue/src/basic/pages/icon/index.vue

@@ -1,37 +1,41 @@
 <template>
   <div class="demo">
+    <h2>Svg 按需使用</h2>
+    <nut-cell>
+      <Add color="red" />
+    </nut-cell>
     <h2>基础用法</h2>
     <nut-cell>
-      <nut-icon name="dongdong"></nut-icon>
-      <nut-icon name="JD"></nut-icon>
+      <IconFont name="dongdong"></IconFont>
+      <IconFont name="JD"></IconFont>
     </nut-cell>
     <h2>图片链接</h2>
     <nut-cell>
-      <nut-icon
+      <IconFont
         size="40"
         name="https://img11.360buyimg.com/imagetools/jfs/t1/137646/13/7132/1648/5f4c748bE43da8ddd/a3f06d51dcae7b60.png"
       >
-      </nut-icon>
+      </IconFont>
     </nut-cell>
     <h2>图标颜色</h2>
     <nut-cell>
-      <nut-icon name="dongdong" color="#fa2c19"></nut-icon>
-      <nut-icon name="dongdong" color="#64b578"></nut-icon>
-      <nut-icon name="JD" color="#fa2c19"></nut-icon>
+      <IconFont name="dongdong" color="#fa2c19"></IconFont>
+      <IconFont name="dongdong" color="#64b578"></IconFont>
+      <IconFont name="JD" color="#fa2c19"></IconFont>
     </nut-cell>
 
     <h2>图标大小</h2>
     <nut-cell>
-      <nut-icon name="dongdong"></nut-icon>
-      <nut-icon name="dongdong" size="24"></nut-icon>
-      <nut-icon name="dongdong" size="26"></nut-icon>
+      <IconFont name="dongdong"></IconFont>
+      <IconFont name="dongdong" size="24"></IconFont>
+      <IconFont name="dongdong" size="26"></IconFont>
     </nut-cell>
 
     <nut-cell-group v-for="item in icons.data" :title="item.name" :key="item">
       <nut-cell>
         <ul>
           <li v-for="item in item.icons" :key="item">
-            <nut-icon :name="item"></nut-icon>
+            <IconFont :name="item"></IconFont>
             <span>{{ item }}</span>
           </li>
         </ul>
@@ -42,10 +46,10 @@
       <nut-cell>
         <ul>
           <li v-for="it in item.icons" :key="it">
-            <nut-icon
+            <IconFont
               :name="it.name"
               :class="`nut-icon-${it['animation-name']} nut-icon-${it['animation-time']}`"
-            ></nut-icon>
+            ></IconFont>
             <span>{{ it['animation-name'] }}</span>
           </li>
         </ul>
@@ -55,11 +59,12 @@
 </template>
 
 <script lang="ts">
-import { config } from '@nutui/icons-vue';
+import { Add, IconFontConfig, IconFont } from '@nutui/icons-vue';
 export default {
   props: {},
+  components: { IconFont, Add },
   setup() {
-    return { icons: [] };
+    return { icons: IconFontConfig };
   }
 };
 </script>