Browse Source

feat: range开发

zy19940510 4 years ago
parent
commit
d26fcaf098
4 changed files with 232 additions and 80 deletions
  1. 60 22
      src/packages/range/demo.vue
  2. 31 32
      src/packages/range/doc.md
  3. 28 0
      src/packages/range/index.scss
  4. 113 26
      src/packages/range/index.vue

+ 60 - 22
src/packages/range/demo.vue

@@ -2,30 +2,60 @@
   <div class="demo">
     <h2>基础用法</h2>
     <nut-cell class="cell">
-      <nut-range v-model="value" @change="onChange"></nut-range>
+      <nut-range v-model="value1" @change="onChange"></nut-range>
     </nut-cell>
-    <h2>指定选择范围</h2>
+    <h2>双滑块</h2>
+    <nut-cell class="cell">
+      <nut-range range v-model="value2" @change="onChange"></nut-range>
+    </nut-cell>
+    <h2>指定范围</h2>
     <nut-cell class="cell">
       <nut-range
-        v-model="value2"
+        v-model="value3"
         max="10"
         min="-10"
-        @change="onChange2"
+        @change="onChange"
       ></nut-range>
     </nut-cell>
     <h2>设置步长</h2>
     <nut-cell class="cell">
-      <nut-range v-model="value3" step="5" @change="onChange3"></nut-range>
+      <nut-range v-model="value4" step="5" @change="onChange"></nut-range>
+    </nut-cell>
+    <h2>隐藏范围</h2>
+    <nut-cell class="cell">
+      <nut-range hidden-range v-model="value5" @change="onChange"></nut-range>
+    </nut-cell>
+    <h2>隐藏标签</h2>
+    <nut-cell class="cell">
+      <nut-range hidden-tag v-model="value6" @change="onChange"></nut-range>
+    </nut-cell>
+    <h2>自定义样式</h2>
+    <nut-cell class="cell">
+      <nut-range
+        v-model="value7"
+        @change="onChange"
+        inactive-color="rgba(163,184,255,1)"
+        button-color="rgba(52,96,250,1)"
+        active-color="linear-gradient(315deg, rgba(73,143,242,1) 0%,rgba(73,101,242,1) 100%)"
+      ></nut-range>
+    </nut-cell>
+    <h2>自定义按钮</h2>
+    <nut-cell class="cell">
+      <nut-range v-model="value8" @change="onChange">
+        <template #button>
+          <div class="custom-button">{{ value10 }}</div>
+        </template>
+      </nut-range>
     </nut-cell>
     <h2>禁用</h2>
     <nut-cell class="cell">
-      <nut-range disabled v-model="value4"></nut-range>
+      <nut-range disabled v-model="value9"></nut-range>
     </nut-cell>
   </div>
 </template>
 
 <script lang="ts">
-import { ref } from 'vue';
+import { toRefs, reactive } from 'vue';
 import { createComponent } from '@/utils/create';
 import { Toast } from '../toast';
 
@@ -33,21 +63,22 @@ 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 state = reactive({
+      value1: 40,
+      value2: [20, 80],
+      value3: 0,
+      value4: 20,
+      value5: 30,
+      value6: 40,
+      value7: 50,
+      value8: [20, 80],
+      value9: 60,
+      value10: 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
+      ...toRefs(state),
+      onChange
     };
   }
 });
@@ -55,8 +86,15 @@ export default createDemo({
 
 <style lang="scss" scoped>
 .cell {
-  padding: 30px 18px;
+  padding: 40px 18px;
 }
-.nut-range {
+.custom-button {
+  width: 26px;
+  color: #fff;
+  font-size: 10px;
+  line-height: 18px;
+  text-align: center;
+  background-color: #ee0a24;
+  border-radius: 100px;
 }
 </style>

+ 31 - 32
src/packages/range/doc.md

@@ -1,34 +1,33 @@
 #  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 |
-    
+  ### 介绍
+  
+  基于 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 |
+  

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

@@ -1,3 +1,16 @@
+.nut-range-container {
+  display: flex;
+  position: relative;
+  width: 100%;
+  height: 3px;
+  align-items: center;
+  .min,
+  .max {
+    font-size: 12px;
+    color: rgba(51, 51, 51, 1);
+    user-select: none;
+  }
+}
 .nut-range {
   display: block;
   position: relative;
@@ -59,6 +72,18 @@
       cursor: grab;
       outline: none;
     }
+
+    .number {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      user-select: none;
+      font-size: 12px;
+      color: rgba(51, 51, 51, 1);
+      transform: translate3d(0, -100%, 0);
+    }
   }
   &-disabled {
     cursor: not-allowed;
@@ -70,4 +95,7 @@
       cursor: not-allowed;
     }
   }
+  &-show-number {
+    margin: 0 15px;
+  }
 }

