浏览代码

upd: 优化 Menu 组件 (#844)

* feat: 修改 Menu 组件

* fix: 微调一处体验问题

* fix: 为 sass 变量增加 后缀

* fix: 去除是否有动画属性

* fix: demo满屏增加full样式类
yangjinjun3 4 年之前
父节点
当前提交
143f98f2a7

+ 65 - 59
src/packages/__VUE/menu/demo.vue

@@ -1,90 +1,96 @@
 <template>
-  <div class="demo">
+  <div class="demo full">
     <h2>基础用法</h2>
     <nut-menu>
-      <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
+      <nut-menu-item v-model="state.value1" :options="state.options1" />
+      <nut-menu-item v-model="state.value2" @change="handleChange" :options="state.options2" />
     </nut-menu>
-    <h2>两列标题</h2>
+    <h2>自定义菜单内容</h2>
     <nut-menu>
-      <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
-      <nut-menu-item title="默认排序" :options="options2"></nut-menu-item>
-    </nut-menu>
-    <h2>获取选择的列表对象</h2>
-    <nut-menu @choose="handleChoose">
-      <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
-    </nut-menu>
-    <h2>一行两列列表对象</h2>
-    <nut-menu col="2">
-      <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
+      <nut-menu-item v-model="state.value1" :options="state.options1" />
+      <nut-menu-item title="筛选" ref="item">
+        <div :style="{ display: 'flex', flex: 1, 'justify-content': 'space-between', 'align-items': 'center' }">
+          <div>自定义内容</div>
+          <nut-button @click="onConfirm">关闭</nut-button>
+        </div>
+      </nut-menu-item>
     </nut-menu>
-    <h2>禁用菜单</h2>
+    <h2>一行两列</h2>
     <nut-menu>
-      <nut-menu-item
-        title="全部商品"
-        disabled="true"
-        :options="options1"
-      ></nut-menu-item>
+      <nut-menu-item v-model="state.value3" :cols="2" :options="state.options3" />
     </nut-menu>
-    <h2>自定义选项的选中态图标颜色</h2>
-    <nut-menu active-color="#0f0">
-      <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
+    <h2>自定义选中态颜色</h2>
+    <nut-menu active-color="green">
+      <nut-menu-item v-model="state.value1" :options="state.options1" />
+      <nut-menu-item v-model="state.value2" @change="handleChange" :options="state.options2" />
     </nut-menu>
-    <h2>自定义菜单内容</h2>
+    <h2>禁用菜单</h2>
     <nut-menu>
-      <nut-menu-item title="筛选" ref="item">
-        <div
-          :style="{
-            display: 'flex',
-            'justify-content': 'space-between',
-            'align-items': 'center'
-          }"
-        >
-          <div :style="{ 'font-size': '12px' }">我是自定义内容</div>
-          <nut-button @click="handleClick">关闭</nut-button>
-        </div>
-      </nut-menu-item>
+      <nut-menu-item disabled v-model="state.value1" :options="state.options1" />
+      <nut-menu-item disabled v-model="state.value2" @change="handleChange" :options="state.options2" />
     </nut-menu>
   </div>
 </template>
 
