ソースを参照

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

yangkaixuan 5 年 前
コミット
88ff0a11d6

+ 1 - 0
.gitignore

@@ -5,6 +5,7 @@ node_modules
 /jd/upload.js
 yarn.lock
 package-lock.json
+/cache
 
 
 # local env files

+ 0 - 1
cache/docs.cache

@@ -1 +0,0 @@
-{"design":"vcgLzZ0ctHqnqRhSMmxbHFPKHOk=","international":"48VFbFFji5XeQPSYVbEkhUXs9jw=","intro":"NzilpMOPGLKCi3NAacAUHKA5PM8=","joinus":"xe5bD6hZaxe0Xg0m4zCFnTb5JKY=","start":"hWB+R788ekxKa3Y8TItd8OEYnyE=","theme":"ul//vB9gvub+r0tmKQ0utENYGGY="}

+ 0 - 1
cache/mdToVue.cache

@@ -1 +0,0 @@
-{"mdToVue":"YyHHl228Lm/wkXcldAK2nCQ7hw8="}

ファイルの差分が大きいため隠しています
+ 0 - 1
cache/src.cache


+ 4 - 0
package.json

@@ -23,6 +23,10 @@
   ],
   "author": "jdcfe",
   "license": "MIT",
+  "publishConfig": {
+    "access": "public",
+    "registry": "https://registry.npmjs.org/"
+  },
   "files": [
     "dist",
     "README.md",

+ 9 - 0
src/config.js

@@ -327,6 +327,15 @@ module.exports = {
         },
         {
           version: '3.0.0',
+          name: 'Range',
+          type: 'component',
+          cName: '区间选择器',
+          desc: '滑动输入条,用于在给定的范围内选择一个值。',
+          sort: 16,
+          show: true,
+          author: 'Jerry'
+        },
+        {
           name: 'PullRefresh',
           type: 'component',
           cName: '下拉刷新',

+ 1 - 1
src/docs/intro.md

@@ -16,7 +16,7 @@ NutUI是一套京东风格的移动端组件库,开发和服务于移动Web界
 
 * 50+ 高质量组件
 * 40+ 京东移动端项目正在使用
-* 基于京东APP 7.0 视觉规范
+* 基于京东APP 9.0 视觉规范
 * 支持按需加载
 * 详尽的文档和示例
 * 支持定制主题

+ 3 - 1
src/packages/backtop/demo.vue

@@ -24,7 +24,9 @@
     <div class="text-data">我是测试数据22</div>
     <div class="text-data">我是测试数据23</div>
     <div class="text-data">我是测试数据24</div>
-    <nut-backtop @click="handleClick" elId="elId" :distance="100" :bottom="90"><view>无</view></nut-backtop>
+    <nut-backtop @click="handleClick" elId="elId" :distance="100" :bottom="90"
+      ><view>无</view></nut-backtop
+    >
     <nut-backtop @click="handleClick" elId="elId" :distance="200"></nut-backtop>
   </div>
 </template>

+ 2 - 1
src/packages/backtop/index.vue

@@ -68,7 +68,8 @@ export default create({
       //滚动条偏移量
       //Window
       if (scrollEl instanceof Window) {
-        const scrollTop = scrollEl.pageYOffset !== undefined ? scrollEl.pageYOffset : '';
+        const scrollTop =
+          scrollEl.pageYOffset !== undefined ? scrollEl.pageYOffset : '';
         backTop.value = scrollTop >= props.distance;
         //DOM
       } else if (scrollEl instanceof HTMLElement) {

+ 1 - 1
src/packages/infiniteloading/demo.vue

@@ -74,7 +74,7 @@
 import { onMounted, ref, reactive, toRefs } from 'vue';
 import { createComponent } from '@/utils/create';
 const { createDemo } = createComponent('infiniteloading');
-import { Toast } from '../toast/toast';
+import { Toast } from '../toast';
 export default createDemo({
   props: {},
   setup() {

+ 1 - 3
src/packages/notify/index.scss

@@ -7,9 +7,6 @@
   overflow-y: auto;
   transition: transform 0.3s;
   z-index: 9999;
-  // &.round {
-  //   border-radius: 0 0 $popup-border-radius $popup-border-radius;
-  // }
 }
 
 .nut-notify {
@@ -20,6 +17,7 @@
   white-space: pre-wrap;
   text-align: center;
   word-wrap: break-word;
+  height: $notify-height;
   &--base {
     background: $notify-base-background-color;
   }

+ 100 - 30
src/packages/pullrefresh/demo.vue

@@ -1,35 +1,70 @@
 <template>
   <div class="demo">
-    <nut-pullrefresh @refresh="refresh" :useWindow="false" containerId="pull">
-      <div class="content" id="pull">
-        <div class="main">
-          <div class="text-data">我是测试数据1</div>
-          <div class="text-data">我是测试数据2</div>
-          <div class="text-data">我是测试数据3</div>
-          <div class="text-data">我是测试数据4</div>
-          <div class="text-data">我是测试数据5</div>
-          <div class="text-data">我是测试数据6</div>
-          <div class="text-data">我是测试数据7</div>
-          <div class="text-data">我是测试数据8</div>
-          <div class="text-data">我是测试数据9</div>
-          <div class="text-data">我是测试数据10</div>
-          <div class="text-data">我是测试数据11</div>
-          <div class="text-data">我是测试数据12</div>
-          <div class="text-data">我是测试数据13</div>
-          <div class="text-data">我是测试数据14</div>
-          <div class="text-data">我是测试数据15</div>
-          <div class="text-data">我是测试数据16</div>
-          <div class="text-data">我是测试数据17</div>
-          <div class="text-data">我是测试数据18</div>
-          <div class="text-data">我是测试数据19</div>
-          <div class="text-data">我是测试数据20</div>
-          <div class="text-data">我是测试数据21</div>
-          <div class="text-data">我是测试数据22</div>
-          <div class="text-data">我是测试数据23</div>
-          <div class="text-data">我是测试数据24</div>
-        </div>
+    <h2>纵向</h2>
+    <nut-cell>
+      <div class="vertical">
+        <nut-pullrefresh
+          @refresh="refresh"
+          :useWindow="false"
+          containerId="pull"
+        >
+          <div class="content" id="pull">
+            <div class="main">
+              <div class="text-data">我是测试数据1</div>
+              <div class="text-data">我是测试数据2</div>
+              <div class="text-data">我是测试数据3</div>
+              <div class="text-data">我是测试数据4</div>
+              <div class="text-data">我是测试数据5</div>
+              <div class="text-data">我是测试数据6</div>
+              <div class="text-data">我是测试数据7</div>
+              <div class="text-data">我是测试数据8</div>
+              <div class="text-data">我是测试数据9</div>
+              <div class="text-data">我是测试数据10</div>
+              <div class="text-data">我是测试数据11</div>
+              <div class="text-data">我是测试数据12</div>
+              <div class="text-data">我是测试数据13</div>
+              <div class="text-data">我是测试数据14</div>
+              <div class="text-data">我是测试数据15</div>
+              <div class="text-data">我是测试数据16</div>
+              <div class="text-data">我是测试数据17</div>
+              <div class="text-data">我是测试数据18</div>
+              <div class="text-data">我是测试数据19</div>
+              <div class="text-data">我是测试数据20</div>
+              <div class="text-data">我是测试数据21</div>
+              <div class="text-data">我是测试数据22</div>
+              <div class="text-data">我是测试数据23</div>
+              <div class="text-data">我是测试数据24</div>
+            </div>
+          </div>
+        </nut-pullrefresh>
       </div>
-    </nut-pullrefresh>
+    </nut-cell>
+
+    <h2>横向</h2>
+    <nut-cell>
+      <div class="horizontal">
+        <nut-pullrefresh
+          @refresh="refresh"
+          :useWindow="false"
+          containerId="pullH"
+          direction="horizontal"
+        >
+          <div class="contentH" id="pullH">
+            <div class="mainH">
+              <div class="text-data">我是测试数据1</div>
+              <div class="text-data">我是测试数据2</div>
+              <div class="text-data">我是测试数据3</div>
+              <div class="text-data">我是测试数据4</div>
+              <div class="text-data">我是测试数据5</div>
+              <div class="text-data">我是测试数据6</div>
+              <div class="text-data">我是测试数据7</div>
+              <div class="text-data">我是测试数据8</div>
+              <div class="text-data">我是测试数据9</div>
+            </div>
+          </div>
+        </nut-pullrefresh>
+      </div>
+    </nut-cell>
   </div>
 </template>
 
@@ -50,12 +85,47 @@ export default createDemo({
 </script>
 
 <style lang="scss" scoped>
+.vertical {
+  height: 300px;
+  overflow: hidden;
+}
 .content {
   height: 100%;
   overflow: auto;
   .main {
     padding: 10px 0;
-    background: #f00;
+  }
+}
+
+.horizontal {
+  width: 100%;
+}
+.contentH {
+  height: 100px;
+  overflow: auto;
+  .mainH {
+    display: flex;
+    height: 100%;
+    .text-data {
+      height: 90% !important;
+      width: 120px !important;
+      flex-shrink: 0;
+      padding: 0 !important;
+      margin: 2px 10px 0 0 !important;
+      justify-content: center;
+    }
+  }
+}
+.content {
+  .main {
+    .text-data {
+      &:first-child {
+        margin-top: 0 !important;
+      }
+      &:last-child {
+        margin-bottom: 0 !important;
+      }
+    }
   }
 }
 </style>

+ 30 - 6
src/packages/pullrefresh/index.css

@@ -10,17 +10,28 @@ view {
 .nut-pullrefresh .pullrefresh-top {
   position: absolute;
   left: 0;
-  width: 100%;
-  height: 50px;
   overflow: hidden;
   color: #969799;
   font-size: 14px;
   line-height: 50px;
   text-align: center;
+}
+
+.nut-pullrefresh .pullrefresh-top.pullrefresh-top-v {
+  width: 100%;
+  height: 50px;
   -webkit-transform: translateY(-100%);
   transform: translateY(-100%);
 }
 
+.nut-pullrefresh .pullrefresh-top.pullrefresh-top-h {
+  width: 50px;
+  height: 100%;
+  -webkit-transform: translateX(-100%);
+  transform: translateX(-100%);
+  writing-mode: tb-rl;
+}
+
 .nut-pullrefresh .pullrefresh-content {
   height: 100%;
   overflow: auto;
@@ -29,15 +40,28 @@ view {
 
 .nut-pullrefresh .pullrefresh-bottom {
   position: absolute;
-  left: 0;
-  bottom: 0;
-  width: 100%;
-  height: 0px;
   overflow: hidden;
   color: #969799;
   font-size: 14px;
   line-height: 50px;
   text-align: center;
+}
+
+.nut-pullrefresh .pullrefresh-bottom.pullrefresh-bottom-v {
+  left: 0;
+  bottom: 0;
+  width: 100%;
+  height: 0px;
   -webkit-transform: translateY(100%);
   transform: translateY(100%);
 }
+
+.nut-pullrefresh .pullrefresh-bottom.pullrefresh-bottom-h {
+  top: 0;
+  right: 0;
+  width: 50px;
+  height: 100%;
+  -webkit-transform: translateX(100%);
+  transform: translateX(100%);
+  writing-mode: tb-rl;
+}

ファイルの差分が大きいため隠しています
+ 1 - 1
src/packages/pullrefresh/index.min.css


+ 33 - 10
src/packages/pullrefresh/index.scss

@@ -9,15 +9,25 @@ view {
   .pullrefresh-top {
     position: absolute;
     left: 0;
-    width: 100%;
-    height: 50px;
+
     overflow: hidden;
     color: #969799;
     font-size: 14px;
     line-height: 50px;
     text-align: center;
-    -webkit-transform: translateY(-100%);
-    transform: translateY(-100%);
+    &.pullrefresh-top-v {
+      width: 100%;
+      height: 50px;
+      -webkit-transform: translateY(-100%);
+      transform: translateY(-100%);
+    }
+    &.pullrefresh-top-h {
+      width: 50px;
+      height: 100%;
+      -webkit-transform: translateX(-100%);
+      transform: translateX(-100%);
+      writing-mode: tb-rl;
+    }
   }
 
   .pullrefresh-content {
@@ -28,16 +38,29 @@ view {
 
   .pullrefresh-bottom {
     position: absolute;
-    left: 0;
-    bottom: 0;
-    width: 100%;
-    height: 0px;
+
     overflow: hidden;
     color: #969799;
     font-size: 14px;
     line-height: 50px;
     text-align: center;
-    -webkit-transform: translateY(100%);
-    transform: translateY(100%);
+
+    &.pullrefresh-bottom-v {
+      left: 0;
+      bottom: 0;
+      width: 100%;
+      height: 0px;
+      -webkit-transform: translateY(100%);
+      transform: translateY(100%);
+    }
+    &.pullrefresh-bottom-h {
+      top: 0;
+      right: 0;
+      width: 50px;
+      height: 100%;
+      -webkit-transform: translateX(100%);
+      transform: translateX(100%);
+      writing-mode: tb-rl;
+    }
   }
 }

+ 197 - 64
src/packages/pullrefresh/index.vue

@@ -7,31 +7,56 @@
     @touchmove="touchMove"
     @touchend="touchEnd"
   >
-    <view class="pullrefresh-top">
-      <template v-if="status == 'loading' && reachTop && distance > 0">
-        加载中...
-      </template>
-      <template v-if="status == 'pulling' && reachTop && distance > 0">
-        下拉刷新...
-      </template>
-      <template v-if="status == 'loosing' && reachTop && distance > 0">
-        释放刷新...
-      </template>
+    <view
+      class="pullrefresh-top"
+      :class="
+        direction == 'horizontal' ? 'pullrefresh-top-h' : 'pullrefresh-top-v'
+      "
+    >
+      <template
+        v-if="status == 'loading' && (reachTop || reachLeft) && distance > 0"
+        >{{ loadingText }}</template
+      >
+      <template
+        v-if="status == 'pulling' && (reachTop || reachLeft) && distance > 0"
+        >{{ pullingText }}</template
+      >
+      <template
+        v-if="status == 'loosing' && (reachTop || reachLeft) && distance > 0"
+        >{{ loosingText }}</template
+      >
     </view>
     <view class="pullrefresh-content" ref="pull">
       <slot></slot>
     </view>
 
-    <view class="pullrefresh-bottom" :style="getBottomStyle">
-      <template v-if="status == 'loading' && reachBottom && distance < 0">
-        加载中...
-      </template>
-      <template v-if="status == 'pulling' && reachBottom && distance < 0">
-        下拉刷新...
-      </template>
-      <template v-if="status == 'loosing' && reachBottom && distance < 0">
-        释放刷新...
-      </template>
+    <view
+      class="pullrefresh-bottom"
+      :class="
+        direction == 'horizontal'
+          ? 'pullrefresh-bottom-h'
+          : 'pullrefresh-bottom-v'
+      "
+      :style="getBottomStyle"
+    >
+      <template
+        v-if="
+          status == 'loading' && (reachBottom || reachRight) && distance < 0
+        "
+        >{{ loadingText }}</template
+      >
+      <template
+        v-if="
+          status == 'pulling' && (reachBottom || reachRight) && distance < 0
+        "
+        >{{ pullingText }}</template
+      >
+      <template
+        v-if="
+          status == 'loosing' && (reachBottom || reachRight) && distance < 0
+        "
+        >{{ loosingText }}</template
+      >
     </view>
   </view>
 </template>
@@ -52,6 +77,28 @@ export default create({
     containerId: {
       type: String,
       default: ''
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    /** 方向 Horizontal */
+    direction: {
+      type: String,
+      default: 'vertical'
+    },
+
+    pullingText: {
+      type: String,
+      default: '下拉刷新'
+    },
+    loosingText: {
+      type: String,
+      default: '松手释放刷新'
+    },
+    loadingText: {
+      type: String,
+      default: '加载中...'
     }
   },
   components: {},
@@ -60,11 +107,14 @@ export default create({
   setup(props, { emit }) {
     console.log('componentName', componentName);
 
-    const { containerId, useWindow } = toRefs(props);
+    const { containerId, useWindow, direction, disabled } = toRefs(props);
 
     const reachTop = ref(false);
     const reachBottom = ref(false);
 
+    const reachLeft = ref(false);
+    const reachRight = ref(false);
+
     const state = reactive({
       status: 'normal',
       distance: 0,
@@ -78,10 +128,12 @@ export default create({
 
     const getStyle = computed(() => {
       let style: CSSProperties = {};
-
+      const { deltaY, deltaX } = touch;
       if (
-        (reachTop.value && touch.deltaY.value > 0 && touch.isVertical()) ||
-        (reachBottom.value && touch.deltaY.value < 0 && touch.isVertical())
+        direction.value == 'vertical' &&
+        ((reachTop.value && deltaY.value > 0) ||
+          (reachBottom.value && deltaY.value < 0)) &&
+        touch.isVertical()
       ) {
         style = {
           transitionDuration: `${state.duration}ms`,
@@ -90,20 +142,53 @@ export default create({
             : `translate3d(0,0,0)`
         };
       }
+      if (
+        direction.value == 'horizontal' &&
+        ((reachLeft.value && deltaX.value > 0) ||
+          (reachRight.value && deltaX.value < 0)) &&
+        touch.isHorizontal()
+      ) {
+        style = {
+          transitionDuration: `${state.duration}ms`,
+          transform: state.distance
+            ? `translate3d(${state.distance}px, 0,0)`
+            : `translate3d(0,0,0)`
+        };
+      }
+
       return style;
     });
 
     const getBottomStyle = computed(() => {
       let style: CSSProperties = {};
-      if (reachBottom.value && touch.deltaY.value < 0 && touch.isVertical()) {
+      if (
+        direction.value == 'vertical' &&
+        reachBottom.value &&
+        touch.deltaY.value < 0 &&
+        touch.isVertical()
+      ) {
         const dis = Math.abs(state.distance) < 50 ? -state.distance : 50;
         style = {
           height: dis + 'px'
         };
       }
+
+      if (
+        direction.value == 'horizontal' &&
+        reachRight.value &&
+        touch.deltaX.value < 0 &&
+        touch.isVertical()
+      ) {
+        const dis = Math.abs(state.distance) < 50 ? -state.distance : 50;
+        style = {
+          width: dis + 'px'
+        };
+      }
       return style;
     });
 
+    const isTouchable = () => state.status !== 'loading' && !disabled.value;
+
     const setStatus = (distance: number, isLoading?: boolean) => {
       state.distance = distance;
 
@@ -157,54 +242,100 @@ export default create({
     };
 
     const touchStart = event => {
-      /** 判断滚动条是否在顶部 */
-      const top = 'scrollTop' in scrollEl ? scrollEl.scrollTop : 0;
-      reachTop.value = Math.max(top, 0) == 0 ? true : false;
-
-      if (reachTop.value) {
-        state.duration = 0;
-        touch.start(event);
-      }
-
-      const { scrollHeight, clientHeight, scrollTop } = scrollEl;
-
-      /** 判断滚动条是否在底部*/
-      reachBottom.value =
-        clientHeight + scrollTop == scrollHeight ? true : false;
-
-      if (reachBottom.value) {
-        state.duration = 0;
-        touch.start(event);
+      if (isTouchable()) {
+        if (direction.value == 'vertical') {
+          /** 判断滚动条是否在顶部 */
+          const top = 'scrollTop' in scrollEl ? scrollEl.scrollTop : 0;
+          reachTop.value = Math.max(top, 0) == 0 ? true : false;
+
+          /** 判断滚动条是否在底部*/
+          const { scrollHeight, clientHeight, scrollTop } = scrollEl;
+          reachBottom.value =
+            clientHeight + scrollTop == scrollHeight ? true : false;
+
+          if (reachTop.value || reachBottom.value) {
+            state.duration = 0;
+            touch.start(event);
+          }
+        } else {
+          const { scrollWidth, clientWidth, scrollLeft } = scrollEl;
+          /** 判断滚动条是否在左边 */
+          const left = 'scrollLeft' in scrollEl ? scrollEl.scrollLeft : 0;
+          reachLeft.value = Math.max(left, 0) == 0 ? true : false;
+
+          /** 判断滚动条是否在右边 */
+          reachRight.value =
+            clientWidth + scrollLeft == scrollWidth ? true : false;
+
+          if (reachLeft.value || reachRight.value) {
+            state.duration = 0;
+            touch.start(event);
+          }
+        }
       }
     };
 
     const touchMove = event => {
-      const { deltaY } = touch;
-
-      touch.move(event);
-      if (reachTop.value && deltaY.value >= 0 && touch.isVertical()) {
-        preventDefault(event);
-        setStatus(ease(deltaY.value));
-      }
+      if (isTouchable()) {
+        const { deltaY, deltaX } = touch;
+        touch.move(event);
+
+        if (
+          direction.value == 'vertical' &&
+          ((reachBottom.value && deltaY.value < 0) ||
+            (reachTop.value && deltaY.value >= 0)) &&
+          touch.isVertical()
+        ) {
+          preventDefault(event);
+          setStatus(ease(deltaY.value));
+        }
 
-      if (reachBottom.value && deltaY.value < 0 && touch.isVertical()) {
-        preventDefault(event);
-        setStatus(ease(deltaY.value));
+        if (
+          direction.value == 'horizontal' &&
+          ((reachLeft.value && deltaX.value >= 0) ||
+            (reachRight.value && deltaX.value < 0)) &&
+          touch.isHorizontal()
+        ) {
+          preventDefault(event);
+          setStatus(ease(deltaX.value));
+        }
       }
     };
     const touchEnd = () => {
-      if (reachTop.value && touch.deltaY.value > 0) {
-        if (state.status === 'loosing') {
-          setStatus(50, true);
-          emit('refresh', refreshDone);
-        } else {
-          setStatus(0);
-        }
-      }
-
-      if (reachBottom.value && touch.deltaY.value < 0) {
+      if (isTouchable()) {
+        const { deltaY, deltaX } = touch;
         if (state.status === 'loosing') {
-          setStatus(-50, true);
+          let dis = 0;
+
+          if (
+            direction.value == 'vertical' &&
+            reachTop.value &&
+            deltaY.value > 0
+          ) {
+            dis = 50;
+          }
+          if (
+            direction.value == 'vertical' &&
+            reachBottom.value &&
+            deltaY.value < 0
+          ) {
+            dis = -50;
+          }
+          if (
+            direction.value == 'horizontal' &&
+            reachLeft.value &&
+            deltaX.value > 0
+          ) {
+            dis = 50;
+          }
+          if (
+            direction.value == 'horizontal' &&
+            reachRight.value &&
+            deltaX.value < 0
+          ) {
+            dis = -50;
+          }
+          setStatus(dis, true);
           emit('refresh', refreshDone);
         } else {
           setStatus(0);
@@ -219,6 +350,8 @@ export default create({
       getStyle,
       reachBottom,
       reachTop,
+      reachRight,
+      reachLeft,
       getBottomStyle,
       ...toRefs(state)
     };

+ 62 - 0
src/packages/range/demo.vue

@@ -0,0 +1,62 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <nut-cell class="cell">
+      <nut-range v-model="value" @change="onChange"></nut-range>
+    </nut-cell>
+    <h2>指定选择范围</h2>
+    <nut-cell class="cell">
+      <nut-range
+        v-model="value2"
+        max="10"
+        min="-10"
+        @change="onChange2"
+      ></nut-range>
+    </nut-cell>
+    <h2>设置步长</h2>
+    <nut-cell class="cell">
+      <nut-range v-model="value3" step="5" @change="onChange3"></nut-range>
+    </nut-cell>
+    <h2>禁用</h2>
+    <nut-cell class="cell">
+      <nut-range disabled v-model="value4"></nut-range>
+    </nut-cell>
+  </div>
+</template>
+
+<script lang="ts">
+import { ref } from 'vue';
+import { createComponent } from '@/utils/create';
+import { Toast } from '../toast';
+
+const { createDemo } = createComponent('range');
+export default createDemo({
+  props: {},
+  setup() {
+    const value = ref(50);
+    const value2 = ref(5);
+    const value3 = ref(50);
+    const value4 = ref(50);
+    const onChange = value => Toast.text('当前值:' + value);
+    const onChange2 = value2 => Toast.text('当前值:' + value2);
+    const onChange3 = value3 => Toast.text('当前值:' + value3);
+    return {
+      value,
+      value2,
+      value3,
+      value4,
+      onChange,
+      onChange2,
+      onChange3
+    };
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.cell {
+  padding: 30px 18px;
+}
+.nut-range {
+}
+</style>

+ 34 - 0
src/packages/range/doc.md

@@ -0,0 +1,34 @@
+#  range组件
+
+    ### 介绍
+    
+    基于 xxxxxxx
+    
+    ### 安装
+    
+    
+    
+    ## 代码演示
+    
+    ### 基础用法1
+    
+
+    
+    ## API
+    
+    ### Props
+    
+    | 参数         | 说明                             | 类型   | 默认值           |
+    |--------------|----------------------------------|--------|------------------|
+    | name         | 图标名称或图片链接               | String | -                |
+    | color        | 图标颜色                         | String | -                |
+    | size         | 图标大小,如 '20px' '2em' '2rem' | String | -                |
+    | class-prefix | 类名前缀,用于使用自定义图标     | String | 'nutui-iconfont' |
+    | tag          | HTML 标签                        | String | 'i'              |
+    
+    ### Events
+    
+    | 事件名 | 说明           | 回调参数     |
+    |--------|----------------|--------------|
+    | click  | 点击图标时触发 | event: Event |
+    

+ 73 - 0
src/packages/range/index.scss

@@ -0,0 +1,73 @@
+.nut-range {
+  display: block;
+  position: relative;
+  width: 100%;
+  height: 3px;
+  background-color: rgba(255, 163, 154, 1);
+  border-radius: 2px;
+  cursor: pointer;
+  &::before {
+    position: absolute;
+    top: -8px;
+    right: 0;
+    bottom: -8px;
+    left: 0;
+    content: '';
+  }
+
+  &-bar {
+    display: block;
+    position: relative;
+    width: 100%;
+    height: 100%;
+    background: linear-gradient(
+      135deg,
+      rgba(250, 44, 25, 1) 0%,
+      rgba(250, 63, 25, 1) 45%,
+      rgba(250, 89, 25, 1) 83%,
+      rgba(250, 100, 25, 1) 100%
+    );
+    border-radius: inherit;
+    transition: all 0.2s;
+  }
+
+  &-button {
+    display: block;
+    width: 24px;
+    height: 24px;
+    background-color: #fff;
+    border-radius: 50%;
+    box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.15);
+    border: 1px solid rgba(250, 44, 25, 1);
+    outline: none;
+
+    &-wrapper,
+    &-wrapper-right {
+      position: absolute;
+      top: 50%;
+      right: 0;
+      transform: translate3d(50%, -50%, 0);
+      cursor: grab;
+      outline: none;
+    }
+
+    &-wrapper-left {
+      position: absolute;
+      top: 50%;
+      left: 0;
+      transform: translate3d(-50%, -50%, 0);
+      cursor: grab;
+      outline: none;
+    }
+  }
+  &-disabled {
+    cursor: not-allowed;
+    opacity: 0.54;
+
+    .nut-range-button-wrapper,
+    .nut-range-button-wrapper-left,
+    .nut-range-button-wrapper-right {
+      cursor: not-allowed;
+    }
+  }
+}

+ 255 - 0
src/packages/range/index.vue

@@ -0,0 +1,255 @@
+<template>
+  <view ref="root" :style="wrapperStyle" :class="classes" @click.stop="onClick">
+    <view class="nut-range-bar" :style="barStyle">
+      <view
+        role="slider"
+        class="nut-range-button-wrapper"
+        :tabindex="disabled ? -1 : 0"
+        :aria-valuemin="+min"
+        :aria-valuenow="curValue()"
+        :aria-valuemax="+max"
+        aria-orientation="horizontal"
+        @touchstart.stop.prevent="onTouchStart"
+        @touchmove.stop.prevent="onTouchMove"
+        @touchend.stop.prevent="onTouchEnd"
+        @touchcancel.stop.prevent="onTouchEnd"
+        @click="e => e.stopPropagation()"
+      >
+        <slot v-if="$slots.button"></slot>
+        <view class="nut-range-button" v-else></view>
+      </view>
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { ref, toRefs, computed, PropType, CSSProperties } from 'vue';
+import { createComponent } from '@/utils/create';
+import { useTouch } from '@/utils/useTouch';
+import { useRect } from '@/utils/useRect';
+const { componentName, create } = createComponent('range');
+
+type SliderValue = number | number[];
+
+export default create({
+  props: {
+    range: {
+      type: Boolean,
+      default: false
+    },
+    disabled: Boolean,
+    barHeight: [Number, String],
+    activeColor: String,
+    inactiveColor: String,
+    min: {
+      type: [Number, String],
+      default: 0
+    },
+    max: {
+      type: [Number, String],
+      default: 100
+    },
+    step: {
+      type: [Number, String],
+      default: 1
+    },
+    modelValue: {
+      type: [Number, Array] as PropType<SliderValue>,
+      default: 0
+    }
+  },
+  components: {},
+  emits: ['change', 'drag-end', 'drag-start', 'update:modelValue'],
+
+  setup(props, { emit }) {
+    let buttonIndex: number;
+    let startValue: SliderValue;
+    let currentValue: SliderValue;
+
+    const root = ref<HTMLElement>();
+    const dragStatus = ref<'start' | 'draging' | ''>();
+    const touch = useTouch();
+
+    const scope = computed(() => Number(props.max) - Number(props.min));
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true,
+        [`${prefixCls}-disabled`]: props.disabled
+      };
+    });
+
+    const wrapperStyle = computed(() => {
+      return {
+        background: props.inactiveColor
+      };
+    });
+
+    const isRange = (val: unknown): val is number[] =>
+      !!props.range && Array.isArray(val);
+
+    // 计算选中条的长度百分比
+    const calcMainAxis = () => {
+      const { modelValue, min } = props;
+      if (isRange(modelValue)) {
+        return `${((modelValue[1] - modelValue[0]) * 100) / scope.value}%`;
+      }
+      return `${((modelValue - Number(min)) * 100) / scope.value}%`;
+    };
+
+    // 计算选中条的开始位置的偏移量
+    const calcOffset = () => {
+      const { modelValue, min } = props;
+      if (isRange(modelValue)) {
+        return `${((modelValue[0] - Number(min)) * 100) / scope.value}%`;
+      }
+      return `0%`;
+    };
+
+    const barStyle = computed<CSSProperties>(() => {
+      return {
+        width: calcMainAxis(),
+        left: calcOffset(),
+        background: props.activeColor,
+        transition: dragStatus.value ? 'none' : undefined
+      };
+    });
+
+    const format = (value: number) => {
+      const { min, max, step } = props;
+      value = Math.max(+min, Math.min(value, +max)); // 拖动范围限制
+      return Math.round(value / +step) * +step; // 每一步四舍五入
+    };
+
+    const isSameValue = (newValue: SliderValue, oldValue: SliderValue) =>
+      JSON.stringify(newValue) === JSON.stringify(oldValue);
+
+    // 处理两个滑块重叠之后的情况
+    const handleOverlap = (value: number[]) => {
+      if (value[0] > value[1]) {
+        return value.slice(0).reverse();
+      }
+      return value;
+    };
+
+    const updateValue = (value: SliderValue, end?: boolean) => {
+      if (isRange(value)) {
+        value = handleOverlap(value).map(format);
+      } else {
+        value = format(value);
+      }
+
+      if (!isSameValue(value, props.modelValue)) {
+        emit('update:modelValue', value);
+      }
+
+      if (end && !isSameValue(value, startValue)) {
+        emit('change', value);
+      }
+    };
+
+    const onClick = (event: MouseEvent) => {
+      if (props.disabled) {
+        return;
+      }
+
+      const { min, modelValue } = props;
+      const rect = useRect(root);
+      const delta = event.clientX - rect.left;
+      const total = rect.width;
+      const value = Number(min) + (delta / total) * scope.value;
+      if (isRange(modelValue)) {
+        const [left, right] = modelValue;
+        const middle = (left + right) / 2;
+
+        if (value <= middle) {
+          updateValue([value, right], true);
+        } else {
+          updateValue([left, value], true);
+        }
+      } else {
+        updateValue(value, true);
+      }
+    };
+
+    const onTouchStart = (event: TouchEvent) => {
+      if (props.disabled) {
+        return;
+      }
+
+      touch.start(event);
+      currentValue = props.modelValue;
+
+      if (isRange(currentValue)) {
+        startValue = currentValue.map(format);
+      } else {
+        startValue = format(currentValue);
+      }
+
+      dragStatus.value = 'start';
+    };
+
+    const onTouchMove = (event: TouchEvent) => {
+      if (props.disabled) {
+        return;
+      }
+
+      if (dragStatus.value === 'start') {
+        emit('drag-start');
+      }
+
+      touch.move(event);
+      dragStatus.value = 'draging';
+
+      const rect = useRect(root);
+      const delta = touch.deltaX.value;
+      const total = rect.width;
+      const diff = (delta / total) * scope.value;
+
+      if (isRange(startValue)) {
+        (currentValue as number[])[buttonIndex] =
+          startValue[buttonIndex] + diff;
+      } else {
+        currentValue = startValue + diff;
+      }
+      updateValue(currentValue);
+    };
+
+    const onTouchEnd = () => {
+      if (props.disabled) {
+        return;
+      }
+      if (dragStatus.value === 'draging') {
+        updateValue(currentValue, true);
+        emit('drag-end');
+      }
+      dragStatus.value = '';
+    };
+
+    const curValue = (idx?) => {
+      const value =
+        typeof idx === 'number'
+          ? (props.modelValue as number[])[idx]
+          : (props.modelValue as number);
+      return value;
+    };
+
+    return {
+      root,
+      classes,
+      wrapperStyle,
+      onClick,
+      onTouchStart,
+      onTouchMove,
+      onTouchEnd,
+      ...toRefs(props),
+      barStyle,
+      curValue
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 1 - 1
src/packages/toast/demo.vue

@@ -34,7 +34,7 @@
 
 <script>
 import { createComponent } from '@/utils/create';
-import { Toast } from './toast';
+import { Toast } from './index.ts';
 const { createDemo } = createComponent('toast');
 
 export default createDemo({

+ 2 - 1
src/packages/toast/toast.ts

@@ -126,5 +126,6 @@ export const Toast = {
 export default {
   install(app: App): void {
     app.config.globalProperties.$toast = Toast;
-  }
+  },
+  Toast
 };

+ 26 - 17
src/sites/doc/components/Header.vue

@@ -10,24 +10,24 @@
       <div class="nav-box">
         <ul class="nav-list">
           <li class="nav-item" :class="{ active: isActive(header[0].name) }">
-            <router-link :to="header[0].name">{{
-              header[0].cName
-            }}</router-link>
+            <router-link :to="header[0].name">
+              {{ header[0].cName }}
+            </router-link>
           </li>
           <li class="nav-item" :class="{ active: isActive(header[1].name) }">
-            <router-link :to="header[1].name">{{
-              header[1].cName
-            }}</router-link>
+            <router-link :to="header[1].name">
+              {{ header[1].cName }}
+            </router-link>
+          </li>
+          <li class="nav-item" :class="{ active: isActive(header[2].name) }">
+            <a href="http://localhost:8080/demo.html#/">
+              {{ header[2].cName }}
+            </a>
           </li>
-          <li class="nav-item" :class="{ active: isActive(header[2].name) }"
-            ><a href="http://localhost:8080/demo.html#/">{{
-              header[2].cName
-            }}</a></li
-          >
           <li class="nav-item" :class="{ active: isActive(header[3].name) }">
-            <router-link :to="header[3].name">{{
-              header[3].cName
-            }}</router-link>
+            <router-link :to="header[3].name">
+              {{ header[3].cName }}
+            </router-link>
           </li>
           <li class="nav-item">
             <div
@@ -234,7 +234,7 @@ export default defineComponent({
         height: 26px;
         vertical-align: middle;
         background: url('../../assets/images/icon-user.png') no-repeat;
-        background-size: 100%;
+        background-size: 26px;
       }
     }
   }
@@ -322,6 +322,9 @@ export default defineComponent({
           }
           .user-link {
             background-position: 0 0;
+            // &:hover {
+            //   background-position: -26px 0;
+            // }
           }
         }
       }
@@ -397,7 +400,10 @@ export default defineComponent({
             }
           }
           .user-link {
-            background-position: 0 -26px;
+            background-position: 0 -25px;
+            // &:hover {
+            //   background-position: -26px -25px;
+            // }
           }
         }
       }
@@ -473,7 +479,10 @@ export default defineComponent({
             }
           }
           .user-link {
-            background-position: 0 -52px;
+            background-position: 0 -51px;
+            // &:hover {
+            //   background-position: -26px -51px;
+            // }
           }
         }
       }

+ 30 - 5
src/sites/doc/components/Search.vue

@@ -20,7 +20,6 @@
           {{ item.name }}
           <span>{{ item.cName }}</span>
         </router-link>
-        <!-- <router-link v-if="!_package.isLink" :to="_package.name.toLowerCase()">{{ _package.cName }}</router-link> -->
       </li>
     </ul>
   </div>
@@ -57,7 +56,6 @@ export default defineComponent({
     watch(
       () => data.searchVal,
       sVal => {
-        console.log(sVal, '改变');
         if (sVal) {
           data.searchList = data.navList.filter(item => {
             if (item.show === false) return false;
@@ -83,11 +81,38 @@ export default defineComponent({
         data.searchVal = '';
       }, 200);
     };
-    const checklist = item => {
-      console.log(1);
+    const checklist = () => {
+      data.searchVal = '';
+      data.searchCurName = '';
+      data.searchIndex = 0;
     };
     const choseList = e => {
-      data.searchIndex = 0;
+      let searchIndex = data.searchIndex;
+      if (e.keyCode == 40) {
+        searchIndex++;
+      }
+      if (e.keyCode == 38) {
+        searchIndex--;
+      }
+      if (searchIndex < 0) {
+        searchIndex = 0;
+      }
+      const searchList = data.searchList;
+      if (searchList.length > 0) {
+        const cName = searchList[searchIndex] && searchList[searchIndex].name;
+        if (cName) {
+          data.searchCurName = cName;
+          data.searchIndex = searchIndex;
+          if (e.keyCode == 13) {
+            data.$router.push({
+              path: '/' + searchList[searchIndex].name
+            });
+            data.searchCurName = '';
+            data.searchIndex = 0;
+            data.searchVal = '';
+          }
+        }
+      }
     };
     return {
       data,

+ 8 - 14
src/sites/doc/views/Resource.vue

@@ -8,7 +8,7 @@
   </div>
   <!-- 设计资源 -->
   <div class="resource-content">
-    <div class="resource-block" v-if="data.articleList.length === 0">
+    <div class="resource-block" v-if="articleList.length === 0">
       <h4 class="sub-title">设计资源</h4>
       <p class="sub-desc"
         >这里提供 NUT UI
@@ -33,18 +33,18 @@
         <div class="tab-hd">
           <div
             class="tab-hd-item"
-            :class="{ active: data.activeIndex === index }"
-            v-for="(item, index) in data.tabData"
+            :class="{ active: activeIndex === index }"
+            v-for="(item, index) in tabData"
             :key="index"
             @click="clickTab(index)"
           >
             {{ item.title }}
           </div>
         </div>
-        <div class="tab-bd" v-show="data.activeIndex === 0">
+        <div class="tab-bd" v-show="activeIndex === 0">
           <div
             class="design-item"
-            v-for="item in data.articleList"
+            v-for="item in articleList"
             :key="item.id"
             @click="toLink(item.id)"
           >
@@ -52,7 +52,7 @@
             <p class="design-title">{{ item.title }}</p>
           </div>
         </div>
-        <div class="tab-bd" v-show="data.activeIndex === 1">
+        <div class="tab-bd" v-show="activeIndex === 1">
           <div class="design-item">
             <img class="img-design" src="../../assets/images/img-article.jpg" />
             <p class="design-title"
@@ -93,7 +93,7 @@
   <doc-footer></doc-footer>
 </template>
 <script lang="ts">
-import { defineComponent, onMounted, reactive } from 'vue';
+import { defineComponent, onMounted, reactive, toRefs } from 'vue';
 import {
   onBeforeRouteUpdate,
   RouteLocationNormalized,
@@ -116,12 +116,6 @@ export default defineComponent({
       tabData: [
         {
           title: '全部文章'
-        },
-        {
-          title: '性能体验'
-        },
-        {
-          title: '性能体验1'
         }
         // {
         //   title: '性能体验'
@@ -158,7 +152,7 @@ export default defineComponent({
       window.open('//jelly.jd.com/article/' + id);
     };
     return {
-      data,
+      ...toRefs(data),
       clickTab,
       toLink
     };

+ 2 - 0
src/sites/mobile/main.ts

@@ -3,6 +3,8 @@ import App from './App.vue';
 import router from './router';
 import NutUI from '@/nutui';
 import '@/sites/assets/styles/reset.scss';
+import '@/utils/touchEmulator';
+
 createApp(App)
   .use(router)
   .use(NutUI)

ファイルの差分が大きいため隠しています
+ 63 - 1
src/styles/font/iconfont.js


+ 3 - 2
src/styles/variables.scss

@@ -152,8 +152,9 @@ $zindex-picker: 10050 !default;
 
 // Notify
 $notify-text-color: #fff;
-$notify-padding: 8px 16px;
-$notify-font-size: 28px;
+$notify-padding: 12px 0;
+$notify-font-size: 14px;
+$notify-height: 44px;
 
 $notify-base-background-color: linear-gradient(
   135deg,

+ 197 - 0
src/utils/touchEmulator.js

@@ -0,0 +1,197 @@
+/* eslint-disable */
+/**
+ * Emulate touch event
+ * Source:https://github.com/hammerjs/touchemulator
+ */
+
+var eventTarget;
+var supportTouch = 'ontouchstart' in window;
+
+// polyfills
+if (!document.createTouch) {
+  document.createTouch = function(
+    view,
+    target,
+    identifier,
+    pageX,
+    pageY,
+    screenX,
+    screenY
+  ) {
+    // auto set
+    return new Touch(
+      target,
+      identifier,
+      {
+        pageX: pageX,
+        pageY: pageY,
+        screenX: screenX,
+        screenY: screenY,
+        clientX: pageX - window.pageXOffset,
+        clientY: pageY - window.pageYOffset
+      },
+      0,
+      0
+    );
+  };
+}
+
+if (!document.createTouchList) {
+  document.createTouchList = function() {
+    var touchList = TouchList();
+    for (var i = 0; i < arguments.length; i++) {
+      touchList[i] = arguments[i];
+    }
+    touchList.length = arguments.length;
+    return touchList;
+  };
+}
+
+/**
+ * create an touch point
+ * @constructor
+ * @param target
+ * @param identifier
+ * @param pos
+ * @param deltaX
+ * @param deltaY
+ * @returns {Object} touchPoint
+ */
+
+var Touch = function Touch(target, identifier, pos, deltaX, deltaY) {
+  deltaX = deltaX || 0;
+  deltaY = deltaY || 0;
+
+  this.identifier = identifier;
+  this.target = target;
+  this.clientX = pos.clientX + deltaX;
+  this.clientY = pos.clientY + deltaY;
+  this.screenX = pos.screenX + deltaX;
+  this.screenY = pos.screenY + deltaY;
+  this.pageX = pos.pageX + deltaX;
+  this.pageY = pos.pageY + deltaY;
+};
+
+/**
+ * create empty touchlist with the methods
+ * @constructor
+ * @returns touchList
+ */
+function TouchList() {
+  var touchList = [];
+
+  touchList['item'] = function(index) {
+    return this[index] || null;
+  };
+
+  // specified by Mozilla
+  touchList['identifiedTouch'] = function(id) {
+    return this[id + 1] || null;
+  };
+
+  return touchList;
+}
+
+/**
+ * only trigger touches when the left mousebutton has been pressed
+ * @param touchType
+ * @returns {Function}
+ */
+
+var initiated = false;
+function onMouse(touchType) {
+  return function(ev) {
+    // prevent mouse events
+
+    if (ev.type === 'mousedown') {
+      initiated = true;
+    }
+
+    if (ev.type === 'mouseup') {
+      initiated = false;
+    }
+
+    if (ev.type === 'mousemove' && !initiated) {
+      return;
+    }
+
+    // The EventTarget on which the touch point started when it was first placed on the surface,
+    // even if the touch point has since moved outside the interactive area of that element.
+    // also, when the target doesnt exist anymore, we update it
+    if (
+      ev.type === 'mousedown' ||
+      !eventTarget ||
+      (eventTarget && !eventTarget.dispatchEvent)
+    ) {
+      eventTarget = ev.target;
+    }
+
+    triggerTouch(touchType, ev);
+
+    // reset
+    if (ev.type === 'mouseup') {
+      eventTarget = null;
+    }
+  };
+}
+
+/**
+ * trigger a touch event
+ * @param eventName
+ * @param mouseEv
+ */
+function triggerTouch(eventName, mouseEv) {
+  var touchEvent = document.createEvent('Event');
+  touchEvent.initEvent(eventName, true, true);
+
+  touchEvent.altKey = mouseEv.altKey;
+  touchEvent.ctrlKey = mouseEv.ctrlKey;
+  touchEvent.metaKey = mouseEv.metaKey;
+  touchEvent.shiftKey = mouseEv.shiftKey;
+
+  touchEvent.touches = getActiveTouches(mouseEv);
+  touchEvent.targetTouches = getActiveTouches(mouseEv);
+  touchEvent.changedTouches = createTouchList(mouseEv);
+
+  eventTarget.dispatchEvent(touchEvent);
+}
+
+/**
+ * create a touchList based on the mouse event
+ * @param mouseEv
+ * @returns {TouchList}
+ */
+function createTouchList(mouseEv) {
+  var touchList = TouchList();
+  touchList.push(new Touch(eventTarget, 1, mouseEv, 0, 0));
+  return touchList;
+}
+
+/**
+ * receive all active touches
+ * @param mouseEv
+ * @returns {TouchList}
+ */
+function getActiveTouches(mouseEv) {
+  // empty list
+  if (mouseEv.type === 'mouseup') {
+    return TouchList();
+  }
+  return createTouchList(mouseEv);
+}
+
+/**
+ * TouchEmulator initializer
+ */
+function TouchEmulator() {
+  window.addEventListener('mousedown', onMouse('touchstart'), true);
+  window.addEventListener('mousemove', onMouse('touchmove'), true);
+  window.addEventListener('mouseup', onMouse('touchend'), true);
+}
+
+// start distance when entering the multitouch mode
+TouchEmulator['multiTouchOffset'] = 75;
+
+if (!supportTouch) {
+  new TouchEmulator();
+}

+ 48 - 0
src/utils/useRect/index.ts

@@ -0,0 +1,48 @@
+/**
+  获取元素的大小及其相对于视口的位置,等价于 Element.getBoundingClientRect。
+  width 宽度	number
+  height 高度	number
+  top	顶部与视图窗口左上角的距离	number
+  left	左侧与视图窗口左上角的距离	number
+  right	右侧与视图窗口左上角的距离	number
+  bottom	底部与视图窗口左上角的距离	number
+ */
+
+import { Ref, unref } from 'vue';
+
+function isWindow(val: unknown): val is Window {
+  return val === window;
+}
+
+export const useRect = (
+  elementRef: (Element | Window) | Ref<Element | Window | undefined>
+) => {
+  const element = unref(elementRef);
+
+  if (isWindow(element)) {
+    const width = element.innerWidth;
+    const height = element.innerHeight;
+
+    return {
+      top: 0,
+      left: 0,
+      right: width,
+      bottom: height,
+      width,
+      height
+    };
+  }
+
+  if (element && element.getBoundingClientRect) {
+    return element.getBoundingClientRect();
+  }
+
+  return {
+    top: 0,
+    left: 0,
+    right: 0,
+    bottom: 0,
+    width: 0,
+    height: 0
+  };
+};

+ 69 - 0
src/utils/useTouch/index.ts

@@ -0,0 +1,69 @@
+import { ref } from 'vue';
+
+const MIN_DISTANCE = 10;
+
+type Direction = '' | 'vertical' | 'horizontal';
+
+function getDirection(x: number, y: number) {
+  if (x > y && x > MIN_DISTANCE) {
+    return 'horizontal';
+  }
+  if (y > x && y > MIN_DISTANCE) {
+    return 'vertical';
+  }
+  return '';
+}
+
+export function useTouch() {
+  const startX = ref(0);
+  const startY = ref(0);
+  const deltaX = ref(0);
+  const deltaY = ref(0);
+  const offsetX = ref(0);
+  const offsetY = ref(0);
+  const direction = ref<Direction>('');
+
+  const isVertical = () => direction.value === 'vertical';
+  const isHorizontal = () => direction.value === 'horizontal';
+
+  const reset = () => {
+    deltaX.value = 0;
+    deltaY.value = 0;
+    offsetX.value = 0;
+    offsetY.value = 0;
+    direction.value = '';
+  };
+
+  const start = ((event: TouchEvent) => {
+    reset();
+    startX.value = event.touches[0].clientX;
+    startY.value = event.touches[0].clientY;
+  }) as EventListener;
+
+  const move = ((event: TouchEvent) => {
+    const touch = event.touches[0];
+    deltaX.value = touch.clientX - startX.value;
+    deltaY.value = touch.clientY - startY.value;
+    offsetX.value = Math.abs(deltaX.value);
+    offsetY.value = Math.abs(deltaY.value);
+
+    if (!direction.value) {
+      direction.value = getDirection(offsetX.value, offsetY.value);
+    }
+  }) as EventListener;
+
+  return {
+    move,
+    start,
+    reset,
+    startX,
+    startY,
+    deltaX,
+    deltaY,
+    offsetX,
+    offsetY,
+    direction,
+    isVertical,
+    isHorizontal
+  };
+}