+ 113 - 26
src/packages/range/index.vue

@@ -1,24 +1,79 @@
 <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 class="nut-range-container">
+    <view class="min" v-if="!hiddenRange">{{ +min }}</view>
+    <view
+      ref="root"
+      :style="wrapperStyle"
+      :class="classes"
+      @click.stop="onClick"
+    >
+      <view class="nut-range-bar" :style="barStyle">
+        <template v-if="range">
+          <view
+            v-for="index of [0, 1]"
+            :key="index"
+            role="slider"
+            :class="{
+              'nut-range-button-wrapper-left': index == 0,
+              'nut-range-button-wrapper-right': index == 1
+            }"
+            :tabindex="disabled ? -1 : 0"
+            :aria-valuemin="+min"
+            :aria-valuenow="curValue(index)"
+            :aria-valuemax="+max"
+            aria-orientation="horizontal"
+            @touchstart.stop.prevent="
+              e => {
+                if (typeof index === 'number') {
+                  // 实时更新当前拖动的按钮索引
+                  buttonIndex = index;
+                }
+                onTouchStart(e);
+              }
+            "
+            @touchmove.stop.prevent="onTouchMove"
+            @touchend.stop.prevent="onTouchEnd"
+            @touchcancel.stop.prevent="onTouchEnd"
+            @click="e => e.stopPropagation()"
+          >
+            <slot v-if="$slots.button" name="button"></slot>
+            <view class="nut-range-button" v-else :style="buttonStyle">
+              <view class="number" v-if="!hiddenTag">{{
+                curValue(index)
+              }}</view>
+            </view>
+          </view>
+        </template>
+        <template v-else>
+          <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="
+              e => {
+                onTouchStart(e);
+              }
+            "
+            @touchmove.stop.prevent="onTouchMove"
+            @touchend.stop.prevent="onTouchEnd"
+            @touchcancel.stop.prevent="onTouchEnd"
+            @click="e => e.stopPropagation()"
+          >
+            <slot v-if="$slots.button" name="button"></slot>
+            <view class="nut-range-button" v-else :style="buttonStyle">
+              <view class="number" v-if="!hiddenTag">{{
+                curValue(index)
+              }}</view>
+            </view>
+          </view>
+        </template>
       </view>
     </view>
+    <view class="max" v-if="!hiddenRange">{{ +max }}</view>
   </view>
 </template>
 <script lang="ts">
@@ -37,9 +92,17 @@ export default create({
       default: false
     },
     disabled: Boolean,
-    barHeight: [Number, String],
     activeColor: String,
     inactiveColor: String,
+    buttonColor: String,
+    hiddenRange: {
+      type: Boolean,
+      default: false
+    },
+    hiddenTag: {
+      type: Boolean,
+      default: false
+    },
     min: {
       type: [Number, String],
       default: 0
@@ -60,8 +123,10 @@ export default create({
   components: {},
   emits: ['change', 'drag-end', 'drag-start', 'update:modelValue'],
 
-  setup(props, { emit }) {
-    let buttonIndex: number;
+  setup(props, { emit, slots }) {
+    console.log(slots.button && slots.button());
+
+    const buttonIndex = ref(0);
     let startValue: SliderValue;
     let currentValue: SliderValue;
 
@@ -69,43 +134,61 @@ export default create({
     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
+        [`${prefixCls}-disabled`]: props.disabled,
+        [`${prefixCls}-show-number`]: !props.hiddenRange
       };
     });
 
+    // 滑轨样式
     const wrapperStyle = computed(() => {
       return {
         background: props.inactiveColor
       };
     });
 
+    // 按钮样式
+    const buttonStyle = computed(() => {
+      return {
+        borderColor: props.buttonColor
+      };
+    });
+
+    // 判断是否是双滑块
     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(),
@@ -124,7 +207,8 @@ export default create({
     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();
@@ -162,6 +246,7 @@ export default create({
         const [left, right] = modelValue;
         const middle = (left + right) / 2;
 
+        // 靠左边点击移动左按钮,靠右边点击移动右按钮
         if (value <= middle) {
           updateValue([value, right], true);
         } else {
@@ -207,8 +292,8 @@ export default create({
       const diff = (delta / total) * scope.value;
 
       if (isRange(startValue)) {
-        (currentValue as number[])[buttonIndex] =
-          startValue[buttonIndex] + diff;
+        (currentValue as number[])[buttonIndex.value] =
+          startValue[buttonIndex.value] + diff;
       } else {
         currentValue = startValue + diff;
       }
@@ -238,13 +323,15 @@ export default create({
       root,
       classes,
       wrapperStyle,
+      buttonStyle,
       onClick,
       onTouchStart,
       onTouchMove,
       onTouchEnd,
       ...toRefs(props),
       barStyle,
-      curValue
+      curValue,
+      buttonIndex
     };
   }
 });