-<script lang="ts">
-import { ref, reactive, toRefs } from 'vue';
+<script>
+import { reactive, ref } from 'vue';
 import { createComponent } from '../../utils/create';
 const { createDemo } = createComponent('menu');
 export default createDemo({
   props: {},
   setup() {
-    const options1 = [
-      { text: '全部商品', value: 0 },
-      { text: '新款商品', value: 1 },
-      { text: '活动商品', value: 2 }
-    ];
-
-    const options2 = [
-      { text: '默认排序', value: 'a' },
-      { text: '好评排序', value: 'b' },
-      { text: '销量排序', value: 'c' }
-    ];
+    const state = reactive({
+      options1: [
+        { text: '全部商品', value: 0 },
+        { text: '新款商品', value: 1 },
+        { text: '活动商品', value: 2 }
+      ],
+      options2: [
+        { text: '默认排序', value: 'a' },
+        { text: '好评排序', value: 'b' },
+        { text: '销量排序', value: 'c' }
+      ],
+      options3: [
+        { text: '全部商品', value: 0 },
+        { text: '家庭清洁/纸品', value: 1 },
+        { text: '个人护理', value: 2 },
+        { text: '美妆护肤', value: 3 },
+        { text: '食品饮料', value: 4 },
+        { text: '家用电器', value: 5 },
+        { text: '母婴', value: 6 },
+        { text: '数码', value: 7 },
+        { text: '电脑、办公', value: 8 },
+        { text: '运动户外', value: 9 },
+        { text: '厨具', value: 10 },
+        { text: '医疗保健', value: 11 },
+        { text: '酒类', value: 12 },
+        { text: '生鲜', value: 13 },
+        { text: '家具', value: 14 },
+        { text: '传统滋补', value: 15 },
+        { text: '汽车用品', value: 16 },
+        { text: '家居日用', value: 17 }
+      ],
+      value1: 0,
+      value2: 'a',
+      value3: 0
+    });
 
-    const item = ref<HTMLElement>();
+    const item = ref('');
 
-    const handleChoose = (val: string, index: string | number) => {
-      console.log(val, index);
+    const onConfirm = () => {
+      item.value.toggle();
     };
 
-    const handleClick = () => {
-      (item.value as any).toggle();
+    const handleChange = (val) => {
+      console.log('val', val);
     };
 
     return {
-      options1,
-      options2,
+      state,
       item,
-      handleChoose,
-      handleClick
+      onConfirm,
+      handleChange
     };
   }
 });
 </script>
-
-<style lang="scss" scoped></style>

+ 89 - 80
src/packages/__VUE/menu/doc.md

@@ -9,12 +9,11 @@
 ``` javascript
 import { createApp } from 'vue';
 // vue
-import { Menu } from '@nutui/nutui';
+import { Menu, MenuItem } from '@nutui/nutui';
 // taro
-import { Menu,MenuItem } from '@nutui/nutui-taro';
+import { Menu, MenuItem } from '@nutui/nutui-taro';
 const app = createApp();
 app.use(Menu);
-app.use(MenuItem);
 
 ```
 
@@ -24,122 +23,132 @@ app.use(MenuItem);
 
 ```html
 <nut-menu>
-  <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
+  <nut-menu-item v-model="state.value1" :options="state.options1" />
+  <nut-menu-item v-model="state.value2" @change="handleChange" :options="state.options2" />
 </nut-menu>
 ```
 ```js
- setup() {
-  const options1 = [
-      { text: '全部商品', value: 0 },
-      { text: '新款商品', value: 1 },
-      { text: '活动商品', value: 2 },
-  ]
-
-  const options2 = [
-    { text: '默认排序', value: 'a' },
-    { text: '好评排序', value: 'b' },
-    { text: '销量排序', value: 'c' },
-  ]
- }
-```
-
-### 两列标题
-
-```html
-<nut-menu>
-  <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
-  <nut-menu-item title="默认排序" :options="options2"></nut-menu-item>
-</nut-menu>
-```
-
-### 获取选择的列表对象
-
-```html
-<nut-menu @choose="handleChoose">
-  <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
-</nut-menu>
-```
-```js
- setup() {
-  const handleChoose = (val, index) => {
-    console.log(val, index)
+import { reactive, ref } from 'vue';
+
+export default {
+  setup() {
+    const state = reactive({
+      options1: [
+        { text: '全部商品', value: 0 },
+        { text: '新款商品', value: 1 },
+        { text: '活动商品', value: 2 }
+      ],
+      options2: [
+        { text: '默认排序', value: 'a' },
+        { text: '好评排序', value: 'b' },
+        { text: '销量排序', value: 'c' },
+      ],
+      options3: [
+        { text: '全部商品', value: 0 },
+        { text: '家庭清洁/纸品', value: 1 },
+        { text: '个人护理', value: 2 },
+        { text: '美妆护肤', value: 3 },
+        { text: '食品饮料', value: 4 },
+        { text: '家用电器', value: 5 },
+        { text: '母婴', value: 6 },
+        { text: '数码', value: 7 },
+        { text: '电脑、办公', value: 8 },
+        { text: '运动户外', value: 9 },
+        { text: '厨具', value: 10 },
+        { text: '医疗保健', value: 11 },
+        { text: '酒类', value: 12 },
+        { text: '生鲜', value: 13 },
+        { text: '家具', value: 14 },
+        { text: '传统滋补', value: 15 },
+        { text: '汽车用品', value: 16 },
+        { text: '家居日用', value: 17 },
+      ],
+      value1: 0,
+      value2: 'a',
+      value3: 0
+    });
+
+    const item = ref('');
+
+    const onConfirm = () => {
+      item.value.toggle();
+    }
+
+    const handleChange = val => {
+      console.log('val', val);
+    }
+
+    return {
+      state,
+      item,
+      onConfirm,
+      handleChange
+    };
   }
- }
+}
 ```
 
-### 一行两列列表对象
+### 自定义菜单内容
+使用实例上的 toggle 方法可以手动关闭弹框。
 
 ```html
-<nut-menu col="2">
-  <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
+<nut-menu>
+  <nut-menu-item v-model="state.value1" :options="state.options1" />
+  <nut-menu-item title="筛选" ref="item">
+    <div :style="{display: 'flex', flex: 1, 'justify-content': 'space-between', 'align-items': 'center'}">
+      <div>自定义内容</div>
+      <div @click="onConfirm">关闭</div>
+    </div>
+  </nut-menu-item>
 </nut-menu>
 ```
 
-### 禁用菜单
+### 一行两列
 
 ```html
 <nut-menu>
-  <nut-menu-item title="全部商品" disabled="true" :options="options1"></nut-menu-item>
+  <nut-menu-item v-model="state.value3" :cols="2" :options="state.options3" />
 </nut-menu>
 ```
 
-### 自定义选项的选中态图标颜色
+### 自定义选中态颜色
 
 ```html
-<nut-menu active-color="#0f0">
-  <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
+<nut-menu active-color="green">
+  <nut-menu-item v-model="state.value1" :options="state.options1" />
+  <nut-menu-item v-model="state.value2" @change="handleChange" :options="state.options2" />
 </nut-menu>
 ```
 
-### 自定义菜单内容
+### 禁用菜单
 
 ```html
 <nut-menu>
-  <nut-menu-item title="筛选" ref="item">
-    <div
-      :style="{
-        display: 'flex',
-        'justify-content': 'space-between',
-        'align-items': 'center'
-      }"
-    >
-      <div :style="{ 'font-size': '12px' }">我是自定义内容</div>
-      <nut-button @click="handleClick">关闭</nut-button>
-    </div>
-  </nut-menu-item>
+  <nut-menu-item disabled v-model="state.value1" :options="state.options1" />
+  <nut-menu-item disabled v-model="state.value2" @change="handleChange" :options="state.options2" />
 </nut-menu>
 ```
-```js
- setup() {
-  const item = ref<HTMLElement>()
-
-  const handleClick = () => {
-    (item.value as any).toggle()
-  }
- }
-```
 
 ## API
 
-### Props
-
-### nut-menu
+### Menu Props
 
 | 参数         | 说明                             | 类型   | 默认值           |
 |--------------|----------------------------------|--------|------------------|
-| col         | 显示的列数     | String/Number | 1                |
-| active-color         | 选项的选中态图标颜色     | String | #f00               |
+| active-color         | 选项的选中态图标颜色     | String | #F2270C               |
 
-### nut-menu-item
+### MenuItem Props
 
 | 参数         | 说明                             | 类型   | 默认值           |
 |--------------|----------------------------------|--------|------------------|
-| title         | 标题     | String | -                |
-| options         | 列表对象     | Array | -                |
+| title         | 菜单项标题     | String | 当前选中项文字               |
+| options         | 选项数组     | Array | -                |
 | disabled         | 是否禁用菜单     | Boolean | false                |
+| cols         | 可以设置一行展示多少列 options     | Number | 1                |
+| title-icon         | 自定义标题图标     | String | 'down-arrow'                |
 
-### Events
+### MenuItem Events
 
 | 事件名 | 说明           | 回调参数     |
 |--------|----------------|--------------|
-| choose  | 单选下,选择之后触发 | 1.选择的列表对象。2.列表索引 |
+| change  | 选择 option 之后触发 | 选择的 value |

+ 32 - 60
src/packages/__VUE/menu/index.scss

@@ -1,73 +1,45 @@
-.nut-menu {
+.nut-menu__bar {
   position: relative;
-  font-size: 14px;
-  color: #1a1a1a;
+  display: flex;
+  line-height: $nut-menu-bar-line-height;
+  background-color: $white;
+  border-bottom: 1px solid $nut-menu-bar-border-bottom-color;
 
-  .title-list {
-    display: flex;
-    line-height: 46px;
-    align-items: center;
-    background-color: #fff;
-    border-bottom: 1px solid #eaf0fb;
-
-    .title {
-      flex: 1;
-      text-align: center;
-
-      &.is-active {
-        font-weight: 600;
-      }
-
-      &.disabled {
-        color: #999;
-      }
-
-      [class*='nut-icon-arrow'] {
-        vertical-align: middle;
-        margin-right: 2px;
-      }
-    }
+  &.opened {
+    z-index: $nut-menu-bar-opened-z-index;
   }
 
-  %itemCommon {
-    position: absolute;
-    left: 0;
-    width: 100%;
-    padding-top: 12px;
-    padding-bottom: 12px;
-    border-radius: 0 0 12px 12px;
-    background-color: #fff;
-  }
+  .nut-menu__item {
+    flex: 1;
+    text-align: center;
+    font-size: $font-size-2;
+    color: $title-color;
+    min-width: 0;
 
-  .option-list {
-    @extend %itemCommon;
-    max-height: 214px;
-    overflow-y: auto;
+    &.disabled {
+      color: $nut-menu-item-disabled-color;
+    }
 
-    ul {
-      display: flex;
-      flex-wrap: wrap;
-      list-style: none;
-      margin: 0;
-      padding: 0;
+    .nut-menu__title-icon {
+      transition: all 0.2s linear;
     }
 
-    li {
-      line-height: 38px;
-      padding-left: 24px;
-      box-sizing: border-box;
+    .nut-menu__title {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      max-width: 100%;
+
+      .nut-menu__title-text {
+        @include text-ellipsis;
+        display: block;
+        padding-left: $nut-menu-title-text-padding-left;
+        padding-right: $nut-menu-title-text-padding-right;
+      }
 
-      .nut-icon-Check {
-        vertical-align: middle;
-        margin-right: 6px;
+      &.active .nut-menu__title-icon {
+        transform: rotate(180deg);
       }
     }
   }
-
-  .customer-item {
-    @extend %itemCommon;
-    padding-left: 24px;
-    padding-right: 24px;
-    box-sizing: border-box;
-  }
 }

+ 85 - 163
src/packages/__VUE/menu/index.taro.vue

@@ -1,166 +1,81 @@
 <template>
-  <view>
-    <nut-popup
-      v-bind="$attrs"
-      v-model:visible="showMask"
-      @close="handleClose"
-    ></nut-popup>
-    <div :class="classes" :style="{ 'z-index': nutMenuIndex }">
-      <div class="title-list">
-        <div
-          v-for="(menu, index) in menuList"
-          :key="index"
-          class="title"
-          :class="{
-            'is-active': activeTitle === menu.title,
-            disabled: menu.disabled
-          }"
-          @click="!menu.disabled && handleClickTitle(menu.title, index)"
+  <view :class="classes">
+    <view class="nut-menu__bar" :class="{ opened: opened }" ref="barRef">
+      <template v-for="(item, index) in children" :key="index">
+        <view
+          class="nut-menu__item"
+          @click="!item.disabled && toggleItem(index)"
+          :class="{ disabled: item.disabled }"
+          :style="{ color: item.state.showPopup ? activeColor : '' }"
         >
-          {{ menu.title }}
-          <nut-icon
-            size="10"
-            color="#333"
-            name="arrow-up"
-            v-if="activeTitle === menu.title"
-          ></nut-icon>
-          <nut-icon size="10" color="#999" name="arrow-down" v-else></nut-icon>
-        </div>
-      </div>
-      <div
-        :key="index"
-        v-for="(menu, index) in menuList"
-        v-show="hasOptions && activeTitle === menu.title"
-        class="option-list"
-      >
-        <ul>
-          <li
-            v-for="(option, index2) in menu.options"
-            :key="index2"
-            @click="handleClickOption(option.text, index, option.value)"
-            :style="styleObj"
-          >
-            <nut-icon
-              size="20"
-              :color="activeColor"
-              name="Check"
-              v-if="menu.title === option.text"
-            ></nut-icon
-            >{{ option.text }}
-          </li>
-        </ul>
-      </div>
-      <view v-show="!hasOptions && isShowCustomer" class="customer-item">
-        <slot></slot>
-      </view>
-    </div>
+          <view class="nut-menu__title" :class="{ active: item.state.showPopup }">
+            <view class="nut-menu__title-text">{{ item.renderTitle() }}</view>
+            <nut-icon :name="item.titleIcon" size="10" class="nut-menu__title-icon"></nut-icon>
+          </view>
+        </view>
+      </template>
+    </view>
+    <slot></slot>
   </view>
 </template>
 <script lang="ts">
-import { toRefs, ref, computed, onMounted, reactive, toRaw } from 'vue';
+import { reactive, provide, computed, ref, Ref, unref } from 'vue';
 import { createComponent } from '../../utils/create';
-import Icon from '../icon/index.vue';
+import Taro from '@tarojs/taro';
 const { componentName, create } = createComponent('menu');
 export default create({
-  components: {
-    [Icon.name]: Icon
-  },
   props: {
-    col: {
-      type: [String, Number],
-      default: 1
-    },
     activeColor: {
       type: String,
-      default: '#f00'
+      default: '#FA2C19'
+    },
+    overlay: {
+      type: Boolean,
+      default: true as const
+    },
+    duration: {
+      type: [Number, String],
+      default: 0.3
     }
   },
-  emits: ['choose'],
-  setup(props, { slots, emit }) {
-    interface IOption {
-      text: string,
-      value: string | number
-    }
-
-    interface IMenuItem {
-      title: string,
-      disabled: boolean,
-      options?: Array<IOption>
-    }
+  setup(props, { emit, slots }) {
+    const barRef = ref<HTMLElement>();
+    const offset = ref(0);
 
-    const menuList:Array<IMenuItem> = reactive([]);
-    let activeTitle = ref('');
-    let showMask = ref(false);
-    let styleObj = reactive({
-      flexBasis: 100 / Number(props.col) + '%'
-    });
-    let nutMenuIndex = ref<String | Number>('auto');
-    let hasOptions = ref(true);
-    let isShowCustomer = ref(false);
+    const useChildren = () => {
+      const publicChildren: any[] = reactive([]);
+      const internalChildren: any[] = reactive([]);
 
-    if(slots.default){
-      for (let i = 0; i < slots.default().length; i++) {
-        if ((slots.default()[i] as any).type['name'] === 'nut-menu-item') {
-          let item:IMenuItem = {
-            title: (slots.default()[i] as any).props['title'],
-            disabled: !!(slots.default()[i] as any).props['disabled']
-          };
-          if ((slots.default()[i] as any).props['options']) {
-            item['options'] = (slots.default()[i] as any).props['options'];
-          } else {
-            hasOptions.value = false;
+      const linkChildren = (value?: any) => {
+        const link = (child: any) => {
+          if (child.proxy) {
+            internalChildren.push(child);
+            publicChildren.push(child.proxy as any);
           }
-          menuList.push(item);
-        }
-      }
-    }
+        };
 
-    const handleClickTitle = (title: string, index: number) => {
-      if (!hasOptions.value) {
-        if (activeTitle.value) {
-          activeTitle.value = '';
-          isShowCustomer.value = false;
-          showMask.value = false;
-          nutMenuIndex.value = 'auto';
-        } else {
-          activeTitle.value = title;
-          isShowCustomer.value = true;
-          showMask.value = true;
-          nutMenuIndex.value = 2001;
-        }
-        return;
-      }
+        provide(
+          'menuParent',
+          Object.assign(
+            {
+              link,
+              children: publicChildren,
+              internalChildren
+            },
+            value
+          )
+        );
+      };
 
-      if (menuList.length > 1) {
-        if (activeTitle.value === title) {
-          activeTitle.value = '';
-          nutMenuIndex.value = 'auto';
-          showMask.value = false;
-        } else {
-          activeTitle.value = title;
-          nutMenuIndex.value = 2001;
-          showMask.value = true;
-        }
-      } else {
-        if (activeTitle.value) {
-          activeTitle.value = '';
-          nutMenuIndex.value = 'auto';
-          showMask.value = false;
-        } else {
-          activeTitle.value = title;
-          nutMenuIndex.value = 2001;
-          showMask.value = true;
-        }
-      }
+      return {
+        children: publicChildren,
+        linkChildren
+      };
     };
 
-    const handleClickOption = (text: string, index: number, value: string | number) => {
-      menuList[index].title = text;
-      activeTitle.value = '';
-      showMask.value = false;
-      nutMenuIndex.value = 'auto';
-      emit('choose', text, value);
-    };
+    const { children, linkChildren } = useChildren();
+
+    const opened = computed(() => children.some((item) => item.state.showWrapper));
 
     const classes = computed(() => {
       const prefixCls = componentName;
@@ -169,32 +84,39 @@ export default create({
       };
     });
 
-    const handleClose = () => {
-      activeTitle.value = '';
-      nutMenuIndex.value = 'auto';
-
-      if (isShowCustomer.value) {
-        isShowCustomer.value = false;
+    const updateOffset = () => {
+      if (barRef.value) {
+        setTimeout(() => {
+          Taro.createSelectorQuery()
+            .select('.nut-menu__bar.opened')
+            .boundingClientRect((rect) => {
+              offset.value = rect.bottom;
+            })
+            .exec();
+        }, 100);
       }
     };
 
+    linkChildren({ props, offset });
+
+    const toggleItem = (active: number) => {
+      children.forEach((item, index) => {
+        if (index === active) {
+          updateOffset();
+          item.toggle();
+        } else if (item.state.showPopup) {
+          item.toggle(false, { immediate: true });
+        }
+      });
+    };
+
     return {
-      menuList,
-      activeTitle,
+      toggleItem,
+      children,
+      opened,
       classes,
-      showMask,
-      styleObj,
-      nutMenuIndex,
-      hasOptions,
-      isShowCustomer,
-      handleClickTitle,
-      handleClickOption,
-      handleClose
+      barRef
     };
   }
 });
 </script>
-
-<style lang="scss">
-@import 'index.scss';
-</style>

+ 79 - 163
src/packages/__VUE/menu/index.vue

@@ -1,166 +1,81 @@
 <template>
-  <view>
-    <nut-popup
-      v-bind="$attrs"
-      v-model:visible="showMask"
-      @close="handleClose"
-    ></nut-popup>
-    <div :class="classes" :style="{ 'z-index': nutMenuIndex }">
-      <div class="title-list">
-        <div
-          v-for="(menu, index) in menuList"
-          :key="index"
-          class="title"
-          :class="{
-            'is-active': activeTitle === menu.title,
-            disabled: menu.disabled
-          }"
-          @click="!menu.disabled && handleClickTitle(menu.title, index)"
+  <view :class="classes">
+    <view class="nut-menu__bar" :class="{ opened: opened }" ref="barRef">
+      <template v-for="(item, index) in children" :key="index">
+        <view
+          class="nut-menu__item"
+          @click="!item.disabled && toggleItem(index)"
+          :class="{ disabled: item.disabled }"
+          :style="{ color: item.state.showPopup ? activeColor : '' }"
         >
-          {{ menu.title }}
-          <nut-icon
-            size="10"
-            color="#333"
-            name="arrow-up"
-            v-if="activeTitle === menu.title"
-          ></nut-icon>
-          <nut-icon size="10" color="#999" name="arrow-down" v-else></nut-icon>
-        </div>
-      </div>
-      <div
-        :key="index"
-        v-for="(menu, index) in menuList"
-        v-show="hasOptions && activeTitle === menu.title"
-        class="option-list"
-      >
-        <ul>
-          <li
-            v-for="(option, index2) in menu.options"
-            :key="index2"
-            @click="handleClickOption(option.text, index, option.value)"
-            :style="styleObj"
-          >
-            <nut-icon
-              size="20"
-              :color="activeColor"
-              name="Check"
-              v-if="menu.title === option.text"
-            ></nut-icon
-            >{{ option.text }}
-          </li>
-        </ul>
-      </div>
-      <view v-show="!hasOptions && isShowCustomer" class="customer-item">
-        <slot></slot>
-      </view>
-    </div>
+          <view class="nut-menu__title" :class="{ active: item.state.showPopup }">
+            <view class="nut-menu__title-text">{{ item.renderTitle() }}</view>
+            <nut-icon :name="item.titleIcon" size="10" class="nut-menu__title-icon"></nut-icon>
+          </view>
+        </view>
+      </template>
+    </view>
+    <slot></slot>
   </view>
 </template>
 <script lang="ts">
-import { toRefs, ref, computed, onMounted, reactive, toRaw } from 'vue';
+import { reactive, provide, computed, ref, Ref, unref } from 'vue';
 import { createComponent } from '../../utils/create';
-import Icon from '../icon/index.vue';
+import { useRect } from '../../utils/useRect';
 const { componentName, create } = createComponent('menu');
 export default create({
-  components: {
-    [Icon.name]: Icon
-  },
   props: {
-    col: {
-      type: [String, Number],
-      default: 1
-    },
     activeColor: {
       type: String,
-      default: '#f00'
+      default: '#FA2C19'
+    },
+    overlay: {
+      type: Boolean,
+      default: true as const
+    },
+    duration: {
+      type: [Number, String],
+      default: 0
     }
   },
-  emits: ['choose'],
-  setup(props, { slots, emit }) {
-    interface IOption {
-      text: string,
-      value: string | number
-    }
-
-    interface IMenuItem {
-      title: string,
-      disabled: boolean,
-      options?: Array<IOption>
-    }
+  setup(props, { emit, slots }) {
+    const barRef = ref<HTMLElement>();
+    const offset = ref(0);
 
-    const menuList:Array<IMenuItem> = reactive([]);
-    let activeTitle = ref('');
-    let showMask = ref(false);
-    let styleObj = reactive({
-      flexBasis: 100 / Number(props.col) + '%'
-    });
-    let nutMenuIndex = ref<String | Number>('auto');
-    let hasOptions = ref(true);
-    let isShowCustomer = ref(false);
+    const useChildren = () => {
+      const publicChildren: any[] = reactive([]);
+      const internalChildren: any[] = reactive([]);
 
-    if(slots.default){
-      for (let i = 0; i < slots.default().length; i++) {
-        if ((slots.default()[i] as any).type['name'] === 'nut-menu-item') {
-          let item:IMenuItem = {
-            title: (slots.default()[i] as any).props['title'],
-            disabled: !!(slots.default()[i] as any).props['disabled']
-          };
-          if ((slots.default()[i] as any).props['options']) {
-            item['options'] = (slots.default()[i] as any).props['options'];
-          } else {
-            hasOptions.value = false;
+      const linkChildren = (value?: any) => {
+        const link = (child: any) => {
+          if (child.proxy) {
+            internalChildren.push(child);
+            publicChildren.push(child.proxy as any);
           }
-          menuList.push(item);
-        }
-      }
-    }
+        };
 
-    const handleClickTitle = (title: string, index: number) => {
-      if (!hasOptions.value) {
-        if (activeTitle.value) {
-          activeTitle.value = '';
-          isShowCustomer.value = false;
-          showMask.value = false;
-          nutMenuIndex.value = 'auto';
-        } else {
-          activeTitle.value = title;
-          isShowCustomer.value = true;
-          showMask.value = true;
-          nutMenuIndex.value = 2001;
-        }
-        return;
-      }
+        provide(
+          'menuParent',
+          Object.assign(
+            {
+              link,
+              children: publicChildren,
+              internalChildren
+            },
+            value
+          )
+        );
+      };
 
-      if (menuList.length > 1) {
-        if (activeTitle.value === title) {
-          activeTitle.value = '';
-          nutMenuIndex.value = 'auto';
-          showMask.value = false;
-        } else {
-          activeTitle.value = title;
-          nutMenuIndex.value = 2001;
-          showMask.value = true;
-        }
-      } else {
-        if (activeTitle.value) {
-          activeTitle.value = '';
-          nutMenuIndex.value = 'auto';
-          showMask.value = false;
-        } else {
-          activeTitle.value = title;
-          nutMenuIndex.value = 2001;
-          showMask.value = true;
-        }
-      }
+      return {
+        children: publicChildren,
+        linkChildren
+      };
     };
 
-    const handleClickOption = (text: string, index: number, value: string | number) => {
-      menuList[index].title = text;
-      activeTitle.value = '';
-      showMask.value = false;
-      nutMenuIndex.value = 'auto';
-      emit('choose', text, value);
-    };
+    const { children, linkChildren } = useChildren();
+
+    const opened = computed(() => children.some((item) => item.state.showWrapper));
 
     const classes = computed(() => {
       const prefixCls = componentName;
@@ -169,32 +84,33 @@ export default create({
       };
     });
 
-    const handleClose = () => {
-      activeTitle.value = '';
-      nutMenuIndex.value = 'auto';
-
-      if (isShowCustomer.value) {
-        isShowCustomer.value = false;
+    const updateOffset = () => {
+      if (barRef.value) {
+        const rect = useRect(barRef);
+        offset.value = rect.bottom;
       }
     };
 
+    linkChildren({ props, offset });
+
+    const toggleItem = (active: number) => {
+      children.forEach((item, index) => {
+        if (index === active) {
+          updateOffset();
+          item.toggle();
+        } else if (item.state.showPopup) {
+          item.toggle(false, { immediate: true });
+        }
+      });
+    };
+
     return {
-      menuList,
-      activeTitle,
+      toggleItem,
+      children,
+      opened,
       classes,
-      showMask,
-      styleObj,
-      nutMenuIndex,
-      hasOptions,
-      isShowCustomer,
-      handleClickTitle,
-      handleClickOption,
-      handleClose
+      barRef
     };
   }
 });
 </script>
-
-<style lang="scss">
-@import 'index.scss';
-</style>

+ 36 - 99
src/packages/__VUE/menuitem/index.scss

@@ -1,109 +1,46 @@
 .nut-menu-item {
-  flex: 1;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  cursor: pointer;
-  padding: 0 5px;
-  &.disabled {
-    color: #999;
-  }
-  &.nut-menu-item-active {
-    .nut-menu-title {
-      font-weight: bold;
-      .icon {
-        color: #1a1a1a;
-        transition: all ease 0.3s;
-        transform: rotate(-180deg);
-        background: url('https://img14.360buyimg.com/imagetools/jfs/t1/144044/16/20214/521/5fe5bb14Ebf213fe3/40f271b982f31898.png')
-          no-repeat;
-        background-size: contain;
-      }
-    }
-    .nut-menu-panel {
-      display: block;
-    }
+  .active {
+    font-weight: 500;
   }
 }
-.nut-menu-title {
-  font-size: 14px;
+
+.nut-menu-item__content {
+  padding: $nut-menu-item-content-padding;
+  max-height: $nut-menu-item-content-max-height;
+  overflow-y: auto;
   display: flex;
-  align-items: center;
-  .title-name {
-    white-space: nowrap;
-    max-width: 100px;
-    overflow: hidden;
-    text-overflow: ellipsis;
-  }
-  .icon {
-    display: inline-block;
-    width: 6px;
-    height: 6px;
-    background: url('https://img13.360buyimg.com/imagetools/jfs/t1/152898/12/10149/452/5fd990b5Ec7c12d70/3bf06076b758bed1.png')
-      no-repeat;
-    background-size: contain;
-    color: #909ca4;
-    transform: rotate(0deg);
-    transition: all ease 0.3s;
-    margin: 0 2px;
+  flex-wrap: wrap;
+
+  .nut-menu-item__option {
+    color: $title-color;
+    font-size: $font-size-2;
+    padding-top: $nut-menu-item-option-padding-top;
+    padding-bottom: $nut-menu-item-option-padding-bottom;
+    display: flex;
+    align-items: center;
+
+    i {
+      margin-right: $nut-menu-item-option-i-margin-right;
+    }
   }
 }
-.nut-menu-panel {
-  display: none;
-  width: 100%;
+
+.nut-menu__overlay {
   position: absolute;
-  left: 0;
-  top: 46px;
-  color: #2d2d2d;
-  overflow: hidden;
-  background-color: #fff;
-  -webkit-box-sizing: border-box;
-  box-sizing: border-box;
-  -webkit-overflow-scrolling: touch;
-  z-index: 9998;
-  border-top: 1px solid #f7f8fa;
-  border-radius: 0 0 15px 15px;
-  box-shadow: 0 4px 5px 0px rgba(236, 236, 236, 0.25);
-  overflow: auto;
-  // &.active{
-  //   display: block;
-  // }
+  top: auto !important;
 }
 
-.menu-list {
-  display: flex;
-  padding: 10px 15px;
-  flex-direction: column;
-  &.bubble-line {
-    flex-flow: wrap;
-    .menu-option {
-      width: 50%;
-    }
-  }
-  &.three-line {
-    flex-flow: wrap;
-    .menu-option {
-      width: 33%;
-    }
-  }
-  .menu-option {
-    min-height: 24px;
-    line-height: 42px;
-    font-size: 14px;
-    color: #1a1a1a;
-    width: 100%;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    padding: 0 5px;
-    &.checked {
-      font-weight: bold;
-    }
-  }
-  .check-icon {
-    width: 14px;
-    height: 14px;
-    margin-right: 5px;
-    color: #fa2c19;
-  }
+.nut-menu__pop {
+  transition: all 0 ease 0;
+  transform: none;
+  top: auto !important;
+}
+
+.placeholder-element {
+  position: fixed;
+  top: -$nut-menu-bar-line-height;
+  left: 0;
+  right: 0;
+  z-index: $nut-menu-bar-opened-z-index;
+  background-color: transparent;
 }

+ 157 - 15
src/packages/__VUE/menuitem/index.taro.vue

@@ -1,25 +1,167 @@
 <template>
-  <div><slot></slot></div>
+  <view :class="classes" v-show="state.showWrapper">
+    <div
+      v-show="state.isShowPlaceholderElement"
+      @click="handleClickOutside"
+      class="placeholder-element"
+      :style="{ height: parent.offset.value + 'px' }"
+    >
+    </div>
+    <nut-popup
+      :style="{ top: parent.offset.value + 'px' }"
+      :overlayStyle="{ top: parent.offset.value + 'px' }"
+      v-bind="$attrs"
+      v-model:visible="state.showPopup"
+      position="top"
+      :duration="parent.props.duration"
+      pop-class="nut-menu__pop"
+      overlayClass="nut-menu__overlay"
+      :overlay="parent.props.overlay"
+      @closed="handleClose"
+    >
+      <view class="nut-menu-item__content">
+        <view
+          v-for="(option, index) in options"
+          :key="index"
+          class="nut-menu-item__option"
+          :class="{ active: option.value === modelValue }"
+          :style="{ 'flex-basis': 100 / cols + '%' }"
+          @click="onClick(option)"
+        >
+          <nut-icon v-if="option.value === modelValue" name="Check" :color="parent.props.activeColor"></nut-icon>
+          <view :style="{ color: option.value === modelValue ? parent.props.activeColor : '' }">{{ option.text }}</view>
+        </view>
+        <slot></slot>
+      </view>
+    </nut-popup>
+  </view>
 </template>
 <script lang="ts">
-import { getCurrentInstance } from 'vue';
+import { reactive, PropType, inject, getCurrentInstance, computed } from 'vue';
 import { createComponent } from '../../utils/create';
-const { create, componentName } = createComponent('menu-item');
+const { componentName, create } = createComponent('menu-item');
+import Icon from '../icon/index.taro.vue';
+import Popup from '../popup/index.taro.vue';
+
+type MenuItemOption = {
+  text: string;
+  value: number | string;
+};
+
 export default create({
-  setup() {
-    let pro = (getCurrentInstance() as any).proxy;
-    pro.toggle = () => {
-      pro.$parent.activeTitle = '';
-      pro.$parent.isShowCustomer = false;
-      pro.$parent.showMask = false;
-      pro.$parent.nutMenuIndex = 'auto';
+  props: {
+    title: String,
+    options: {
+      type: Array as PropType<MenuItemOption[]>,
+      default: []
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    modelValue: null as unknown as PropType<unknown>,
+    cols: {
+      type: Number,
+      default: 1
+    },
+    titleIcon: {
+      type: String,
+      default: 'down-arrow'
+    }
+  },
+  components: {
+    [Icon.name]: Icon,
+    [Popup.name]: Popup
+  },
+  emits: ['update:modelValue', 'change'],
+  setup(props, { emit, slots }) {
+    const state = reactive({
+      showPopup: false,
+      transition: true,
+      showWrapper: false,
+      isShowPlaceholderElement: false
+    });
+
+    const useParent: any = () => {
+      const parent = inject('menuParent', null);
+
+      if (parent) {
+        // 获取子组件自己的实例
+        const instance = getCurrentInstance()!;
+
+        const { link } = parent;
+
+        // @ts-ignore
+        link(instance);
+
+        return {
+          parent
+        };
+      }
+    };
+
+    const { parent } = useParent();
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const toggle = (show = !state.showPopup, options: { immediate?: boolean } = {}) => {
+      if (show === state.showPopup) {
+        return;
+      }
+
+      state.showPopup = show;
+      state.isShowPlaceholderElement = show;
+      // state.transition = !options.immediate;
+
+      if (show) {
+        state.showWrapper = true;
+      }
     };
 
-    return {};
+    const renderTitle = () => {
+      if (props.title) {
+        return props.title;
+      }
+
+      const match: any = props.options?.find((option: any) => option.value === props.modelValue);
+
+      return match ? match.text : '';
+    };
+
+    const onClick = (option: MenuItemOption) => {
+      state.showPopup = false;
+      state.isShowPlaceholderElement = false;
+
+      if (option.value !== props.modelValue) {
+        emit('update:modelValue', option.value);
+        emit('change', option.value);
+      }
+    };
+
+    const handleClose = () => {
+      state.showWrapper = false;
+      state.isShowPlaceholderElement = false;
+    };
+
+    const handleClickOutside = () => {
+      state.showPopup = false;
+    };
+
+    return {
+      classes,
+      renderTitle,
+      state,
+      parent,
+      toggle,
+      onClick,
+      handleClose,
+      handleClickOutside
+    };
   }
 });
 </script>
-
-<style lang="scss" scoped>
-@import './index.scss';
-</style>

+ 158 - 15
src/packages/__VUE/menuitem/index.vue

@@ -1,25 +1,168 @@
 <template>
-  <div><slot></slot></div>
+  <view :class="classes" v-show="state.showWrapper">
+    <div
+      v-show="state.isShowPlaceholderElement"
+      @click="handleClickOutside"
+      class="placeholder-element"
+      :style="{ height: parent.offset.value + 'px' }"
+    >
+    </div>
+    <nut-popup
+      :style="{ top: parent.offset.value + 'px' }"
+      :overlayStyle="{ top: parent.offset.value + 'px' }"
+      v-bind="$attrs"
+      v-model:visible="state.showPopup"
+      position="top"
+      :duration="parent.props.duration"
+      pop-class="nut-menu__pop"
+      overlayClass="nut-menu__overlay"
+      :overlay="parent.props.overlay"
+      @closed="handleClose"
+      :isWrapTeleport="false"
+    >
+      <view class="nut-menu-item__content">
+        <view
+          v-for="(option, index) in options"
+          :key="index"
+          class="nut-menu-item__option"
+          :class="{ active: option.value === modelValue }"
+          :style="{ 'flex-basis': 100 / cols + '%' }"
+          @click="onClick(option)"
+        >
+          <nut-icon v-if="option.value === modelValue" name="Check" :color="parent.props.activeColor"></nut-icon>
+          <view :style="{ color: option.value === modelValue ? parent.props.activeColor : '' }">{{ option.text }}</view>
+        </view>
+        <slot></slot>
+      </view>
+    </nut-popup>
+  </view>
 </template>
 <script lang="ts">
-import { getCurrentInstance } from 'vue';
+import { reactive, PropType, inject, getCurrentInstance, computed } from 'vue';
 import { createComponent } from '../../utils/create';
-const { create, componentName } = createComponent('menu-item');
+const { componentName, create } = createComponent('menu-item');
+import Icon from '../icon/index.vue';
+import Popup from '../popup/index.vue';
+
+type MenuItemOption = {
+  text: string;
+  value: number | string;
+};
+
 export default create({
-  setup() {
-    let pro = (getCurrentInstance() as any).proxy;
-    pro.toggle = () => {
-      pro.$parent.activeTitle = '';
-      pro.$parent.isShowCustomer = false;
-      pro.$parent.showMask = false;
-      pro.$parent.nutMenuIndex = 'auto';
+  props: {
+    title: String,
+    options: {
+      type: Array as PropType<MenuItemOption[]>,
+      default: []
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    modelValue: null as unknown as PropType<unknown>,
+    cols: {
+      type: Number,
+      default: 1
+    },
+    titleIcon: {
+      type: String,
+      default: 'down-arrow'
+    }
+  },
+  components: {
+    [Icon.name]: Icon,
+    [Popup.name]: Popup
+  },
+  emits: ['update:modelValue', 'change'],
+  setup(props, { emit, slots }) {
+    const state = reactive({
+      showPopup: false,
+      transition: true,
+      showWrapper: false,
+      isShowPlaceholderElement: false
+    });
+
+    const useParent: any = () => {
+      const parent = inject('menuParent', null);
+
+      if (parent) {
+        // 获取子组件自己的实例
+        const instance = getCurrentInstance()!;
+
+        const { link } = parent;
+
+        // @ts-ignore
+        link(instance);
+
+        return {
+          parent
+        };
+      }
+    };
+
+    const { parent } = useParent();
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const toggle = (show = !state.showPopup, options: { immediate?: boolean } = {}) => {
+      if (show === state.showPopup) {
+        return;
+      }
+
+      state.showPopup = show;
+      state.isShowPlaceholderElement = show;
+      // state.transition = !options.immediate;
+
+      if (show) {
+        state.showWrapper = true;
+      }
     };
 
-    return {};
+    const renderTitle = () => {
+      if (props.title) {
+        return props.title;
+      }
+
+      const match: any = props.options?.find((option: any) => option.value === props.modelValue);
+
+      return match ? match.text : '';
+    };
+
+    const onClick = (option: MenuItemOption) => {
+      state.showPopup = false;
+      state.isShowPlaceholderElement = false;
+
+      if (option.value !== props.modelValue) {
+        emit('update:modelValue', option.value);
+        emit('change', option.value);
+      }
+    };
+
+    const handleClose = () => {
+      state.showWrapper = false;
+      state.isShowPlaceholderElement = false;
+    };
+
+    const handleClickOutside = () => {
+      state.showPopup = false;
+    };
+
+    return {
+      classes,
+      renderTitle,
+      state,
+      parent,
+      toggle,
+      onClick,
+      handleClose,
+      handleClickOutside
+    };
   }
 });
 </script>
-
-<style lang="scss" scoped>
-@import './index.scss';
-</style>

+ 36 - 25
src/packages/__VUE/popup/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <Teleport :to="teleport">
+  <Teleport :to="teleport" v-if="isWrapTeleport">
     <nut-overlay
       v-if="overlay"
       :visible="visible"
@@ -11,17 +11,8 @@
       :duration="duration"
       @click="onClickOverlay"
     />
-    <Transition
-      :name="transitionName"
-      @after-enter="onOpened"
-      @after-leave="onClosed"
-    >
-      <view
-        v-show="visible"
-        :class="classes"
-        :style="popStyle"
-        @click="onClick"
-      >
+    <Transition :name="transitionName" @after-enter="onOpened" @after-leave="onClosed">
+      <view v-show="visible" :class="classes" :style="popStyle" @click="onClick">
         <slot v-if="showSlot"></slot>
         <view
           v-if="closeable"
@@ -34,6 +25,32 @@
       </view>
     </Transition>
   </Teleport>
+  <view v-else>
+    <nut-overlay
+      v-if="overlay"
+      :visible="visible"
+      :close-on-click-overlay="closeOnClickOverlay"
+      :class="overlayClass"
+      :style="overlayStyle"
+      :z-index="zIndex"
+      :lock-scroll="lockScroll"
+      :duration="duration"
+      @click="onClickOverlay"
+    />
+    <Transition :name="transitionName" @after-enter="onOpened" @after-leave="onClosed">
+      <view v-show="visible" :class="classes" :style="popStyle" @click="onClick">
+        <slot v-if="showSlot"></slot>
+        <view
+          v-if="closeable"
+          @click="onClickCloseIcon"
+          class="nutui-popup__close-icon"
+          :class="'nutui-popup__close-icon--' + closeIconPosition"
+        >
+          <nut-icon :name="closeIcon" size="12px" />
+        </view>
+      </view>
+    </Transition>
+  </view>
 </template>
 <script lang="ts">
 import {
@@ -109,6 +126,11 @@ export const popupProps = {
   round: {
     type: Boolean,
     default: false
+  },
+
+  isWrapTeleport: {
+    type: Boolean,
+    default: true
   }
 };
 export default create({
@@ -119,16 +141,7 @@ export default create({
   props: {
     ...popupProps
   },
-  emits: [
-    'click',
-    'click-close-icon',
-    'open',
-    'close',
-    'opend',
-    'closed',
-    'update:visible',
-    'click-overlay'
-  ],
+  emits: ['click', 'click-close-icon', 'open', 'close', 'opend', 'closed', 'update:visible', 'click-overlay'],
 
   setup(props, { emit }) {
     const state = reactive({
@@ -257,9 +270,7 @@ export default create({
     watch(
       () => props.position,
       (value) => {
-        value === 'center'
-          ? (state.transitionName = 'popup-fade')
-          : (state.transitionName = `popup-slide-${value}`);
+        value === 'center' ? (state.transitionName = 'popup-fade') : (state.transitionName = `popup-slide-${value}`);
       }
     );
 

+ 13 - 0
src/packages/styles/variables.scss

@@ -360,6 +360,19 @@ $tabs-vertical-titles-item-height: 40px !default;
 $tabs-vertical-titles-item-active-line-height: 14px !default;
 $tabs-vertical-titles-width: 100px !default;
 
+// menu
+$nut-menu-bar-line-height: 46px !default;
+$nut-menu-bar-border-bottom-color: #eaf0fb !default;
+$nut-menu-bar-opened-z-index: 2001 !default;
+$nut-menu-item-disabled-color: #969799 !default;
+$nut-menu-title-text-padding-left: 8px !default;
+$nut-menu-title-text-padding-right: 8px !default;
+$nut-menu-item-content-padding: 12px 24px !default;
+$nut-menu-item-content-max-height: 214px !default;
+$nut-menu-item-option-padding-top: 12px !default;
+$nut-menu-item-option-padding-bottom: 12px !default;
+$nut-menu-item-option-i-margin-right: 6px !default;
+
 // searchbar
 $searchbar-background: $white !default;
 $searchbar-input-background: #f7f7f7 !default;

+ 63 - 55
src/sites/mobile-taro/vue/src/nav/pages/menu/index.vue

@@ -2,84 +2,92 @@
   <div class="demo">
     <h2>基础用法</h2>
     <nut-menu>
-      <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
+      <nut-menu-item v-model="state.value1" :options="state.options1" />
+      <nut-menu-item v-model="state.value2" @change="handleChange" :options="state.options2" />
     </nut-menu>
-    <h2>两列标题</h2>
+    <h2>自定义菜单内容</h2>
     <nut-menu>
-      <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
-      <nut-menu-item title="默认排序" :options="options2"></nut-menu-item>
-    </nut-menu>
-    <h2>获取选择的列表对象</h2>
-    <nut-menu @choose="handleChoose">
-      <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
-    </nut-menu>
-    <h2>一行两列列表对象</h2>
-    <nut-menu col="2">
-      <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
+      <nut-menu-item v-model="state.value1" :options="state.options1" />
+      <nut-menu-item title="筛选" ref="item">
+        <div :style="{ display: 'flex', flex: 1, 'justify-content': 'space-between', 'align-items': 'center' }">
+          <div>自定义内容</div>
+          <nut-button @click="onConfirm">关闭</nut-button>
+        </div>
+      </nut-menu-item>
     </nut-menu>
-    <h2>禁用菜单</h2>
+    <h2>一行两列</h2>
     <nut-menu>
-      <nut-menu-item
-        title="全部商品"
-        disabled="true"
-        :options="options1"
-      ></nut-menu-item>
+      <nut-menu-item v-model="state.value3" :cols="2" :options="state.options3" />
     </nut-menu>
-    <h2>自定义选项的选中态图标颜色</h2>
-    <nut-menu active-color="#0f0">
-      <nut-menu-item title="全部商品" :options="options1"></nut-menu-item>
+    <h2>自定义选中态颜色</h2>
+    <nut-menu active-color="green">
+      <nut-menu-item v-model="state.value1" :options="state.options1" />
+      <nut-menu-item v-model="state.value2" @change="handleChange" :options="state.options2" />
     </nut-menu>
-    <h2>自定义菜单内容</h2>
+    <h2>禁用菜单</h2>
     <nut-menu>
-      <nut-menu-item title="筛选" ref="item">
-        <div
-          :style="{
-            display: 'flex',
-            'justify-content': 'space-between',
-            'align-items': 'center'
-          }"
-        >
-          <div :style="{ 'font-size': '12px' }">我是自定义内容</div>
-          <nut-button @click="handleClick">关闭</nut-button>
-        </div>
-      </nut-menu-item>
+      <nut-menu-item disabled v-model="state.value1" :options="state.options1" />
+      <nut-menu-item disabled v-model="state.value2" @change="handleChange" :options="state.options2" />
     </nut-menu>
   </div>
 </template>
 
-<script lang="ts">
+<script>
 import { ref, reactive, toRefs } from 'vue';
 export default {
   props: {},
   setup() {
-    const options1 = [
-      { text: '全部商品', value: 0 },
-      { text: '新款商品', value: 1 },
-      { text: '活动商品', value: 2 }
-    ];
-
-    const options2 = [
-      { text: '默认排序', value: 'a' },
-      { text: '好评排序', value: 'b' },
-      { text: '销量排序', value: 'c' }
-    ];
+    const state = reactive({
+      options1: [
+        { text: '全部商品', value: 0 },
+        { text: '新款商品', value: 1 },
+        { text: '活动商品', value: 2 }
+      ],
+      options2: [
+        { text: '默认排序', value: 'a' },
+        { text: '好评排序', value: 'b' },
+        { text: '销量排序', value: 'c' }
+      ],
+      options3: [
+        { text: '全部商品', value: 0 },
+        { text: '家庭清洁/纸品', value: 1 },
+        { text: '个人护理', value: 2 },
+        { text: '美妆护肤', value: 3 },
+        { text: '食品饮料', value: 4 },
+        { text: '家用电器', value: 5 },
+        { text: '母婴', value: 6 },
+        { text: '数码', value: 7 },
+        { text: '电脑、办公', value: 8 },
+        { text: '运动户外', value: 9 },
+        { text: '厨具', value: 10 },
+        { text: '医疗保健', value: 11 },
+        { text: '酒类', value: 12 },
+        { text: '生鲜', value: 13 },
+        { text: '家具', value: 14 },
+        { text: '传统滋补', value: 15 },
+        { text: '汽车用品', value: 16 },
+        { text: '家居日用', value: 17 }
+      ],
+      value1: 0,
+      value2: 'a',
+      value3: 0
+    });
 
-    const item = ref<HTMLElement>();
+    const item = ref('');
 
-    const handleChoose = (val: string, index: string | number) => {
-      console.log(val, index);
+    const onConfirm = () => {
+      item.value.toggle();
     };
 
-    const handleClick = () => {
-      (item.value as any).toggle();
+    const handleChange = (val) => {
+      console.log('val', val);
     };
 
     return {
-      options1,
-      options2,
+      state,
       item,
-      handleChoose,
-      handleClick
+      onConfirm,
+      handleChange
     };
   }
 };