richard1015 5 years ago
parent
commit
45aa220b1c

+ 1 - 0
babel.config.js

@@ -2,6 +2,7 @@ const presets = [
 	[
 		'@babel/preset-env',
 		{
+			loose: false,
 			modules: 'commonjs',
 			targets: {
 				browsers: ['Android >= 4', 'iOS >= 8']

+ 10 - 0
src/config.json

@@ -603,6 +603,16 @@
       "sort": "3",
       "showDemo": false,
       "author": "szg2008"
+    },
+    {
+      "version": "1.0.0",
+      "name": "Drag",
+      "chnName": "拖拽",
+      "desc": "实现可拖拽的任意元素",
+      "type": "component",
+      "sort": "5",
+      "showDemo": true,
+      "author": "张宇"
     }
   ]
 }

+ 4 - 1
src/nutui.js

@@ -118,6 +118,8 @@ import SubSideNavBar from "./packages/subsidenavbar/index.js";
 import "./packages/subsidenavbar/subsidenavbar.scss";
 import SideNavBarItem from "./packages/sidenavbaritem/index.js";
 import "./packages/sidenavbaritem/sidenavbaritem.scss";
+import Drag from "./packages/drag/index.js";
+import "./packages/drag/drag.scss";
 
 const packages = {
   Cell,
@@ -177,7 +179,8 @@ const packages = {
   TimeLineItem: TimeLineItem,
   SideNavBar: SideNavBar,
   SubSideNavBar: SubSideNavBar,
-  SideNavBarItem: SideNavBarItem
+  SideNavBarItem: SideNavBarItem,
+  Drag: Drag
 };
 
 const components = {};

+ 74 - 0
src/packages/drag/demo.vue

@@ -0,0 +1,74 @@
+<template>
+  <div class="demo-list">
+    <nut-noticebar :closeMode="true" v-if="!isMobile"
+      >此 Demo 仅能在移动端浏览器体验,建议在 Android 或 iOS 设备上体验。
+    </nut-noticebar>
+    <h4>基本用法</h4>
+    <nut-drag name="demo1" :style="{ top: '100px', left: '8px' }">
+      <div class="touch-dom">基本用法</div>
+    </nut-drag>
+    <h4 :style="{ top: '150px' }">限制拖拽方向</h4>
+    <nut-drag name="demo2" direction="x" :style="{ top: '200px', left: '8px' }">
+      <div class="touch-dom">只能在X轴拖动</div>
+    </nut-drag>
+    <h4 :style="{ top: '250px' }">自动吸边</h4>
+    <nut-drag
+      name="demo3"
+      direction="x"
+      :attract="true"
+      :style="{ top: '300px', left: '8px' }"
+    >
+      <div class="touch-dom">拖动我</div>
+    </nut-drag>
+    <h4 :style="{ top: '350px' }">限制拖动边界</h4>
+    <div class="drag-boundary"></div>
+    <nut-drag
+      :boundary="{ top: 401, left: 9, bottom: bottom(), right: right() }"
+      name="demo4"
+      :attract="true"
+      :style="{ top: '400px', left: '8px' }"
+    >
+      <div class="touch-dom">拖动我</div>
+    </nut-drag>
+  </div>
+</template>
+
+<script>
+export default {
+  components: {},
+  data() {
+    return {};
+  },
+  methods: {
+    right() {
+      return document.documentElement.clientWidth - 300 - 9;
+    },
+    bottom() {
+      return document.documentElement.clientHeight - 601;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.touch-dom {
+  height: 30px;
+  padding: 10px;
+  line-height: 30px;
+  text-align: center;
+  color: #fff;
+  background: red;
+  overflow: hidden;
+}
+h4 {
+  position: absolute;
+}
+.drag-boundary {
+  position: absolute;
+  top: 400px;
+  left: 8px;
+  width: 300px;
+  height: 200px;
+  border: 1px solid red;
+}
+</style>

+ 0 - 0
src/packages/drag/doc.md


+ 9 - 0
src/packages/drag/drag.scss

@@ -0,0 +1,9 @@
+.nut-drag {
+  position: fixed;
+  display: inline-block;
+  top: 0;
+  right: 0;
+  z-index: 9997 !important;
+  width: fit-content;
+  height: fit-content;
+}

+ 206 - 0
src/packages/drag/drag.vue

@@ -0,0 +1,206 @@
+<template>
+  <div class="nut-drag" @touchstart="touchStart($event)">
+    <slot></slot>
+  </div>
+</template>
+<script>
+import requestAniFrame from "../../utils/raf.js";
+/**
+ * @module drag
+ * @description 拖拽组件,用于页面中需要拖拽的元素
+ * @vue-prop {Boolean} [attract=false] - 拖拽元素是否需要自动吸边
+ * @vue-prop {String} [direction='all'] - 拖拽元素的拖拽方向
+ * @vue-prop {Number | String} [zIndex=11] - 拖拽元素的堆叠顺序
+ * @vue-prop {Object} [boundary={top: 0,left: 0,right: 0,bottom: 0}] - 拖拽元素的拖拽边界
+ * @vue-data {Number} elWidth 拖拽元素的宽度
+ * @vue-data {Number} elHeight 拖拽元素的高度
+ * @vue-data {Number} screenWidth 屏幕的宽度
+ * @vue-data {Number} screenHeight 屏幕的高度
+ * @vue-data {Number} startTop 拖拽元素距离顶部的距离
+ * @vue-data {Number} startLeft 拖拽元素距离左侧的距离
+ * @vue-data {Object} position 鼠标点击的位置,包含距离x轴和y轴的距离
+ */
+export default {
+  name: "nut-drag",
+  props: {
+    attract: {
+      type: Boolean,
+      default: false,
+    },
+    direction: {
+      type: String,
+      default: "all",
+    },
+    zIndex: {
+      type: [Number, String],
+      default: 11,
+    },
+    boundary: {
+      type: Object,
+      default: function() {
+        return {
+          top: 0,
+          left: 0,
+          right: 0,
+          bottom: 0,
+        };
+      },
+    },
+  },
+  data() {
+    return {
+      elWidth: 0,
+      elHeight: 0,
+      screenWidth: 0,
+      screenHeight: 0,
+      startTop: 0,
+      startLeft: 0,
+      position: { x: 0, y: 0 },
+    };
+  },
+  methods: {
+    /**
+     * 获取拖拽元素的属性和屏幕的宽高,初始化拖拽元素的位置
+     */
+    getElementInfo() {
+      const el = this.$el;
+      const domElem = document.documentElement;
+      this.elWidth = el.offsetWidth;
+      this.elHeight = el.offsetHeight;
+      this.screenWidth = domElem.clientWidth;
+      this.screenHeight = domElem.clientHeight;
+      el.style.zIndex = this.zIndex;
+      if (this.boundary.left) {
+        el.style.left = this.boundary.left + "px";
+      } else {
+        el.style.right = this.boundary.right + "px";
+      }
+      if (this.boundary.top) {
+        el.style.top = this.boundary.top + "px";
+      } else {
+        el.style.bottom = this.boundary.bottom + "px";
+      }
+    },
+    touchStart(e) {
+      const target = e.currentTarget;
+      this.startTop = target.offsetTop; // 元素距离顶部的距离
+      this.startLeft = target.offsetLeft; // 元素距离左侧的距离
+      this.position.x = e.touches[0].clientX; // 鼠标点击的x轴的距离
+      this.position.y = e.touches[0].clientY; // 鼠标点击的y轴的距离
+      this.$el.addEventListener("touchmove", this.touchMove, false);
+      this.$el.addEventListener("touchend", this.touchEnd, false);
+    },
+    touchMove(e) {
+      e.preventDefault();
+      const target = e.currentTarget;
+      if (e.targetTouches.length == 1) {
+        const touch = e.targetTouches[0];
+        this.nx = touch.clientX - this.position.x;
+        this.ny = touch.clientY - this.position.y;
+        this.xPum = this.startLeft + this.nx;
+        this.yPum = this.startTop + this.ny;
+        const rightLocation =
+          this.screenWidth - this.elWidth - this.boundary.right;
+        // 限制左右拖拽边界
+        if (Math.abs(this.xPum) > rightLocation) {
+          this.xPum = rightLocation;
+        } else if (this.xPum <= this.boundary.left) {
+          this.xPum = this.boundary.left;
+        }
+        // 限制上下拖拽边界
+        if (this.yPum < this.boundary.top) {
+          this.yPum = this.boundary.top;
+        } else if (
+          this.yPum >
+          this.screenHeight - this.elHeight - this.boundary.bottom
+        ) {
+          this.yPum = this.screenHeight - this.elHeight - this.boundary.bottom;
+        }
+        if (this.direction != "y") {
+          target.style.left = this.xPum + "px";
+        }
+        if (this.direction != "x") {
+          target.style.top = this.yPum + "px";
+        }
+      }
+    },
+    touchEnd(e) {
+      const target = e.currentTarget;
+      const touch = e.changedTouches[0];
+      let currX = touch.clientX;
+      const rightLocation =
+        this.screenWidth - this.elWidth - this.boundary.right;
+      if (currX > rightLocation) {
+        currX = rightLocation;
+        // console.log('往右划出边界');
+      } else if (currX < this.boundary.left) {
+        currX = this.boundary.left;
+        // console.log('往左划出边界');
+      } else {
+        currX =
+          currX < this.screenWidth / 2 ? this.boundary.left : rightLocation;
+        // console.log('在边界内滑动');
+      }
+      if (this.direction != "y" && this.attract) {
+        if (currX < this.screenWidth / 2) {
+          this.goLeft(target);
+        } else {
+          this.goRight(target, rightLocation);
+        }
+      }
+      if (this.direction != "x") {
+        target.style.top = this.yPum + "px";
+      }
+    },
+    goLeft(target) {
+      if (this.boundary.left) {
+        if (target.style.left.split("px")[0] > this.boundary.left) {
+          target.style.left = target.style.left.split("px")[0] - 10 + "px";
+          requestAniFrame(() => {
+            this.goLeft(target);
+          });
+        } else {
+          target.style.left = `${this.boundary.left}px`;
+        }
+      } else {
+        if (target.style.left.split("px")[0] > 10) {
+          target.style.left = target.style.left.split("px")[0] - 10 + "px";
+          requestAniFrame(() => {
+            this.goLeft(target);
+          });
+        } else {
+          target.style.left = "0px";
+        }
+      }
+    },
+    goRight(target, rightLocation) {
+      if (rightLocation - parseInt(target.style.left.split("px")[0]) > 10) {
+        target.style.left =
+          parseInt(target.style.left.split("px")[0]) + 10 + "px";
+        requestAniFrame(() => {
+          this.goRight(target, rightLocation);
+        });
+      } else {
+        target.style.left = rightLocation + "px";
+      }
+    },
+  },
+  mounted() {
+    this.getElementInfo();
+  },
+  activated() {
+    if (this.keepAlive) {
+      this.keepAlive = false;
+    }
+  },
+  deactivated() {
+    this.keepAlive = true;
+    this.$el.removeEventListener("touchmove", this.handleScroll, false);
+    this.$el.removeEventListener("touchend", this.handleScroll, false);
+  },
+  destroyed() {
+    this.$el.removeEventListener("touchmove", this.handleScroll, false);
+    this.$el.removeEventListener("touchend", this.handleScroll, false);
+  },
+};
+</script>

+ 8 - 0
src/packages/drag/index.js

@@ -0,0 +1,8 @@
+import Drag from './drag.vue';
+import './drag.scss';
+
+Drag.install = function(Vue) {
+  Vue.component(Drag.name, Drag);
+};
+
+export default Drag

+ 7 - 0
src/packages/sidenavbar/__test__/sidenavbar.spec.js

@@ -0,0 +1,7 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import SideNavBar from '../sidenavbar.vue';
+
+describe('SideNavBar.vue', () => {
+    
+});

+ 106 - 8
src/packages/sidenavbar/demo.vue

@@ -1,12 +1,87 @@
 <template>
+  <div class="demo-list">
+    <h4>基本用法</h4>
     <div>
-        <nut-sidenavbar>
-          <nut-subsidenavbar>
-            <nut-sidenavbaritem key="1" title="云存储"></nut-sidenavbaritem>
-            <nut-sidenavbaritem key="2" title="弹性云计算"></nut-sidenavbaritem>
-          </nut-subsidenavbar>
-        </nut-sidenavbar>
+        <nut-cell :is-link="true" :show-icon="true" @click.native="showNav">
+            <span slot="title">
+                <label>右侧</label>
+            </span>
+        </nut-cell>
+        <nut-popup
+            position="right"
+            v-model="show1"
+            :style="{ width, height }"
+        >
+            <nut-sidenavbar :show="show1">
+                <nut-subsidenavbar title="图像理解" ikey="3" :open="false">
+                    <nut-sidenavbaritem ikey="4" title="菜品识别"></nut-sidenavbaritem>
+                    <nut-sidenavbaritem ikey="5" title="拍照购"></nut-sidenavbaritem>
+                </nut-subsidenavbar>
+                <nut-subsidenavbar title="自然语言处理" ikey="12">
+                    <nut-sidenavbaritem ikey="13" title="词法分析"></nut-sidenavbaritem>
+                    <nut-sidenavbaritem ikey="14" title="句法分析"></nut-sidenavbaritem>
+                </nut-subsidenavbar>
+            </nut-sidenavbar>
+        </nut-popup>
     </div>
+    <div>
+        <nut-cell :is-link="true" :show-icon="true" @click.native="showNav2">
+            <span slot="title">
+                <label>左侧</label>
+            </span>
+        </nut-cell>
+        <nut-popup
+            position="left"
+            v-model="show2"
+            :style="{ width, height }"
+        >
+            <nut-sidenavbar>
+                <nut-subsidenavbar title="图像理解" ikey="3" :open="false">
+                    <nut-sidenavbaritem ikey="4" title="菜品识别"></nut-sidenavbaritem>
+                    <nut-sidenavbaritem ikey="5" title="拍照购"></nut-sidenavbaritem>
+                </nut-subsidenavbar>
+                <nut-subsidenavbar title="自然语言处理" ikey="12">
+                    <nut-sidenavbaritem ikey="13" title="词法分析"></nut-sidenavbaritem>
+                    <nut-sidenavbaritem ikey="14" title="句法分析"></nut-sidenavbaritem>
+                </nut-subsidenavbar>
+            </nut-sidenavbar>
+        </nut-popup>
+    </div>
+    <h4>导航嵌套(建议最多三层),点击第一条回调</h4>
+    <div>
+        <nut-cell :is-link="true" :show-icon="true" @click.native="showNav3">
+            <span slot="title">
+                <label>显示</label>
+            </span>
+        </nut-cell>
+        <nut-popup
+            position="right"
+            v-model="show3"
+            :style="{ width, height }"
+        >
+            <nut-sidenavbar :show="show3">
+                <nut-sidenavbaritem ikey="1" title="人脸识别" @click="handleClick('人脸识别')"></nut-sidenavbaritem>
+                <nut-sidenavbaritem ikey="2" title="云存自然语言处理"></nut-sidenavbaritem>
+                <nut-subsidenavbar title="图像理解" ikey="3" :open="false">
+                    <nut-sidenavbaritem ikey="4" title="菜品识别"></nut-sidenavbaritem>
+                    <nut-sidenavbaritem ikey="5" title="拍照购"></nut-sidenavbaritem>
+                </nut-subsidenavbar>
+                <nut-subsidenavbar title="智能城市AI" ikey="6">
+                    <nut-sidenavbaritem ikey="7" title="企业风险预警模型"></nut-sidenavbaritem>
+                    <nut-sidenavbaritem ikey="8" title="水质量检测"></nut-sidenavbaritem>
+                    <nut-subsidenavbar title="人体识别" ikey="9">
+                        <nut-sidenavbaritem ikey="10" title="人体检测"></nut-sidenavbaritem>
+                        <nut-sidenavbaritem ikey="11" title="细粒度人像分割"></nut-sidenavbaritem>
+                    </nut-subsidenavbar>
+                </nut-subsidenavbar>
+                <nut-subsidenavbar title="自然语言处理" ikey="12">
+                    <nut-sidenavbaritem ikey="13" title="词法分析"></nut-sidenavbaritem>
+                    <nut-sidenavbaritem ikey="14" title="句法分析"></nut-sidenavbaritem>
+                </nut-subsidenavbar>
+            </nut-sidenavbar>
+        </nut-popup>
+    </div>
+  </div>
 </template>
 
 <script>
@@ -15,10 +90,33 @@ export default {
 
   },
   data() {
-    return {};
+    return {
+        show1: false,
+        show2: false,
+        show3: false,
+        width: '80%',
+        height: '100%'
+    };
   },
   methods: {
-      
+    showNav() {
+        this.show1 = true
+        this.show2 = false
+        this.show3 = false
+    },
+    showNav2() {
+        this.show1 = false
+        this.show2 = true
+        this.show3 = false
+    },
+    showNav3() {
+        this.show1 = false
+        this.show2 = false
+        this.show3 = true
+    },
+    handleClick(str) {
+        alert(str)
+    }
   }
 };
 </script>

+ 109 - 0
src/packages/sidenavbar/doc.md

@@ -0,0 +1,109 @@
+# SideNavBar 侧边栏导航
+
+侧边栏导航
+
+## 基础用法
+
+```html
+<nut-popup
+    position="right"
+    v-model="show1"
+    :style="{ width, height }"
+>
+    <nut-sidenavbar :show="show1">
+        <nut-subsidenavbar title="图像理解" ikey="3" :open="false">
+            <nut-sidenavbaritem ikey="4" title="菜品识别"></nut-sidenavbaritem>
+            <nut-sidenavbaritem ikey="5" title="拍照购"></nut-sidenavbaritem>
+        </nut-subsidenavbar>
+        <nut-subsidenavbar title="自然语言处理" ikey="12">
+            <nut-sidenavbaritem ikey="13" title="词法分析"></nut-sidenavbaritem>
+            <nut-sidenavbaritem ikey="14" title="句法分析"></nut-sidenavbaritem>
+        </nut-subsidenavbar>
+    </nut-sidenavbar>
+</nut-popup>
+```
+
+## 嵌套(建议最多三层)
+```html
+<nut-popup
+    position="right"
+    v-model="show3"
+    :style="{ width, height }"
+>
+    <nut-sidenavbar :show="show3">
+        <nut-sidenavbaritem ikey="1" title="人脸识别" @click="handleClick('人脸识别')"></nut-sidenavbaritem>
+        <nut-sidenavbaritem ikey="2" title="云存自然语言处理"></nut-sidenavbaritem>
+        <nut-subsidenavbar title="图像理解" ikey="3" :open="false">
+            <nut-sidenavbaritem ikey="4" title="菜品识别"></nut-sidenavbaritem>
+            <nut-sidenavbaritem ikey="5" title="拍照购"></nut-sidenavbaritem>
+        </nut-subsidenavbar>
+        <nut-subsidenavbar title="智能城市AI" ikey="6">
+            <nut-sidenavbaritem ikey="7" title="企业风险预警模型"></nut-sidenavbaritem>
+            <nut-sidenavbaritem ikey="8" title="水质量检测"></nut-sidenavbaritem>
+            <nut-subsidenavbar title="人体识别" ikey="9">
+                <nut-sidenavbaritem ikey="10" title="人体检测"></nut-sidenavbaritem>
+                <nut-sidenavbaritem ikey="11" title="细粒度人像分割"></nut-sidenavbaritem>
+            </nut-subsidenavbar>
+        </nut-subsidenavbar>
+        <nut-subsidenavbar title="自然语言处理" ikey="12">
+            <nut-sidenavbaritem ikey="13" title="词法分析"></nut-sidenavbaritem>
+            <nut-sidenavbaritem ikey="14" title="句法分析"></nut-sidenavbaritem>
+        </nut-subsidenavbar>
+    </nut-sidenavbar>
+</nut-popup>
+```
+
+```javascript
+export default {
+  data() {
+    return {
+        show1: false,
+        show3: false,
+        width: '80%',
+        height: '100%'
+    };
+  },
+  methods: {
+    showNav() {
+        this.show1 = true
+        this.show3 = false
+    },
+    showNav3() {
+        this.show1 = false
+        this.show3 = true
+    },
+    handleClick(str) {
+        alert(str)
+    }
+  }
+};
+```
+
+## SideNavBar
+
+| 字段 | 说明 | 类型 | 默认值
+| ----- | ----- | ----- | -----
+| offset | 导航缩进宽度 | Number/String | 15
+
+## SubSideNavBar
+| 字段 | 说明 | 类型 | 默认值
+| ----- | ----- | ----- | -----
+| title | 导航标题 | String | ''
+| ikey | 导航唯一标识 | String/Number | ''
+| open | 导航是否默认展开 | Boolean | true
+
+## SubSideNavBar Event
+| 字段 | 说明 | 回调参数
+| ----- | ----- | -----
+| titleClick | 导航点击 | 无
+
+## SideNavBarItem
+| 字段 | 说明 | 类型 | 默认值
+| ----- | ----- | ----- | -----
+| title | 导航标题 | String | ''
+| ikey | 导航唯一标识 | String/Number | ''
+
+## SideNavBarItem Event
+| 字段 | 说明 | 回调参数
+| ----- | ----- | -----
+| click | 导航点击 | 无

+ 27 - 26
src/packages/sidenavbar/sidenavbar.vue

@@ -1,46 +1,47 @@
 <template>
     <div class="nut-sidenavbar">
-        <nut-popup
-            :position="position"
-            v-model="show"
-            :style="{ width, height }"
-        >
-            <div class="sidenavbar-content">
-                <div class="sidenavbar-list">
-                    <slot></slot>
-                </div>
+        <div class="sidenavbar-content">
+            <div class="sidenavbar-list">
+                <slot></slot>
             </div>
-        </nut-popup>
+        </div>
     </div>
 </template>
 <script>
 export default {
     name:'nut-sidenavbar',
     props: {
-        show:{
-            type: Boolean,
-            default: true
-        },
-        position:{
-            type: String,
-            default: 'right'
-        },
-        width:{
+        offset:{
             type:[String,Number],
-            default: '80%'
-        },
-        height:{
-            type:[String,Number],
-            default: '100%'
+            default:15
+        }
+    },
+    mounted() {
+        let slots = this.$slots.default;
+        if(slots) {
+            slots = slots.filter(item => item.elm.nodeType !== 3).map(item => {
+                return item.elm
+            })
+            
+            this.setPaddingLeft(slots)
         }
     },
     data() {
         return {
-            
+            count: 1
         };
     },
     methods: {
-
+        setPaddingLeft(nodeList, level = 1) {
+            for(let i = 0; i < nodeList.length; i++) {
+                let item = nodeList[i];
+                item.children[0].style.paddingLeft = this.offset * level + 'px'
+                if(item.className !== 'nut-sidenavbaritem') {
+                    this.setPaddingLeft(Array.from(item.children[1].children), ++this.count)
+                }
+            }
+            this.count = 1;
+        }
     }
 }
 </script>

+ 5 - 6
src/packages/sidenavbaritem/sidenavbaritem.scss

@@ -1,9 +1,8 @@
 .nut-sidenavbaritem{
-    li{
-        .sub-sidenavbar-item-item{
-            line-height: 40px;
-            padding: 0 50px;
-            color: #9b9ea0;
-        }
+    height: 40px;
+    line-height: 40px;
+    .item-title{
+        color: $title-color;
+        background-color: #fff;
     }
 }

+ 11 - 16
src/packages/sidenavbaritem/sidenavbaritem.vue

@@ -1,32 +1,27 @@
 <template>
-    <div class="nut-sidenavbaritem">     
-        <li>
-            <a href="javascript:;" class="item-title sub-sidenavbar-item-item">
-                {{ title }}
-            </a>
-        </li>
+    <div class="nut-sidenavbaritem" @click.stop="handleClick" :ikey="ikey"> 
+        <a href="javascript:;" class="item-title">
+            {{ title }}
+        </a>
     </div>
 </template>
 <script>
 export default {
     name:'nut-sidenavbaritem',
     props: {
-        disabled: {
-            type: Boolean,
-            default: true
-        },
         title: {
             type: String,
             default: ''
+        },
+        ikey: {
+            type: String,
+            default: ''
         }
     },
-    data() {
-        return {
-            
-        };
-    },
     methods: {
-
+        handleClick() {
+            this.$emit('click');
+        }
     }
 }
 </script>

+ 19 - 31
src/packages/subsidenavbar/subsidenavbar.scss

@@ -1,4 +1,8 @@
 .nut-subsidenavbar{
+    display: grid;
+    float: left;
+    width: 100%;
+    position: relative;
     .item-title{
         display: block;
         width: 100%;
@@ -8,44 +12,28 @@
         text-overflow: ellipsis;
         white-space: nowrap;
         box-sizing: border-box;
-        border-bottom: 1px solid #f8f8f8;
+        border-bottom: 1px solid $light-color;
+        color: $title-color;
+        font-size: $font-size-large;
+        background-color: $light-color;
+        .sidenavbar-title{
+            line-height: 40px;
+            color: $title-color;
+        }
     }
     .sidenavbar-icon{
         position: absolute;
         top: 50%;
         right: 20px;
         transform: translateY(-50%);
-    }
-    .sidenavbar-title{
-        font-size: 16px;
-        line-height: 40px;
-        height: 40px;
-    }
-    .sidenavbar-item{
-        float: left;
-        width: 100%;
-        position: relative;
-        border-bottom: 1px solid #f8f8f8;
-        background-color: #f8f8f8;
-        .sidenavbar-item-title{
-            padding: 0 18px;
-            .sidenavbar-title{
-                color: #333;
-            }
-        }
-        .sub-sidenavbar-box{
-            background-color: #fefefe;
-            .sub-sidenavbar-list{
-                .sub-sidenavbar-item{
-                    width: 100%;
-                    .sub-sidenavbar-item-title{
-                        padding: 0 34px;
-                        .sub-sidenavbar-title{
-                            color: #333;
-                        }
-                    }
-                }
+        i{
+            transition: transform $animation-duration $ease-in-out;
+            &.up{
+                transform: rotate(-180deg);
             }
         }
     }
+    .sub-sidenavbar-list{
+        width: 100%;
+    }
 }

+ 30 - 30
src/packages/subsidenavbar/subsidenavbar.vue

@@ -1,31 +1,14 @@
 <template>
-    <div class="nut-subsidenavbar">
-        <div class="sidenavbar-item">
-            <div class="item-title sidenavbar-item-title">
-                <a href="javascript:;" class="sidenavbar-title">
-                    云产品
-                </a>
-                <span class="sidenavbar-icon">
-                    <nut-icon type="down"></nut-icon>
-                </span>
-            </div>
-            <div class="sub-sidenavbar-box">
-                <div class="sub-sidenavbar-list">
-                    <div class="sub-sidenavbar-item">
-                        <div class="item-title sub-sidenavbar-item-title">
-                            <a href="javascript:;" class="sidenavbar-title sub-sidenavbar-title">
-                                弹性运算
-                            </a>
-                            <span class="sidenavbar-icon">
-                                <nut-icon type="down"></nut-icon>
-                            </span>
-                        </div>
-                        <ul class="three-sidenavbar-list">
-                            <slot></slot>
-                        </ul>
-                    </div>
-                </div>
-            </div>
+    <div class="nut-subsidenavbar sidenavbar-item" :ikey="ikey">
+        <div class="item-title" @click.stop="handleClick">
+            <a href="javascript:;" class="sidenavbar-title">{{ title }}</a>
+            <span class="sidenavbar-icon"><nut-icon type="down" :class="direction"></nut-icon></span>
+        </div>
+        <div class="sub-sidenavbar-list" 
+            :class="!direction ? 'nutFadeIn' : 'nutFadeOut'"
+            :style="{height: !direction ? 'auto' : 0}" 
+            >
+            <slot></slot>
         </div>
     </div>
 </template>
@@ -33,15 +16,32 @@
 export default {
     name:'nut-subsidenavbar',
     props: {
-        
+        title: {
+            type: String,
+            default: ''
+        },
+        ikey: {
+            type: [String, Number],
+            default: ''
+        },
+        open: {
+            type: Boolean,
+            default: true
+        }
     },
     data() {
         return {
-            
+            direction: ''
         };
     },
+    mounted() {
+        this.direction = this.open ? '' : 'up';
+    },
     methods: {
-
+        handleClick() {
+            this.$emit('titleClick');
+            this.direction = !this.direction ? 'up' : ''
+        }
     }
 }
 </script>

+ 45 - 44
src/packages/tabselect/doc.md

@@ -39,93 +39,93 @@ export default {
   components: {},
   data() {
     return {
-      mainTitle: '配送',
-      subTitle: '送达时间',
+      mainTitle: "配送",
+      subTitle: "送达时间",
       defaultContent: [
-        '9:00——10:00',
-        '10:00——11:00',
-        '11:00——12:00',
-        '12:00——13:00',
-        '13:00——15:00',
-        '15:00——17:00',
-        '17:00——19:00'
+        "9:00——10:00",
+        "10:00——11:00",
+        "11:00——12:00",
+        "12:00——13:00",
+        "13:00——15:00",
+        "15:00——17:00",
+        "17:00——19:00",
       ],
       tabList: [
         {
-          tabTitle: '京东快递', // 一级tab标题
+          tabTitle: "京东快递", // 一级tab标题
           children: [
             // 一级tab内容
             {
-              tabTitle: '1月13日 (星期一)', // 二级tab标题
+              tabTitle: "1月13日 (星期一)", // 二级tab标题
               content: [
                 // 二级tab内容,不传默认使用defaultContent字段
-                '11:00——12:00',
-                '12:00——13:00',
-                '13:00——15:00',
-                '15:00——17:00',
-                '17:00——19:00'
-              ]
+                "11:00——12:00",
+                "12:00——13:00",
+                "13:00——15:00",
+                "15:00——17:00",
+                "17:00——19:00",
+              ],
             },
             {
-              tabTitle: '1月14日 (星期二)'
+              tabTitle: "1月14日 (星期二)",
             },
             {
-              tabTitle: '1月15日 (星期三)'
+              tabTitle: "1月15日 (星期三)",
             },
             {
-              tabTitle: '1月16日 (星期四)'
+              tabTitle: "1月16日 (星期四)",
             },
             {
-              tabTitle: '1月17日 (星期五)'
+              tabTitle: "1月17日 (星期五)",
             },
             {
-              tabTitle: '1月18日 (星期六)'
+              tabTitle: "1月18日 (星期六)",
             },
             {
-              tabTitle: '1月19日 (星期天)'
-            }
-          ]
+              tabTitle: "1月19日 (星期天)",
+            },
+          ],
         },
         {
-          tabTitle: '上门自提',
+          tabTitle: "上门自提",
           children: [
             {
-              tabTitle: '2月13日 (星期一)',
-              content: ['13:00——15:00', '15:00——17:00', '17:00——19:00']
+              tabTitle: "2月13日 (星期一)",
+              content: ["13:00——15:00", "15:00——17:00", "17:00——19:00"],
             },
             {
-              tabTitle: '2月14日 (星期二)'
+              tabTitle: "2月14日 (星期二)",
             },
             {
-              tabTitle: '2月15日 (星期三)'
+              tabTitle: "2月15日 (星期三)",
             },
             {
-              tabTitle: '2月16日 (星期四)'
+              tabTitle: "2月16日 (星期四)",
             },
             {
-              tabTitle: '2月17日 (星期五)'
+              tabTitle: "2月17日 (星期五)",
             },
             {
-              tabTitle: '2月18日 (星期六)'
+              tabTitle: "2月18日 (星期六)",
             },
             {
-              tabTitle: '2月19日 (星期天)'
-            }
-          ]
-        }
+              tabTitle: "2月19日 (星期天)",
+            },
+          ],
+        },
       ],
-      show: false
-    }
+      show: false,
+    };
   },
   methods: {
     choose(title, item) {
-      console.log(title, item)
+      console.log(title, item);
     },
     onOkBtn(event) {
-      console.log(event)
-    }
-  }
-}
+      console.log(event);
+    },
+  },
+};
 ```
 
 ### Prop
@@ -140,6 +140,7 @@ export default {
 | show              | 是否显示                                   | Boolean | false    |
 | max               | 多选时最多可选个数                         | Number  | Infinity |
 | isDefaultSelected | 单选时是否默认选中第一项(多选默认不选中) | Boolean | false    |
+| isLockBgScroll    | 是否锁定背景滚动                           | Boolean | true     |
 
 ### Event
 

+ 1 - 1
src/packages/tabselect/tabselect.scss

@@ -43,7 +43,7 @@
       min-width: 158px;
     }
     .nut-title-nav {
-      height: 40px;
+      min-height: 40px;
       line-height: 40px;
       background: #f4f4f4;
       padding-left: 18px;

+ 240 - 196
src/packages/tabselect/tabselect.vue

@@ -1,202 +1,246 @@
 <template>
-	<div class="nut-tabselect" v-if="list.length">
-		<nut-popup round closeable v-model="isShow" position="bottom" :style="{ height: '457px' }">
-			<div class="nut-tabselect-main-title" v-html="mainTitle"></div>
-			<nut-tab @tab-switch="tabSwitchOuter" :init-data="list">
-				<nut-tab-panel v-for="(value, idx) in list" v-bind:key="value.tabTitle" :tabTitle="value.tabTitle">
-					<div class="nut-tabselect-sub-title" v-html="subTitle"></div>
-					<nut-tab @tab-switch="tabSwitchInner" positionNav="left" class="nut-tab-inner" :init-data="value.children" :defIndex="defIndex">
-						<nut-tab-panel v-for="(item, index) in value.children" v-bind:key="item.tabTitle" :tabTitle="item.tabTitle">
-							<ul>
-								<template v-if="item.content">
-									<li
-										v-for="(sitem, sIndex) in item.content"
-										v-bind:key="sitem"
-										@click="choose(idx, index, sIndex, item, sitem)"
-										class="nut-tab-panel-list"
-										:class="{
-											'nut-tab-panel-list-active': isActive(idx, index, sIndex)
-										}"
-									>
-										{{ sitem }}
-									</li>
-								</template>
-								<template v-else-if="defaultContent">
-									<li
-										v-for="(sitem, sIndex) in defaultContent"
-										v-bind:key="sitem"
-										@click="choose(idx, index, sIndex, item, sitem)"
-										class="nut-tab-panel-list"
-										:class="{
-											'nut-tab-panel-list-active': isActive(idx, index, sIndex)
-										}"
-									>
-										{{ sitem }}
-									</li>
-								</template>
-							</ul>
-						</nut-tab-panel>
-					</nut-tab>
-				</nut-tab-panel>
-			</nut-tab>
-			<div class="nut-tabselect-btn">
-				<a href="javascript:;" @click="clickHandler">确定</a>
-			</div>
-		</nut-popup>
-	</div>
+  <div class="nut-tabselect" v-if="list.length">
+    <nut-popup
+      round
+      closeable
+      v-model="isShow"
+      position="bottom"
+      :style="{ height: '457px' }"
+      :lock-scroll="isLockBgScroll"
+    >
+      <div class="nut-tabselect-main-title" v-html="mainTitle"></div>
+      <nut-tab @tab-switch="tabSwitchOuter" :init-data="list">
+        <nut-tab-panel
+          v-for="(value, idx) in list"
+          v-bind:key="value.tabTitle"
+          :tabTitle="value.tabTitle"
+        >
+          <div class="nut-tabselect-sub-title" v-html="subTitle"></div>
+          <nut-tab
+            @tab-switch="tabSwitchInner"
+            positionNav="left"
+            class="nut-tab-inner"
+            :init-data="value.children"
+            :defIndex="defIndex"
+          >
+            <nut-tab-panel
+              v-for="(item, index) in value.children"
+              v-bind:key="item.tabTitle"
+              :tabTitle="item.tabTitle"
+            >
+              <ul>
+                <template v-if="item.content">
+                  <li
+                    v-for="(sitem, sIndex) in item.content"
+                    v-bind:key="sitem"
+                    @click="choose(idx, index, sIndex, item, sitem)"
+                    class="nut-tab-panel-list"
+                    :class="{
+                      'nut-tab-panel-list-active': isActive(idx, index, sIndex),
+                    }"
+                  >
+                    {{ sitem }}
+                  </li>
+                </template>
+                <template v-else-if="defaultContent">
+                  <li
+                    v-for="(sitem, sIndex) in defaultContent"
+                    v-bind:key="sitem"
+                    @click="choose(idx, index, sIndex, item, sitem)"
+                    class="nut-tab-panel-list"
+                    :class="{
+                      'nut-tab-panel-list-active': isActive(idx, index, sIndex),
+                    }"
+                  >
+                    {{ sitem }}
+                  </li>
+                </template>
+              </ul>
+            </nut-tab-panel>
+          </nut-tab>
+        </nut-tab-panel>
+      </nut-tab>
+      <div class="nut-tabselect-btn">
+        <a href="javascript:;" @click="clickHandler">确定</a>
+      </div>
+    </nut-popup>
+  </div>
 </template>
 <script>
-import nuttab from '../tab/tab.vue';
-import '../tab/tab.scss';
-import nutpop from '../popup/popup.vue';
-import '../popup/popup.scss';
+import nuttab from "../tab/tab.vue";
+import "../tab/tab.scss";
+import nutpop from "../popup/popup.vue";
+import "../popup/popup.scss";
 export default {
-	name: 'nut-tabselect',
-	props: {
-		mainTitle: {
-			type: String,
-			default: ''
-		},
-		subTitle: {
-			type: String,
-			default: ''
-		},
-		defaultContent: {
-			type: Array,
-			default: () => []
-		},
-		tabList: {
-			type: Array,
-			default: () => []
-		},
-		show: {
-			type: Boolean,
-			default: false
-		},
-		multiple: {
-			type: Boolean,
-			default: false
-		},
-		max: {
-			type: Number,
-			default: Infinity
-		},
-		isDefaultSelected: {
-			type: Boolean,
-			default: false
-		}
-	},
-	data() {
-		return {
-			isShow: false,
-			level0: 0,
-			level1: new Set([0]),
-			level2: this.isDefaultSelected ? new Set(['0-0']) : new Set(),
-			allChoose: this.getText(0, 0, this.isDefaultSelected ? 0 : null),
-			list: [],
-			defIndex: 0
-		};
-	},
-	components: {
-		[nuttab.name]: nuttab,
-		[nutpop.name]: nutpop
-	},
-	watch: {
-		show(val) {
-			this.isShow = val;
-		},
-		isShow(val) {
-			if (!val) {
-				this.$emit('close');
-			}
-		},
-		tabList: {
-			handler(val) {
-				this.list = val;
-				this.level0 = 0;
-				this.level1 = new Set([0]);
-				this.level2 = this.isDefaultSelected ? new Set(['0-0']) : new Set();
-				this.allChoose = this.getText(0, 0, this.isDefaultSelected ? 0 : null);
-				this.emit();
-			},
-			deep: true
-		}
-	},
-	mounted() {
-		this.list = this.tabList;
-		this.allChoose = this.getText(0, 0, this.isDefaultSelected ? 0 : null);
-		this.emit();
-	},
-	methods: {
-		emit() {
-			this.$emit(
-				'choose',
-				(this.list && this.list[this.level0] && this.list[this.level0].tabTitle) || '',
-				(this.allChoose && [...this.allChoose]) || []
-			);
-		},
-		getText(idx, index, sIndex) {
-			if (sIndex === null) {
-				return null;
-			}
-			const tab = (this.list && this.list[idx] && this.list[idx].children[index]) || {};
-			const subTit = tab.tabTitle;
-			const content = (tab.content && tab.content[sIndex]) || this.defaultContent[sIndex];
-			return new Set([{ subTit, content }]);
-		},
-		tabSwitchOuter: function(index, event) {
-			this.defIndex = 0;
-			this.level0 = index;
-			this.level1 = new Set([0]);
-			this.level2 = this.isDefaultSelected ? new Set(['0-0']) : new Set();
-			this.allChoose = this.getText(index, 0, this.isDefaultSelected ? 0 : null);
-			this.emit();
-		},
-		tabSwitchInner: function(index, event) {
-			this.defIndex = index;
-			if (!this.multiple) {
-				this.level1 = new Set([index]);
-			} else {
-				this.level1.add(index);
-			}
-		},
-		unChoose(index, sIndex) {
-			this.level2.delete(index + '-' + sIndex);
-			this.level2 = new Set(this.level2);
-		},
-		choose(idx, index, sIndex) {
-			if (this.multiple && this.isActive(idx, index, sIndex)) {
-				this.unChoose(index, sIndex);
-				this.allChoose.delete(this.getText(idx, index, sIndex));
-				this.emit();
-				return;
-			}
-			if (!this.multiple) {
-				this.level2 = new Set([index + '-' + sIndex]);
-				this.allChoose = this.getText(idx, index, sIndex);
-			} else {
-				if (this.max !== Infinity && this.max === this.level2.size) {
-					return;
-				}
-				this.level2 = new Set([...this.level2.add(index + '-' + sIndex)]);
-				if (this.allChoose) {
-					this.allChoose.add(...this.getText(idx, index, sIndex));
-				} else {
-					this.allChoose = this.getText(idx, index, sIndex);
-				}
-			}
-			this.emit();
-		},
-		clickHandler(event) {
-			this.$emit('onOkBtn', event);
-			this.isShow = false;
-		},
-		isActive(idx, index, sIndex) {
-			if (idx === this.level0 && this.level1.has(index) && this.level2.has(index + '-' + sIndex)) {
-				return true;
-			}
-			return false;
-		}
-	}
+  name: "nut-tabselect",
+  props: {
+    mainTitle: {
+      type: String,
+      default: "",
+    },
+    subTitle: {
+      type: String,
+      default: "",
+    },
+    isLockBgScroll: {
+      type: Boolean,
+      default: true,
+    },
+    defaultContent: {
+      type: Array,
+      default: () => [],
+    },
+    tabList: {
+      type: Array,
+      default: () => [],
+    },
+    show: {
+      type: Boolean,
+      default: false,
+    },
+    multiple: {
+      type: Boolean,
+      default: false,
+    },
+    max: {
+      type: Number,
+      default: Infinity,
+    },
+    isDefaultSelected: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      isShow: false,
+      level0: 0,
+      level1: new Set([0]),
+      level2: this.isDefaultSelected ? new Set(["0-0"]) : new Set(),
+      allChoose: this.getText(0, 0, this.isDefaultSelected ? 0 : null),
+      list: [],
+      defIndex: 0,
+    };
+  },
+  components: {
+    [nuttab.name]: nuttab,
+    [nutpop.name]: nutpop,
+  },
+  watch: {
+    show(val) {
+      this.isShow = val;
+    },
+    isShow(val) {
+      if (!val) {
+        this.$emit("close");
+      }
+    },
+    tabList: {
+      handler(val) {
+        this.list = val;
+        this.level0 = 0;
+        this.level1 = new Set([0]);
+        this.level2 = this.isDefaultSelected ? new Set(["0-0"]) : new Set();
+        this.allChoose = this.getText(0, 0, this.isDefaultSelected ? 0 : null);
+        this.emit();
+      },
+      deep: true,
+    },
+  },
+  mounted() {
+    this.list = this.tabList;
+    this.allChoose = this.getText(0, 0, this.isDefaultSelected ? 0 : null);
+    this.emit();
+  },
+  methods: {
+    emit() {
+      this.$emit(
+        "choose",
+        (this.list &&
+          this.list[this.level0] &&
+          this.list[this.level0].tabTitle) ||
+          "",
+        (this.allChoose && [...this.allChoose]) || []
+      );
+    },
+    getText(idx, index, sIndex) {
+      if (sIndex === null) {
+        return null;
+      }
+      const tab =
+        (this.list && this.list[idx] && this.list[idx].children[index]) || {};
+      const subTit = tab.tabTitle;
+      const content =
+        (tab.content && tab.content[sIndex]) || this.defaultContent[sIndex];
+      return new Set([{ subTit, content }]);
+    },
+    tabSwitchOuter: function(index, event) {
+      this.defIndex = 0;
+      this.level0 = index;
+      this.level1 = new Set([0]);
+      this.level2 = this.isDefaultSelected ? new Set(["0-0"]) : new Set();
+      this.allChoose = this.getText(
+        index,
+        0,
+        this.isDefaultSelected ? 0 : null
+      );
+      this.emit();
+    },
+    tabSwitchInner: function(index, event) {
+      this.defIndex = index;
+      if (!this.multiple) {
+        this.level1 = new Set([index]);
+      } else {
+        this.level1.add(index);
+      }
+    },
+    unChoose(index, sIndex) {
+      this.level2.delete(index + "-" + sIndex);
+      this.level2 = new Set(this.level2);
+    },
+    choose(idx, index, sIndex) {
+      if (this.multiple && this.isActive(idx, index, sIndex)) {
+        this.unChoose(index, sIndex);
+        this.getText(idx, index, sIndex).forEach((o) => {
+          for (let indexdel of this.allChoose.values()) {
+            if (JSON.stringify(o) === JSON.stringify(indexdel)) {
+              this.allChoose.delete(indexdel);
+            }
+          }
+        });
+        this.emit();
+        return;
+      }
+      if (!this.multiple) {
+        this.level2 = new Set([index + "-" + sIndex]);
+        this.allChoose = this.getText(idx, index, sIndex);
+      } else {
+        if (this.max !== Infinity && this.max === this.level2.size) {
+          return;
+        }
+        this.level2 = new Set([...this.level2.add(index + "-" + sIndex)]);
+        if (this.allChoose) {
+          this.allChoose.add(...this.getText(idx, index, sIndex));
+        } else {
+          this.allChoose = this.getText(idx, index, sIndex);
+        }
+      }
+      this.emit();
+    },
+    clickHandler(event) {
+      this.$emit("onOkBtn", event);
+      this.isShow = false;
+    },
+    isActive(idx, index, sIndex) {
+      if (
+        idx === this.level0 &&
+        this.level1.has(index) &&
+        this.level2.has(index + "-" + sIndex)
+      ) {
+        return true;
+      }
+      return false;
+    },
+  },
 };
 </script>