ソースを参照

feat: +Menu组件

liuguanglun 5 年 前
コミット
146806b3a5

+ 1 - 0
.gitignore

@@ -22,3 +22,4 @@ lib/plugin/cli/site/doc/view/
 lib/plugin/cli/site/doc/page/
 yarn.lock
 test_script/
+.idea

+ 1 - 0
package.json

@@ -68,6 +68,7 @@
     "@commitlint/config-conventional": "^8.0.0",
     "babel-plugin-istanbul": "^6.0.0",
     "gsap": "^3.2.6",
+    "lodash.clonedeep": "^4.5.0",
     "vue-lazyload": "^1.3.3",
     "vue-qr": "^2.2.1"
   },

+ 11 - 1
src/config.json

@@ -417,6 +417,16 @@
             "sort": "0",
             "showDemo": true,
             "author": "xuhui"
+        },
+        {
+            "version": "1.0.0",
+            "name": "Menu",
+            "sort": "3",
+            "chnName": "菜单",
+            "desc": "菜单",
+            "type": "component",
+            "showDemo": true,
+            "author": "liuguanglun"
         }
     ]
-}
+}

+ 4 - 1
src/nutui.js

@@ -77,6 +77,8 @@ import Tag from './packages/tag/index.js';
 import './packages/tag/tag.scss';
 import Swiper from './packages/swiper/index.js';
 import './packages/swiper/swiper.scss';
+import Menu from './packages/menu/index.js';
+import './packages/menu/menu.scss';
 import ImagePreview from './packages/imagepreview/index.js';
 import './packages/imagepreview/imagepreview.scss';
 import Badge from './packages/badge/index.js';
@@ -126,6 +128,7 @@ const packages = {
     Address: Address,
     Tag,
     Swiper,
+    Menu,
     ImagePreview,
     Badge,
     Field: Field,
@@ -206,4 +209,4 @@ export default {
     ...filters,
     ...directives,
     ...methods
-};
+};

+ 143 - 0
src/packages/menu/demo.vue

