浏览代码

feat(menu): 增加向上弹出和自定义选项图标 (#1346)

* feat: add vertical divider

* fix: fix fixed menu style

* feat: internationalize Menu、Pagination、Divider and ImagePreview

* feat: 菜单组件增加点击遮照关闭

* feat: 增加props的版本号;增加菜单组件中data的国际化

* feat: add four variables for Divider in jdt

* fix: 修复两处单元测试错误

* feat: 菜单组件增加向上弹出和自定义选项图标

* feat: 表单组件新增 title-class 和 lock-scroll 属性
yangjinjun3 3 年之前
父节点
当前提交
93fcad2fee

+ 7 - 5
src/packages/__VUE/divider/demo.vue

@@ -14,12 +14,12 @@
       translate('text')
     }}</nut-divider>
     <h2>{{ translate('verticalDivider') }}</h2>
-    <div :style="{ fontSize: '14px' }">
+    <div :style="{ fontSize: '14px', marginLeft: '27px', color: '#909ca4' }">
       {{ translate('text') }}
       <nut-divider direction="vertical" />
-      <a href="#">{{ translate('text') }}</a>
+      <a href="#" :style="{ color: '#1989fa' }">{{ translate('link') }}</a>
       <nut-divider direction="vertical" />
-      <a href="#">{{ translate('text') }}</a>
+      <a href="#" :style="{ color: '#1989fa' }">{{ translate('link') }}</a>
     </div>
   </div>
 </template>
@@ -36,7 +36,8 @@ useTranslate({
     dashed: '虚线',
     customStyle: '自定义样式',
     verticalDivider: '垂直分割线',
-    text: '文本'
+    text: '文本',
+    link: '链接'
   },
   'en-US': {
     basic: 'Basic Usage',
@@ -45,7 +46,8 @@ useTranslate({
     dashed: 'Dashed',
     customStyle: 'Custom Style',
     verticalDivider: 'Vertical Divider',
-    text: 'text'
+    text: 'Text',
+    link: 'Link'
   }
 });
 export default createDemo({

+ 3 - 3
src/packages/__VUE/divider/doc.en-US.md

@@ -109,11 +109,11 @@ User can custom divider style with style attribute.
 <template>
     <nut-cell>
         <div :style="{fontSize: '14px'}">
-            text
+            Text
             <nut-divider direction="vertical" />
-            <a href="#">text</a>
+            <a href="#" :style="{ color: '#1989fa' }">Link</a>
             <nut-divider direction="vertical" />
-            <a href="#">text</a>
+            <a href="#" :style="{ color: '#1989fa' }">Link</a>
         </div>
     </nut-cell>
 </template>

+ 2 - 2
src/packages/__VUE/divider/doc.md

@@ -112,9 +112,9 @@ app.use(Divider);
         <div :style="{fontSize: '14px'}">
             文本
             <nut-divider direction="vertical" />
-            <a href="#">文本</a>
+            <a href="#" :style="{ color: '#1989fa' }">链接</a>
             <nut-divider direction="vertical" />
-            <a href="#">文本</a>
+            <a href="#" :style="{ color: '#1989fa' }">链接</a>
         </div>
     </nut-cell>
 </template>

+ 79 - 4
src/packages/__VUE/menu/__tests__/menu.spec.ts

@@ -2,6 +2,7 @@ import { config, mount } from '@vue/test-utils';
 import Menu from '../index.vue';
 import MenuItem from '../../menuitem/index.vue';
 import NutIcon from '../../icon/index.vue';
+import { mockScrollTop } from './../../../utils/unit';
 import { h, nextTick } from 'vue';
 
 const options1 = [
@@ -125,11 +126,11 @@ test('menu item title props: nut-menu__title-text html should contain custom tit
   expect(wrapper.find('.nut-menu__title-text').html()).toContain('custom title');
 });
 
-test('menu item title icon props: nut-menu__title-text html should contain custom title', async () => {
+test('menu item title icon props: nut-menu__title i classes should contain nut-icon-joy-smile', async () => {
   const wrapper = mount(Menu, {
     slots: {
       default: h(MenuItem, {
-        titleIcon: 'plus',
+        titleIcon: 'joy-smile',
         modelValue: 0,
         options: options1
       })
@@ -137,8 +138,44 @@ test('menu item title icon props: nut-menu__title-text html should contain custo
   });
   await nextTick();
 
-  const barItem: any = wrapper.find('.nut-menu__item .nut-menu__title i');
-  expect(barItem.classes()).toContain('nut-icon-plus');
+  const titleIcon: any = wrapper.find('.nut-menu__item .nut-menu__title i');
+  expect(titleIcon.classes()).toContain('nut-icon-joy-smile');
+});
+
+test('menu item option icon props: nut-menu-item__option i classes should contain nut-icon-checklist', async () => {
+  const wrapper = mount(Menu, {
+    slots: {
+      default: h(MenuItem, {
+        optionIcon: 'checklist',
+        modelValue: 0,
+        options: options1
+      })
+    }
+  });
+
+  await nextTick();
+
+  const optionIcon: any = wrapper.find<HTMLElement>('.nut-menu-item__option i');
+  expect(optionIcon.classes()).toContain('nut-icon-checklist');
+});
+
+test('menu direction props: nut-menu__title i classes should contain nut-icon-arrow-up', async () => {
+  const wrapper = mount(Menu, {
+    props: {
+      direction: 'up'
+    },
+    slots: {
+      default: h(MenuItem, {
+        modelValue: 0,
+        options: options1
+      })
+    }
+  });
+
+  await nextTick();
+
+  const titleIcon: any = wrapper.find<HTMLElement>('.nut-menu__title i');
+  expect(titleIcon.classes()).toContain('nut-icon-arrow-up');
 });
 
 test('active color props: i in active nut-menu-item__option color and active nut-menu__item color should be both green', async () => {
@@ -215,3 +252,41 @@ test('menu close-on-click-overlay props: ', async () => {
 
   expect(wrapper.find<HTMLElement>('.nut-overlay').element.style.display).toEqual('none');
 });
+
+test('menu scroll-fixed props: nut-menu classes should contain scroll-fixed', async () => {
+  const wrapper = mount(Menu, {
+    props: {
+      scrollFixed: 50
+    },
+    slots: {
+      default: h(MenuItem, {
+        modelValue: 0,
+        options: options1
+      })
+    }
+  });
+
+  await mockScrollTop(100);
+
+  expect(wrapper.find('.nut-menu').classes()).toContain('scroll-fixed');
+});
+
+test('menu title-class props: nut-menu__title classes should contain custom-title-class', async () => {
+  const wrapper = mount(Menu, {
+    props: {
+      titleClass: 'custom-title-class'
+    },
+    slots: {
+      default: h(MenuItem, {
+        modelValue: 0,
+        options: options1
+      })
+    }
+  });
+
+  await nextTick();
+
+  const menuTitle: any = wrapper.find<HTMLElement>('.nut-menu__title');
+
+  expect(menuTitle.classes()).toContain('custom-title-class');
+});

+ 30 - 16
src/packages/__VUE/menu/demo.vue

@@ -24,6 +24,16 @@
       <nut-menu-item v-model="state.value1" :options="options1" />
       <nut-menu-item v-model="state.value2" @change="handleChange" :options="options2" />
     </nut-menu>
+    <h2>{{ translate('customIcons') }}</h2>
+    <nut-menu>
+      <nut-menu-item v-model="state.value1" :options="options1" titleIcon="joy-smile" />
+      <nut-menu-item v-model="state.value2" @change="handleChange" :options="options2" optionIcon="checklist" />
+    </nut-menu>
+    <h2>{{ translate('expandDirection') }}</h2>
+    <nut-menu direction="up">
+      <nut-menu-item v-model="state.value1" :options="options1" />
+      <nut-menu-item v-model="state.value2" @change="handleChange" :options="options2" />
+    </nut-menu>
     <h2>{{ translate('disableMenu') }}</h2>
     <nut-menu>
       <nut-menu-item disabled v-model="state.value1" :options="options1" />
@@ -46,6 +56,8 @@ useTranslate({
     confirm: '确认',
     twoColsInOneLine: '一行两列',
     customActiveColor: '自定义选中态颜色',
+    customIcons: '自定义图标',
+    expandDirection: '向上展开',
     disableMenu: '禁用菜单',
     allProducts: '全部商品',
     newProducts: '新款商品',
@@ -79,6 +91,8 @@ useTranslate({
     confirm: 'Confirm',
     twoColsInOneLine: 'Two Cols In One Line',
     customActiveColor: 'Custom Active Color',
+    customIcons: 'Custom Icons',
+    expandDirection: 'Expand Direction',
     disableMenu: 'Disable Menu',
     allProducts: 'All Products',
     newProducts: 'New Products',
@@ -87,22 +101,22 @@ useTranslate({
     praiseSort: 'Praise Sort',
     salesVolumeSort: 'Sales Volume Sort',
     product1: 'Product1',
-    product2: 'product2',
-    product3: 'product3',
-    product4: 'product4',
-    product5: 'product5',
-    product6: 'product6',
-    product7: 'product7',
-    product8: 'product8',
-    product9: 'product9',
-    product10: 'product10',
-    product11: 'product11',
-    product12: 'product12',
-    product13: 'product13',
-    product14: 'product14',
-    product15: 'product15',
-    product16: 'Product1',
-    product17: 'Product1'
+    product2: 'Product2',
+    product3: 'Product3',
+    product4: 'Product4',
+    product5: 'Product5',
+    product6: 'Product6',
+    product7: 'Product7',
+    product8: 'Product8',
+    product9: 'Product9',
+    product10: 'Product10',
+    product11: 'Product11',
+    product12: 'Product12',
+    product13: 'Product13',
+    product14: 'Product14',
+    product15: 'Product15',
+    product16: 'Product16',
+    product17: 'Product17'
   }
 });
 export default createDemo({

+ 157 - 58
src/packages/__VUE/menu/doc.en-US.md

@@ -36,14 +36,14 @@ export default {
   setup() {
     const state = reactive({
       options1: [
-        { text: 'Option1', value: 0 },
-        { text: 'Option2', value: 1 },
-        { text: 'Option3', value: 2 }
+        { text: 'All Products', value: 0 },
+        { text: 'New Products', value: 1 },
+        { text: 'Activity Products', value: 2 }
       ],
       options2: [
-        { text: 'Option A', value: 'a' },
-        { text: 'Option B', value: 'b' },
-        { text: 'Option C', value: 'c' },
+        { text: 'Default Sort', value: 'a' },
+        { text: 'Praise Sort', value: 'b' },
+        { text: 'Sales Volume Sort', value: 'c' }
       ],
       value1: 0,
       value2: 'a'
@@ -89,9 +89,9 @@ export default {
   setup() {
     const state = reactive({
       options1: [
-        { text: 'Option1', value: 0 },
-        { text: 'Option2', value: 1 },
-        { text: 'Option3', value: 2 }
+        { text: 'All Products', value: 0 },
+        { text: 'New Products', value: 1 },
+        { text: 'Activity Products', value: 2 }
       ],
       value1: 0
     });
@@ -132,24 +132,24 @@ export default {
   setup() {
     const state = reactive({
       options3: [
-        { text: 'Option1', value: 0 },
-        { text: 'Option2', value: 1 },
-        { text: 'Option3', value: 2 },
-        { text: 'Option4', value: 3 },
-        { text: 'Option5', value: 4 },
-        { text: 'Option6', value: 5 },
-        { text: 'Option7', value: 6 },
-        { text: 'Option8', value: 7 },
-        { text: 'Option9', value: 8 },
-        { text: 'Option10', value: 9 },
-        { text: 'Option11', value: 10 },
-        { text: 'Option12', value: 11 },
-        { text: 'Option13', value: 12 },
-        { text: 'Option14', value: 13 },
-        { text: 'Option15', value: 14 },
-        { text: 'Option16', value: 15 },
-        { text: 'Option17', value: 16 },
-        { text: 'Option18', value: 17 },
+        { text: 'All Products', value: 0 },
+        { text: 'Product1', value: 1 },
+        { text: 'Product2', value: 2 },
+        { text: 'Product3', value: 3 },
+        { text: 'Product4', value: 4 },
+        { text: 'Product5', value: 5 },
+        { text: 'Product6', value: 6 },
+        { text: 'Product7', value: 7 },
+        { text: 'Product8', value: 8 },
+        { text: 'Product9', value: 9 },
+        { text: 'Product10', value: 10 },
+        { text: 'Product11', value: 11 },
+        { text: 'Product12', value: 12 },
+        { text: 'Product13', value: 13 },
+        { text: 'Product14', value: 14 },
+        { text: 'Product15', value: 15 },
+        { text: 'Product16', value: 16 },
+        { text: 'Product17', value: 17 }
       ],
       value3: 0
     });
@@ -183,14 +183,108 @@ export default {
   setup() {
     const state = reactive({
       options1: [
-        { text: 'Option1', value: 0 },
-        { text: 'Option2', value: 1 },
-        { text: 'Option3', value: 2 }
+        { text: 'All Products', value: 0 },
+        { text: 'New Products', value: 1 },
+        { text: 'Activity Products', value: 2 }
       ],
       options2: [
-        { text: 'Option A', value: 'a' },
-        { text: 'Option B', value: 'b' },
-        { text: 'Option C', value: 'c' },
+        { text: 'Default Sort', value: 'a' },
+        { text: 'Praise Sort', value: 'b' },
+        { text: 'Sales Volume Sort', value: 'c' }
+      ],
+      value1: 0,
+      value2: 'a'
+    });
+
+    const handleChange = val => {
+      console.log('val', val);
+    }
+
+    return {
+      state,
+      handleChange
+    };
+  }
+}
+</script>
+```
+
+:::
+
+### Custom Icons
+
+:::demo
+
+```html
+<template>
+  <nut-menu>
+    <nut-menu-item v-model="state.value1" :options="state.options1" titleIcon="joy-smile" />
+    <nut-menu-item v-model="state.value2" @change="handleChange" :options="state.options2" optionIcon="checklist" />
+  </nut-menu>
+</template>
+
+<script>
+import { reactive, ref } from 'vue';
+
+export default {
+  setup() {
+    const state = reactive({
+      options1: [
+        { text: 'All Products', value: 0 },
+        { text: 'New Products', value: 1 },
+        { text: 'Activity Products', value: 2 }
+      ],
+      options2: [
+        { text: 'Default Sort', value: 'a' },
+        { text: 'Praise Sort', value: 'b' },
+        { text: 'Sales Volume Sort', value: 'c' }
+      ],
+      value1: 0,
+      value2: 'a'
+    });
+
+    const handleChange = val => {
+      console.log('val', val);
+    }
+
+    return {
+      state,
+      handleChange
+    };
+  }
+}
+</script>
+```
+
+:::
+
+### Expand Directions
+
+:::demo
+
+```html
+<template>
+  <nut-menu>
+    <nut-menu-item v-model="state.value1" :options="state.options1" />
+    <nut-menu-item v-model="state.value2" @change="handleChange" :options="state.options2" />
+  </nut-menu>
+</template>
+
+<script>
+import { reactive, ref } from 'vue';
+
+export default {
+  setup() {
+    const state = reactive({
+      options1: [
+        { text: 'All Products', value: 0 },
+        { text: 'New Products', value: 1 },
+        { text: 'Activity Products', value: 2 }
+      ],
+      options2: [
+        { text: 'Default Sort', value: 'a' },
+        { text: 'Praise Sort', value: 'b' },
+        { text: 'Sales Volume Sort', value: 'c' }
       ],
       value1: 0,
       value2: 'a'
@@ -230,34 +324,34 @@ export default {
   setup() {
     const state = reactive({
       options1: [
-        { text: 'Option1', value: 0 },
-        { text: 'Option2', value: 1 },
-        { text: 'Option3', value: 2 }
+        { text: 'All Products', value: 0 },
+        { text: 'New Products', value: 1 },
+        { text: 'Activity Products', value: 2 }
       ],
       options2: [
-        { text: 'Option A', value: 'a' },
-        { text: 'Option B', value: 'b' },
-        { text: 'Option C', value: 'c' },
+        { text: 'Default Sort', value: 'a' },
+        { text: 'Praise Sort', value: 'b' },
+        { text: 'Sales Volume Sort', value: 'c' }
       ],
       options3: [
-        { text: 'Option1', value: 0 },
-        { text: 'Option2', value: 1 },
-        { text: 'Option3', value: 2 },
-        { text: 'Option4', value: 3 },
-        { text: 'Option5', value: 4 },
-        { text: 'Option6', value: 5 },
-        { text: 'Option7', value: 6 },
-        { text: 'Option8', value: 7 },
-        { text: 'Option9', value: 8 },
-        { text: 'Option10', value: 9 },
-        { text: 'Option11', value: 10 },
-        { text: 'Option12', value: 11 },
-        { text: 'Option13', value: 12 },
-        { text: 'Option14', value: 13 },
-        { text: 'Option15', value: 14 },
-        { text: 'Option16', value: 15 },
-        { text: 'Option17', value: 16 },
-        { text: 'Option18', value: 17 },
+        { text: 'All Products', value: 0 },
+        { text: 'Product1', value: 1 },
+        { text: 'Product2', value: 2 },
+        { text: 'Product3', value: 3 },
+        { text: 'Product4', value: 4 },
+        { text: 'Product5', value: 5 },
+        { text: 'Product6', value: 6 },
+        { text: 'Product7', value: 7 },
+        { text: 'Product8', value: 8 },
+        { text: 'Product9', value: 9 },
+        { text: 'Product10', value: 10 },
+        { text: 'Product11', value: 11 },
+        { text: 'Product12', value: 12 },
+        { text: 'Product13', value: 13 },
+        { text: 'Product14', value: 14 },
+        { text: 'Product15', value: 15 },
+        { text: 'Product16', value: 16 },
+        { text: 'Product17', value: 17 }
       ],
       value1: 0,
       value2: 'a',
@@ -295,6 +389,9 @@ export default {
 |--------------|----------------------------------|--------|------------------|
 | active-color         | Active color of title and option     | String | #F2270C               |
 | close-on-click-overlay `v3.1.21`        | Whether to close when overlay is clicked     | Boolean | true               |
+| scroll-fixed `v3.1.22`        | Whether to fixed when window is scrolled, fixed position can be set     | Boolean、String、Number | false               |
+| title-class `v3.1.22`        | Custome title class     | String | -               |
+| lock-scroll `v3.1.22`        | Whether the background is locked     | Boolean | true               |
 
 ### MenuItem Props
 
@@ -304,7 +401,9 @@ export default {
 | options         | Options     | Array | -                |
 | disabled         | Whether to disable dropdown item     | Boolean | false                |
 | cols         | Display how many options in one line     | Number | 1                |
-| title-icon         | Custome title icon     | String | 'down-arrow'                |
+| title-icon         | Custome title icon     | String | -                |
+| option-icon `v3.1.22`         | Custome option icon     | String | 'Check'                |
+| direction `v3.1.22`         | Expand direction, can be set to up     | String | 'down'                |
 
 ### MenuItem Events
 

+ 100 - 1
src/packages/__VUE/menu/doc.md

@@ -213,6 +213,100 @@ export default {
 
 :::
 
+### 自定义图标
+
+:::demo
+
+```html
+<template>
+  <nut-menu>
+    <nut-menu-item v-model="state.value1" :options="state.options1" titleIcon="joy-smile" />
+    <nut-menu-item v-model="state.value2" @change="handleChange" :options="state.options2" optionIcon="checklist" />
+  </nut-menu>
+</template>
+
+<script>
+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' },
+      ],
+      value1: 0,
+      value2: 'a'
+    });
+
+    const handleChange = val => {
+      console.log('val', val);
+    }
+
+    return {
+      state,
+      handleChange
+    };
+  }
+}
+</script>
+```
+
+:::
+
+### 向上展开
+
+:::demo
+
+```html
+<template>
+  <nut-menu>
+    <nut-menu-item v-model="state.value1" :options="state.options1" />
+    <nut-menu-item v-model="state.value2" @change="handleChange" :options="state.options2" />
+  </nut-menu>
+</template>
+
+<script>
+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' },
+      ],
+      value1: 0,
+      value2: 'a'
+    });
+
+    const handleChange = val => {
+      console.log('val', val);
+    }
+
+    return {
+      state,
+      handleChange
+    };
+  }
+}
+</script>
+```
+
+:::
+
 ### 禁用菜单
 
 :::demo
@@ -297,6 +391,9 @@ export default {
 |--------------|----------------------------------|--------|------------------|
 | active-color         | 选项的选中态图标颜色     | String | #F2270C               |
 | close-on-click-overlay `v3.1.21`        | 是否在点击遮罩层后关闭菜单     | Boolean | true               |
+| scroll-fixed `v3.1.22`        | 滚动后是否固定,可设置固定位置     | Boolean、String、Number | false               |
+| title-class `v3.1.22`        | 自定义标题样式类     | String | -               |
+| lock-scroll `v3.1.22`        | 背景是否锁定     | Boolean | true               |
 
 ### MenuItem Props
 
@@ -306,7 +403,9 @@ export default {
 | options         | 选项数组     | Array | -                |
 | disabled         | 是否禁用菜单     | Boolean | false                |
 | cols         | 可以设置一行展示多少列 options     | Number | 1                |
-| title-icon         | 自定义标题图标     | String | 'down-arrow'                |
+| title-icon         | 自定义标题图标     | String | -                |
+| option-icon `v3.1.22`         | 自定义选项图标     | String | 'Check'                |
+| direction  `v3.1.22`        | 菜单展开方向,可选值为up     | String | 'down'                |
 
 ### MenuItem Events
 

+ 45 - 36
src/packages/__VUE/menu/index.scss

@@ -1,48 +1,57 @@
-.nut-menu__bar {
-  position: relative;
-  display: flex;
-  line-height: $menu-bar-line-height;
-  background-color: $white;
-  box-shadow: $menu-bar-box-shadow;
-
-  &.opened {
-    z-index: $menu-bar-opened-z-index;
+.nut-menu {
+  &.scroll-fixed {
+    position: fixed;
+    top: $nut-menu-scroll-fixed-top;
+    z-index: $nut-menu-scroll-fixed-z-index;
+    width: 100%;
   }
 
-  .nut-menu__item {
-    flex: 1;
-    text-align: center;
-    font-size: $menu-item-font-size;
-    color: $menu-item-text-color;
-    min-width: 0;
+  .nut-menu__bar {
+    position: relative;
+    display: flex;
+    line-height: $menu-bar-line-height;
+    background-color: $white;
+    box-shadow: $menu-bar-box-shadow;
 
-    &.active {
-      color: $menu-item-active-text-color;
+    &.opened {
+      z-index: $menu-bar-opened-z-index;
     }
 
-    &.disabled {
-      color: $menu-item-disabled-color;
-    }
+    .nut-menu__item {
+      flex: 1;
+      text-align: center;
+      font-size: $menu-item-font-size;
+      color: $menu-item-text-color;
+      min-width: 0;
 
-    .nut-menu__title-icon {
-      transition: all 0.2s linear;
-    }
+      &.active {
+        color: $menu-item-active-text-color;
+      }
+
+      &.disabled {
+        color: $menu-item-disabled-color;
+      }
 
-    .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: $menu-title-text-padding-left;
-        padding-right: $menu-title-text-padding-right;
+      .nut-menu__title-icon {
+        transition: all 0.2s linear;
       }
 
-      &.active .nut-menu__title-icon {
-        transform: rotate(180deg);
+      .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: $menu-title-text-padding-left;
+          padding-right: $menu-title-text-padding-right;
+        }
+
+        &.active .nut-menu__title-icon {
+          transform: rotate(180deg);
+        }
       }
     }
   }

+ 68 - 9
src/packages/__VUE/menu/index.taro.vue

@@ -8,9 +8,13 @@
           :class="{ disabled: item.disabled, active: item.state.showPopup }"
           :style="{ color: item.state.showPopup ? activeColor : '' }"
         >
-          <view class="nut-menu__title" :class="{ active: item.state.showPopup }">
+          <view class="nut-menu__title" :class="getClasses(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>
+            <nut-icon
+              :name="item.titleIcon || (direction === 'up' ? 'arrow-up' : 'down-arrow')"
+              size="10"
+              class="nut-menu__title-icon"
+            ></nut-icon>
           </view>
         </view>
       </template>
@@ -19,9 +23,9 @@
   </view>
 </template>
 <script lang="ts">
-import { reactive, provide, computed, ref, Ref, unref } from 'vue';
+import { reactive, provide, computed, ref } from 'vue';
 import { createComponent } from '@/packages/utils/create';
-import Taro from '@tarojs/taro';
+import Taro, { usePageScroll } from '@tarojs/taro';
 const { componentName, create } = createComponent('menu');
 export default create({
   props: {
@@ -33,14 +37,32 @@ export default create({
       type: Boolean,
       default: true as const
     },
+    lockScroll: {
+      type: Boolean,
+      default: true as const
+    },
     duration: {
       type: [Number, String],
       default: 0.3
-    }
+    },
+    closeOnClickOverlay: {
+      type: Boolean,
+      default: true
+    },
+    direction: {
+      type: String,
+      default: 'down'
+    },
+    scrollFixed: {
+      type: [Boolean, String, Number],
+      default: false
+    },
+    titleClass: [String]
   },
-  setup(props, { emit, slots }) {
+  setup(props) {
     const barRef = ref<HTMLElement>();
     const offset = ref(0);
+    const isScrollFixed = ref(false);
 
     const useChildren = () => {
       const publicChildren: any[] = reactive([]);
@@ -80,7 +102,8 @@ export default create({
     const classes = computed(() => {
       const prefixCls = componentName;
       return {
-        [prefixCls]: true
+        [prefixCls]: true,
+        'scroll-fixed': isScrollFixed.value
       };
     });
 
@@ -90,7 +113,11 @@ export default create({
           Taro.createSelectorQuery()
             .select('.nut-menu__bar.opened')
             .boundingClientRect((rect) => {
-              offset.value = rect.bottom;
+              if (props.direction === 'down') {
+                offset.value = rect.bottom;
+              } else {
+                offset.value = Taro.getSystemInfoSync().windowHeight - rect.top;
+              }
             })
             .exec();
         }, 100);
@@ -110,12 +137,44 @@ export default create({
       });
     };
 
+    const onScroll = (res: { scrollTop: number }) => {
+      const { scrollFixed } = props;
+
+      const scrollTop = res.scrollTop;
+
+      isScrollFixed.value = scrollTop > (typeof scrollFixed === 'boolean' ? 30 : Number(scrollFixed));
+    };
+
+    const getClasses = (showPopup: boolean) => {
+      let str = '';
+      const { titleClass } = props;
+
+      if (showPopup) {
+        str += 'active';
+      }
+
+      if (titleClass) {
+        str += ` ${titleClass}`;
+      }
+
+      return str;
+    };
+
+    usePageScroll((res) => {
+      const { scrollFixed } = props;
+
+      if (scrollFixed) {
+        onScroll(res);
+      }
+    });
+
     return {
       toggleItem,
       children,
       opened,
       classes,
-      barRef
+      barRef,
+      getClasses
     };
   }
 });

+ 75 - 7
src/packages/__VUE/menu/index.vue

@@ -8,9 +8,13 @@
           :class="{ disabled: item.disabled, active: item.state.showPopup }"
           :style="{ color: item.state.showPopup ? activeColor : '' }"
         >
-          <view class="nut-menu__title" :class="{ active: item.state.showPopup }">
+          <view class="nut-menu__title" :class="getClasses(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>
+            <nut-icon
+              :name="item.titleIcon || (direction === 'up' ? 'arrow-up' : 'down-arrow')"
+              size="10"
+              class="nut-menu__title-icon"
+            ></nut-icon>
           </view>
         </view>
       </template>
@@ -19,7 +23,7 @@
   </view>
 </template>
 <script lang="ts">
-import { reactive, provide, computed, ref, Ref, unref } from 'vue';
+import { reactive, provide, computed, ref, onMounted, onUnmounted } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 import { useRect } from '@/packages/utils/useRect';
 const { componentName, create } = createComponent('menu');
@@ -33,6 +37,10 @@ export default create({
       type: Boolean,
       default: true as const
     },
+    lockScroll: {
+      type: Boolean,
+      default: true as const
+    },
     duration: {
       type: [Number, String],
       default: 0
@@ -40,11 +48,21 @@ export default create({
     closeOnClickOverlay: {
       type: Boolean,
       default: true
-    }
+    },
+    direction: {
+      type: String,
+      default: 'down'
+    },
+    scrollFixed: {
+      type: [Boolean, String, Number],
+      default: false
+    },
+    titleClass: [String]
   },
   setup(props, { emit, slots }) {
     const barRef = ref<HTMLElement>();
     const offset = ref(0);
+    const isScrollFixed = ref(false);
 
     const useChildren = () => {
       const publicChildren: any[] = reactive([]);
@@ -84,14 +102,20 @@ export default create({
     const classes = computed(() => {
       const prefixCls = componentName;
       return {
-        [prefixCls]: true
+        [prefixCls]: true,
+        'scroll-fixed': isScrollFixed.value
       };
     });
 
     const updateOffset = () => {
       if (barRef.value) {
         const rect = useRect(barRef);
-        offset.value = rect.bottom;
+
+        if (props.direction === 'down') {
+          offset.value = rect.bottom;
+        } else {
+          offset.value = window.innerHeight - rect.top;
+        }
       }
     };
 
@@ -108,12 +132,56 @@ export default create({
       });
     };
 
+    const getScrollTop = (el: Element | Window) => {
+      return Math.max(0, 'scrollTop' in el ? el.scrollTop : el.pageYOffset);
+    };
+
+    const onScroll = () => {
+      const { scrollFixed } = props;
+
+      const scrollTop = getScrollTop(window);
+
+      isScrollFixed.value = scrollTop > (typeof scrollFixed === 'boolean' ? 30 : Number(scrollFixed));
+    };
+
+    const getClasses = (showPopup: boolean) => {
+      let str = '';
+      const { titleClass } = props;
+
+      if (showPopup) {
+        str += 'active';
+      }
+
+      if (titleClass) {
+        str += ` ${titleClass}`;
+      }
+
+      return str;
+    };
+
+    onMounted(() => {
+      const { scrollFixed } = props;
+
+      if (scrollFixed) {
+        window.addEventListener('scroll', onScroll);
+      }
+    });
+
+    onUnmounted(() => {
+      const { scrollFixed } = props;
+
+      if (scrollFixed) {
+        window.removeEventListener('scroll', onScroll);
+      }
+    });
+
     return {
       toggleItem,
       children,
       opened,
       classes,
-      barRef
+      barRef,
+      getClasses
     };
   }
 });

+ 4 - 0
src/packages/__VUE/menuitem/index.scss

@@ -43,4 +43,8 @@
   right: 0;
   z-index: $menu-bar-opened-z-index;
   background-color: transparent;
+
+  &.up {
+    bottom: -$menu-bar-line-height;
+  }
 }

+ 26 - 7
src/packages/__VUE/menuitem/index.taro.vue

@@ -4,20 +4,27 @@
       v-show="state.isShowPlaceholderElement"
       @click="handleClickOutside"
       class="placeholder-element"
-      :style="{ height: parent.offset.value + 'px' }"
+      :class="{ up: parent.props.direction === 'up' }"
+      :style="placeholderElementStyle"
     >
     </div>
     <nut-popup
-      :style="{ top: parent.offset.value + 'px' }"
-      :overlayStyle="{ top: parent.offset.value + 'px' }"
+      :style="
+        parent.props.direction === 'down' ? { top: parent.offset.value + 'px' } : { bottom: parent.offset.value + 'px' }
+      "
+      :overlayStyle="
+        parent.props.direction === 'down' ? { top: parent.offset.value + 'px' } : { bottom: parent.offset.value + 'px' }
+      "
       v-bind="$attrs"
       v-model:visible="state.showPopup"
-      position="top"
+      :position="parent.props.direction === 'down' ? 'top' : 'bottom'"
       :duration="parent.props.duration"
       pop-class="nut-menu__pop"
       overlayClass="nut-menu__overlay"
       :overlay="parent.props.overlay"
+      :lockScroll="parent.props.lockScroll"
       @closed="handleClose"
+      :close-on-click-overlay="parent.props.closeOnClickOverlay"
     >
       <view class="nut-menu-item__content">
         <view
@@ -28,7 +35,7 @@
           :style="{ 'flex-basis': 100 / cols + '%' }"
           @click="onClick(option)"
         >
-          <nut-icon v-if="option.value === modelValue" name="Check" :color="parent.props.activeColor"></nut-icon>
+          <nut-icon v-if="option.value === modelValue" :name="optionIcon" :color="parent.props.activeColor"></nut-icon>
           <view :style="{ color: option.value === modelValue ? parent.props.activeColor : '' }">{{ option.text }}</view>
         </view>
         <slot></slot>
@@ -59,9 +66,10 @@ export default create({
       type: Number,
       default: 1
     },
-    titleIcon: {
+    titleIcon: String,
+    optionIcon: {
       type: String,
-      default: 'down-arrow'
+      default: 'Check'
     }
   },
   components: {
@@ -104,6 +112,16 @@ export default create({
       };
     });
 
+    const placeholderElementStyle = computed(() => {
+      const heightStyle = { height: parent.offset.value + 'px' };
+
+      if (parent.props.direction === 'down') {
+        return heightStyle;
+      } else {
+        return { ...heightStyle, top: 'auto' };
+      }
+    });
+
     const toggle = (show = !state.showPopup, options: { immediate?: boolean } = {}) => {
       if (show === state.showPopup) {
         return;
@@ -149,6 +167,7 @@ export default create({
 
     return {
       classes,
+      placeholderElementStyle,
       renderTitle,
       state,
       parent,

+ 25 - 7
src/packages/__VUE/menuitem/index.vue

@@ -4,20 +4,26 @@
       v-show="state.isShowPlaceholderElement"
       @click="handleClickOutside"
       class="placeholder-element"
-      :style="{ height: parent.offset.value + 'px' }"
+      :class="{ up: parent.props.direction === 'up' }"
+      :style="placeholderElementStyle"
     >
     </div>
     <nut-popup
-      :style="{ top: parent.offset.value + 'px' }"
-      :overlayStyle="{ top: parent.offset.value + 'px' }"
+      :style="
+        parent.props.direction === 'down' ? { top: parent.offset.value + 'px' } : { bottom: parent.offset.value + 'px' }
+      "
+      :overlayStyle="
+        parent.props.direction === 'down' ? { top: parent.offset.value + 'px' } : { bottom: parent.offset.value + 'px' }
+      "
       v-bind="$attrs"
       v-model:visible="state.showPopup"
-      position="top"
+      :position="parent.props.direction === 'down' ? 'top' : 'bottom'"
       :duration="parent.props.duration"
       pop-class="nut-menu__pop"
       overlayClass="nut-menu__overlay"
       :overlay="parent.props.overlay"
       @closed="handleClose"
+      :lockScroll="parent.props.lockScroll"
       :isWrapTeleport="false"
       :close-on-click-overlay="parent.props.closeOnClickOverlay"
     >
@@ -30,7 +36,7 @@
           :style="{ 'flex-basis': 100 / cols + '%' }"
           @click="onClick(option)"
         >
-          <nut-icon v-if="option.value === modelValue" name="Check" :color="parent.props.activeColor"></nut-icon>
+          <nut-icon v-if="option.value === modelValue" :name="optionIcon" :color="parent.props.activeColor"></nut-icon>
           <view :style="{ color: option.value === modelValue ? parent.props.activeColor : '' }">{{ option.text }}</view>
         </view>
         <slot></slot>
@@ -61,9 +67,10 @@ export default create({
       type: Number,
       default: 1
     },
-    titleIcon: {
+    titleIcon: String,
+    optionIcon: {
       type: String,
-      default: 'down-arrow'
+      default: 'Check'
     }
   },
   components: {
@@ -106,6 +113,16 @@ export default create({
       };
     });
 
+    const placeholderElementStyle = computed(() => {
+      const heightStyle = { height: parent.offset.value + 'px' };
+
+      if (parent.props.direction === 'down') {
+        return heightStyle;
+      } else {
+        return { ...heightStyle, top: 'auto' };
+      }
+    });
+
     const toggle = (show = !state.showPopup, options: { immediate?: boolean } = {}) => {
       if (show === state.showPopup) {
         return;
@@ -151,6 +168,7 @@ export default create({
 
     return {
       classes,
+      placeholderElementStyle,
       renderTitle,
       state,
       parent,

+ 2 - 0
src/packages/styles/variables-jdb.scss

@@ -660,6 +660,8 @@ $menu-item-option-padding-top: 12px !default;
 $menu-item-option-padding-bottom: 12px !default;
 $menu-item-option-i-margin-right: 6px !default;
 $menu-bar-box-shadow: 0 2px 12px rgba(89, 89, 89, 0.12) !default;
+$nut-menu-scroll-fixed-top: 0 !default;
+$nut-menu-scroll-fixed-z-index: 1000 !default;
 
 // collapse
 $collapse-item-padding: 13px 36px 13px 26px !default;

+ 2 - 0
src/packages/styles/variables-jdt.scss

@@ -566,6 +566,8 @@ $menu-item-option-padding-top: 12px !default;
 $menu-item-option-padding-bottom: 12px !default;
 $menu-item-option-i-margin-right: 6px !default;
 $menu-bar-box-shadow: 0 2px 12px rgba(89, 89, 89, 0.12) !default;
+$nut-menu-scroll-fixed-top: 0 !default;
+$nut-menu-scroll-fixed-z-index: 1000 !default;
 
 // collapse
 $collapse-item-padding: 13px 36px 13px 26px !default;

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

@@ -591,6 +591,8 @@ $menu-item-option-padding-top: 12px !default;
 $menu-item-option-padding-bottom: 12px !default;
 $menu-item-option-i-margin-right: 6px !default;
 $menu-bar-box-shadow: 0 2px 12px rgba(89, 89, 89, 0.12) !default;
+$nut-menu-scroll-fixed-top: 0 !default;
+$nut-menu-scroll-fixed-z-index: 1000 !default;
 
 // collapse
 $collapse-item-padding: 13px 36px 13px 26px !default;

+ 3 - 3
src/sites/mobile-taro/vue/src/layout/pages/divider/index.vue

@@ -12,12 +12,12 @@
     <h2>自定义样式</h2>
     <nut-divider :style="{ color: '#1989fa', borderColor: '#1989fa', padding: '0 16px' }">文本</nut-divider>
     <h2>垂直分割线</h2>
-    <div :style="{ fontSize: '14px' }">
+    <div :style="{ fontSize: '14px', marginLeft: '27px', color: '#909ca4' }">
       文本
       <nut-divider direction="vertical" />
-      <a href="#">文本</a>
+      <a href="#" :style="{ color: '#1989fa' }">链接</a>
       <nut-divider direction="vertical" />
-      <a href="#">文本</a>
+      <a href="#" :style="{ color: '#1989fa' }">链接</a>
     </div>
   </div>
 </template>

+ 10 - 0
src/sites/mobile-taro/vue/src/nav/pages/menu/index.vue

@@ -24,6 +24,16 @@
       <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>
+    <nut-menu>
+      <nut-menu-item v-model="state.value1" :options="state.options1" titleIcon="joy-smile" />
+      <nut-menu-item v-model="state.value2" @change="handleChange" :options="state.options2" optionIcon="checklist" />
+    </nut-menu>
+    <h2>向上展开</h2>
+    <nut-menu direction="up">
+      <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>
     <nut-menu>
       <nut-menu-item disabled v-model="state.value1" :options="state.options1" />