@@ -0,0 +1,143 @@
+<template>
+  <div>
+    <h4>基础样式</h4>
+    <nut-menu mask :list="list" type="simple" @open="onOpen" @close="onClose" />
+    <h4>多选样式</h4>
+    <nut-menu :class="{ hidden: isVisible1 }" mask :list="list1" type="multiple" :max="2" @maxTip="onMaxTip" @reset="onReset" @ok="onOK"></nut-menu>
+  </div>
+</template>
+
+<script>
+import cloneDeep from 'lodash.clonedeep';
+const list = [
+  {
+    id: 0,
+    text: '综合',
+    selected: true,
+    children: [
+      {
+        id: 1,
+        text: '全部订单',
+        selected: false,
+      },
+      {
+        id: 2,
+        text: '派送订单',
+        selected: false,
+      },
+      {
+        id: 3,
+        text: '揽收订单',
+        selected: false,
+      },
+      {
+        id: 4,
+        text: '其他订单',
+        selected: false,
+      },
+    ],
+  },
+  {
+    id: 1,
+    text: '综合',
+    selected: true,
+    children: [
+      {
+        id: 1,
+        text: '全部订单',
+        selected: false,
+      },
+      {
+        id: 2,
+        text: '派送订单',
+        selected: false,
+      },
+      {
+        id: 3,
+        text: '揽收订单',
+        selected: false,
+      },
+      {
+        id: 4,
+        text: '其他订单',
+        selected: false,
+      },
+    ],
+  },
+  {
+    id: 2,
+    text: '综合',
+    selected: true,
+    children: [
+      {
+        id: 1,
+        text: '全部订单',
+        selected: false,
+      },
+      {
+        id: 2,
+        text: '派送订单',
+        selected: false,
+      },
+      {
+        id: 3,
+        text: '揽收订单',
+        selected: false,
+      },
+      {
+        id: 4,
+        text: '其他订单',
+        selected: false,
+      },
+    ],
+  },
+];
+export default {
+  data() {
+    return {
+      num: 1,
+      titlenum: 1,
+      max: 6,
+      isVisible1: false,
+      list,
+      list1: cloneDeep(list),
+    };
+  },
+
+  methods: {
+    onOpen() {
+      this.isVisible1 = true;
+    },
+    onClose() {
+      this.isVisible1 = false;
+    },
+    onOK(data) {
+      console.log('onReset data = ', data);
+    },
+    onReset(data, index) {
+      console.log('onReset data = ', data, ', index = ', index);
+    },
+    chooseMenu(item, index, list) {},
+    onMaxTip() {
+      alert('超过了最大选择数');
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.hidden {
+  display: none;
+}
+
+.custom-wrap {
+  height: 260px;
+  line-height: 260px;
+  text-align: center;
+}
+.nut-menu.custom-nut-menu {
+}
+.nut-menu-title {
+  width: 50px !important;
+}
+</style>

+ 240 - 0
src/packages/menu/doc.md

@@ -0,0 +1,240 @@
+# Menu 菜单
+
+## 基础样式
+```html
+<nut-menu 
+    mask
+    :list="list1"
+    @open="onOpen" 
+    @close="onClose"
+/>
+```
+
+## 多选样式
+```html
+<nut-menu 
+   mask 
+   :list="list1" 
+   type="multiple"
+   :max="2"
+   @maxTip="onMaxTip"
+   @reset="onReset"
+   @ok="onOK"
+/>
+```
+
+## 单选且为单列数据
+
+```html
+<nut-menu
+    mask 
+    :list="list" 
+    type="simple" 
+    @open="onOpen" 
+    @close="onClose"
+    @reset="onReset"
+    @ok="onOK" 
+    @maxtip="onReset" 
+/>
+list:
+[
+    {
+    id: 1,
+    text: '全部订单',
+    selected: false,
+    },
+    {
+    id: 2,
+    text: '派送订单',
+    selected: false,
+    },
+    {
+    id: 3,
+    text: '揽收订单',
+    selected: false,
+    },
+    {
+    id: 4,
+    text: '其他订单',
+    selected: false,
+    },
+]
+
+```
+
+## 自定义菜单
+
+```html
+<nut-menu>
+    <div slot="custom" class="custom-wrap"><span>自定义</span></div>
+</nut-menu>
+```
+## DEMO
+```javascript
+<template>
+  <div>
+    <h4>基础样式</h4>
+    <nut-menu mask :list="list"  @open="onOpen" @close="onClose" />
+    <h4>多选样式</h4>
+    <nut-menu :class="{ hidden: isVisible1 }" mask :list="list1" type="multiple" :max="2" @maxTip="onMaxTip"  @reset="onReset" @ok="onOK"></nut-menu>
+  </div>
+</template>
+
+<script>
+import cloneDeep from 'lodash.clonedeep';
+const list = [
+  {
+    id: 0,
+    text: '综合',
+    selected: true,
+    children: [
+      {
+        id: 1,
+        text: '全部订单',
+        selected: false,
+      },
+      {
+        id: 2,
+        text: '派送订单',
+        selected: false,
+      },
+      {
+        id: 3,
+        text: '揽收订单',
+        selected: false,
+      },
+      {
+        id: 4,
+        text: '其他订单',
+        selected: false,
+      },
+    ],
+  },
+  {
+    id: 1,
+    text: '综合',
+    selected: true,
+    children: [
+      {
+        id: 1,
+        text: '全部订单',
+        selected: false,
+      },
+      {
+        id: 2,
+        text: '派送订单',
+        selected: false,
+      },
+      {
+        id: 3,
+        text: '揽收订单',
+        selected: false,
+      },
+      {
+        id: 4,
+        text: '其他订单',
+        selected: false,
+      },
+    ],
+  },
+  {
+    id: 2,
+    text: '综合',
+    selected: true,
+    children: [
+      {
+        id: 1,
+        text: '全部订单',
+        selected: false,
+      },
+      {
+        id: 2,
+        text: '派送订单',
+        selected: false,
+      },
+      {
+        id: 3,
+        text: '揽收订单',
+        selected: false,
+      },
+      {
+        id: 4,
+        text: '其他订单',
+        selected: false,
+      },
+    ],
+  },
+]
+export default {
+  data() {
+    return {
+      num: 1,
+      titlenum: 1,
+      max: 6,
+      isVisible1: false,
+      list,
+      list1: cloneDeep(list),
+    };
+  },
+
+  methods: {
+    onOpen() {
+      this.isVisible1 = true;
+    },
+    onClose() {
+      this.isVisible1 = false;
+    },
+    onOK(data) {
+      console.log('onReset data = ', data);
+    },
+    onReset(data, index) {
+      console.log('onReset data = ', data, ', index = ', index);
+    },
+    chooseMenu(item, index, list) {},
+    onMaxTip() {
+      alert('超过了最大选择数');
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.hidden {
+  display: none;
+}
+
+.custom-wrap {
+  height: 260px;
+  line-height: 260px;
+  text-align: center;
+}
+.nut-menu.custom-nut-menu {
+}
+.nut-menu-title {
+  width: 50px !important;
+}
+</style>
+
+
+```
+## Prop
+
+| 字段 | 说明 | 类型 | 默认值
+|----- | ----- | ----- | -----
+| isAnimation | 是否启用动画 | Boolean | true
+| visible | 是否显示 | Boolean | false
+| type | 单选simple,多选multiple | String | simple
+| col | 显示的列数 | String/Number | 1
+| max | 多选下,允许选择的最大标签数 | String/Number | 5
+| list | 选项列表 | Array | [ { text: '全部订单', selected: false }, ...] 或 [ {children:[{ text: '全部订单', selected: false }]}, ...]
+
+## Event
+
+| 字段 | 说明 | 回调参数
+|----- | ----- | -----
+| close | 遮罩点击 | 无
+| open | 面板展开 | 无
+| choose | 单选下,选择之后触发 | 1.选择的列表对象。2.列表索引
+| maxTip | 多选下,选择的数量最大时触发 | 无
+| reset | 多选下,重置按钮触发 | 返回重置之后的列表
+| ok | 多选下,点击确定之后触发 | 返回列表

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

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

+ 185 - 0
src/packages/menu/menu.scss

@@ -0,0 +1,185 @@
+$titleHeight: 50px;
+
+.nut-menu {
+  position: relative;
+  height: $titleHeight;
+
+  &-mask {
+    position: absolute;
+    width: 100%;
+    height: 100vh;
+    background-color: $mask-bg;
+  }
+
+  &-title {
+    position: absolute;
+    width: 100%;
+    display: flex;
+    flex-flow: row;
+    justify-content: space-around;
+    z-index: $zindex-dialog;
+    background-color: white;
+
+    &-wrapper {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      width: auto;
+      min-width: 60px;
+      height: $titleHeight;
+      margin: 0 20px;
+      background: rgba(255, 255, 255, 1);
+      flex: 1;
+
+      span {
+        width: 100%;
+        max-width: 85px;
+        height: 24px;
+        line-height: 24px;
+        font-size: 15px;
+        font-family: PingFangSC-Regular;
+        color: rgba(100, 100, 100, 1);
+        padding-right: 3px;
+        @include text-ellipsis();
+      }
+
+      .nut-icon svg,
+      .nut-icon {
+        color: rgba(200, 200, 200, 1) !important;
+        width: 10px;
+        height: 16px;
+      }
+
+      .nut-icon-up {
+        transform: rotate(180deg);
+      }
+    }
+  }
+
+  &-panel {
+    top: $titleHeight;
+    display: block;
+    width: 100%;
+    position: absolute;
+    left: 0;
+    color: $title-color;
+    overflow: hidden;
+    background-color: #fff;
+    box-sizing: border-box;
+    -webkit-overflow-scrolling: touch;
+    z-index: $zindex-dialog;
+    box-shadow: inset 0px 1px 3px -1px #c8c7cc;
+  }
+
+  &-simple {
+    padding-top: 5px;
+
+    ul {
+      position: relative;
+      padding: 0;
+      margin: 0;
+      min-height: 190px;
+      max-height: 300px;
+      list-style: none;
+      overflow-y: auto;
+
+      li {
+        position: relative;
+        width: 100%;
+        height: 48px;
+        line-height: 45px;
+        padding: 0 0 0 20px;
+        font-size: $font-size-base;
+        box-sizing: border-box;
+        @include text-ellipsis();
+
+        &.selected {
+          color: $primary-color;
+        }
+
+        &:before {
+          content: ' ';
+          height: 1px;
+          position: absolute;
+          left: 20px;
+          right: 20px;
+          bottom: 0;
+          z-index: 1;
+          pointer-events: none;
+          background-color: #e5e5e5;
+          transform: scaleY(0.5);
+          transform-origin: 50% 100%;
+        }
+      }
+    }
+
+    li.selected {
+      span {
+        position: absolute;
+        top: 10px;
+        right: 10px;
+      }
+    }
+  }
+
+  &-multiple {
+    &-content {
+      display: flex;
+      flex-wrap: wrap;
+      flex-direction: row;
+      padding: 6px 16px;
+      max-height: 300px;
+      overflow-y: auto;
+    }
+    li {
+      .nut-checkbox {
+        margin-right: 10px;
+        vertical-align: middle;
+      }
+
+      label {
+        vertical-align: middle;
+      }
+    }
+
+    .nut-tag {
+      width: calc((100% - 20px) / 3);
+      background: #f7f7f7;
+      color: #646464;
+      margin: 3px;
+      padding: 10px;
+      display: unset;
+      text-align: center;
+      min-width: 25%;
+      font-size: 15px;
+      line-height: 15px;
+      @include text-ellipsis();
+    }
+
+    .nut-menu-selected {
+      background: #fce9e8;
+      color: #f62f2f;
+    }
+  }
+
+  &-item {
+    width: 100%;
+    height: 100%;
+    background-color: white;
+  }
+
+  &-button-group {
+    display: flex;
+    flex-flow: row;
+    margin-top: 5px;
+
+    .nut-button:first-child {
+      border: none;
+      box-shadow: inset 0px 1px 1px -1px #c8c7cc;
+    }
+
+    .nut-button:last-child {
+      border-right: none;
+    }
+  }
+}

+ 228 - 0
src/packages/menu/menu.vue

@@ -0,0 +1,228 @@
+<template>
+  <div class="nut-menu">
+    <transition :name="!isAnimation ? 'nutFade': ''">
+      <div class="nut-menu-mask" @click="onMaskClick" v-show="isVisible && mask" />
+    </transition>
+    <div class="nut-menu-title">
+      <div v-for="(title, index) in getMenuTitles" class="nut-menu-title-wrapper" :key="index" @click="onMenu(index)">
+        <span>{{ title }}</span>
+        <nut-icon type="self" :url="require('../../assets/svg/down.svg')" :class="isVisible && index === menuIndex ? 'nut-icon-up' : ''"></nut-icon>
+      </div>
+    </div>
+    <transition :name="!isAnimation ? 'nutSlideDown': ''">
+      <div class="nut-menu-panel" v-show="isVisible">
+        <div class="nut-menu-simple" v-if="type === 'simple' && !$slots.custom">
+          <ul>
+            <li
+              v-for="(item, index) in getMenuData"
+              :key="index"
+              :class="'nut-menu-item ' + (item.selected ? 'selected' : '')"
+              @click="onSelectedItem(item, index)"
+            >
+              {{ item.text }}
+            </li>
+          </ul>
+        </div>
+        <div class="nut-menu-multiple" v-if="type === 'multiple'">
+          <div class="nut-menu-multiple-content">
+            <nut-tag
+              v-for="(item, index) in menuData"
+              :key="index"
+              :class="'nut-menu-item ' + (item.selected ? 'nut-menu-selected' : '')"
+              @click="onSelectedItem(item, index)"
+            >
+              {{ item.text }}
+            </nut-tag>
+          </div>
+          <div class="nut-menu-button-group">
+            <nut-button block type="light" @click="onReset">{{ resetBtnTxt }}</nut-button>
+            <nut-button block @click="onOk">{{ okBtnTxt }}</nut-button>
+          </div>
+        </div>
+        <div class="menu-menu-custom" v-if="$slots.custom">
+          <slot name="custom"></slot>
+        </div>
+      </div>
+    </transition>
+  </div>
+</template>
+<script>
+import Button from '../button/button.vue';
+import Tag from '../tag/tag.vue';
+import Icon from '../icon/icon.vue';
+import locale from '../../mixins/locale';
+import cloneDeep from 'lodash.clonedeep'
+
+const lockMaskScroll = ((bodyCls) => {
+  let scrollTop;
+  return {
+    afterOpen: function () {
+      scrollTop = document.scrollingElement.scrollTop || document.body.scrollTop;
+      document.body.classList.add(bodyCls);
+      document.body.style.top = -scrollTop + 'px';
+    },
+    beforeClose: function () {
+      if (document.body.classList.contains(bodyCls)) {
+        document.body.classList.remove(bodyCls);
+        document.scrollingElement.scrollTop = scrollTop;
+      }
+    },
+  };
+})('dialog-open');
+export default {
+  name: 'nut-menu',
+  mixins: [locale],
+  components: {
+    'nut-button': Button,
+    'nut-icon': Icon,
+    'nut-tag': Tag,
+  },
+  props: {
+    isAnimation: {
+      //是否有动画效果
+      type: Boolean,
+      default: false,
+    },
+    visible: {
+      //是否显示
+      type: Boolean,
+      default: false,
+    },
+    mask: {
+      //是否显示mask
+      type: Boolean,
+      default: false,
+    },
+    type: {
+      //单选 simple  多选  multiple
+      type: String,
+      default: 'simple',
+    },
+    col: {
+      //显示的列数
+      type: [Number, String],
+      default: 1,
+    },
+    max: {
+      type: [String, Number],
+      default: 5,
+    },
+    list: {
+      type: Array,
+      default: () => {
+        return [];
+      },
+    },
+  },
+  watch: {
+    isVisible(val) {
+      lockMaskScroll[val ? 'afterOpen' : 'beforeClose']();
+    },
+  },
+  data() {
+    return {
+      menuData: [], //多选模式,数据临时数据
+      datalist: [], // menu组件数据源
+      menuIndex: 0, // 当前列索引
+      isVisible: false, // 当前面板是否打开
+    };
+  },
+  mounted() {
+    this.datalist = this.list;
+    this.isVisible = this.visible;
+  },
+  computed: {
+    okBtnTxt() {
+      return this.nutTranslate('lang.okBtnTxt');
+    },
+    resetBtnTxt() {
+      return this.nutTranslate('lang.menu.resetBtnTxt');
+    },
+    // 判断数据源为复杂类型,即多元数组
+    isMulColumn() {
+      if (!this.datalist || !this.datalist.length) return false;
+      const firstChildren = this.datalist[0].children;
+      const mulColumn = firstChildren && firstChildren.length;
+      return !!mulColumn;
+    },
+    // 获取nut-menu-title
+    getMenuTitles() {
+      if (!this.datalist || !this.datalist.length) return ['请选择'];
+      if (!this.isMulColumn) {
+        return [
+          this.datalist
+            .filter((item) => item.selected)
+            .map((item) => item.text)
+            .join() || '请选择',
+        ];
+      } else {
+        return this.datalist.map((item) => {
+          const children = item.children || [];
+          return (
+            children
+              .filter((item) => item.selected)
+              .map((item) => item.text)
+              .join() || '请选择'
+          );
+        });
+      }
+    },
+    // 获取选中但前列的数据源
+    getMenuData() {
+      if (this.isMulColumn) {
+        return this.datalist[this.menuIndex].children || [];
+      }
+      return this.datalist;
+    },
+  },
+  methods: {
+    onSelectedItem(item, index) {
+      // 多选
+      if (this.type === 'multiple') {
+        const isSelected = item.selected;
+        // 多选时,会限制最大选项数
+        if (!isSelected) {
+          const selectedCount = this.menuData.filter((item) => item.selected).length;
+          if (selectedCount >= Number(this.max)) {
+            this.$emit('maxTip');
+            return;
+          }
+        }
+        item.selected = !item.selected;
+      } else {
+        // 单选
+        this.getMenuData.forEach((item) => item.selected && (item.selected = false));
+        item.selected = true;
+        this.close();
+      }
+      this.$emit('choose', item, index);
+    },
+    onMenu(index) {
+      this.isVisible = this.menuIndex !== index || !this.isVisible;
+      this.menuIndex = index;
+      if (this.isVisible) {
+        this.menuData = cloneDeep(this.datalist[index].children)
+        this.$emit('open');
+      } else {
+        this.$emit('close');
+      }
+    },
+    onReset() {
+      this.getMenuData.forEach((item) => item.selected && (item.selected = false));
+      this.$emit('reset', this.datalist, this.menuIndex);
+    },
+    onOk() {
+      this.datalist[this.menuIndex].children = this.menuData;
+      this.close()
+      this.$emit('ok', this.datalist);
+    },
+    close() {
+      this.isVisible = false;
+      this.$emit('close');
+    },
+    onMaskClick() {
+      this.close()
+    },
+  },
+};
+</script>