This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
@import '../popup/index';
.nut-theme-dark {
.nut-menu-item__content {
.nut-menu-item__option {
color: $dark-color;
}
}
}
.nut-menu-item {
position: fixed;
right: 0;
left: 0;
z-index: $menu-bar-opened-z-index;
height: 100vh;
overflow: hidden;
.active {
font-weight: $menu-active-item-font-weight;
color: $menu-item-active-text-color !important;
}
}
.nut-menu-item__content {
display: flex;
flex-wrap: wrap;
max-height: $menu-item-content-max-height;
padding: $menu-item-content-padding;
&.nut-menu-item__overflow {
overflow-y: auto;
}
.nut-menu-item__option {
display: flex;
align-items: center;
padding-top: $menu-item-option-padding-top;
padding-bottom: $menu-item-option-padding-bottom;
padding-left: 12px;
padding-right: 12px;
font-size: $font-size-2;
color: $title-color;
.nut-menu-item__span {
display: flex;
align-items: center;
margin-right: $menu-item-option-i-margin-right;
}
}
}
.nut-menu-item-placeholder-element {
position: fixed;
right: 0;
left: 0;
z-index: $menu-bar-opened-z-index;
background-color: transparent;
}

View File

@@ -0,0 +1 @@
export * from './menuitem'

View File

@@ -0,0 +1,74 @@
import type { ExtractPropTypes } from 'vue'
import { CHANGE_EVENT, CLOSE_EVENT, OPEN_EVENT, UPDATE_MODEL_EVENT } from '../_constants'
import { commonProps, makeArrayProp, makeNumberProp, makeStringProp } from '../_utils'
export interface MenuItemOption {
text: string
value: number | string
}
export const menuitemProps = {
...commonProps,
/**
* @@description 菜单项标题
*/
title: String,
/**
* @description 选项数组
*/
options: makeArrayProp<MenuItemOption>([]),
/**
* @description 是否禁用菜单
*/
disabled: Boolean,
modelValue: [String, Number],
/**
* @description 可以设置一行展示多少列 `options`
*/
cols: makeNumberProp(1),
/**
* @description 选项选中时自定义标题样式类
*/
activeTitleClass: String,
/**
* @description 选项非选中时自定义标题样式类
*/
inactiveTitleClass: String,
/**
* @description 选项选中时选中图标
*/
optionIcon: makeStringProp('Check'),
}
export type MenuItemProps = ExtractPropTypes<typeof menuitemProps>
/* eslint-disable unused-imports/no-unused-vars */
export const menuitemEmits = {
[UPDATE_MODEL_EVENT]: (value: number | string) => true,
[CHANGE_EVENT]: (value: number | string) => true,
[OPEN_EVENT]: () => true,
[CLOSE_EVENT]: () => true,
itemClick: (item: MenuItemOption) => true,
}
/* eslint-enable unused-imports/no-unused-vars */
export type MenuitemEmits = typeof menuitemEmits
export interface MenuItemInst {
/**
* @description 变更选择项
*/
change: (value?: number | string) => any
/**
* @description 切换菜单展示状态,传 `true` 为显示,`false` 为隐藏,不传参为取反
*/
toggle: (show?: boolean) => boolean
/**
* @description 打开菜单栏
*/
open: () => void
/**
* @description 关闭菜单栏
*/
close: () => void
}

View File

@@ -0,0 +1,196 @@
<script lang="ts">
import type { Ref } from 'vue'
import { computed, defineComponent, reactive } from 'vue'
import { CLOSE_EVENT, OPEN_EVENT, PREFIX } from '../_constants'
import { useInject } from '../_hooks'
import { getMainClass, getMainStyle } from '../_utils'
import Icon from '../icon/icon.vue'
import type { MenuProps } from '../menu'
import { MENU_KEY } from '../menu/menu'
import PopUp from '../popup/popup.vue'
import type { MenuItemOption } from './menuitem'
import { menuitemEmits, menuitemProps } from './menuitem'
const componentName = `${PREFIX}-menu-item`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
components: {
PopUp,
Icon,
},
props: menuitemProps,
emits: menuitemEmits,
setup(props, { emit, expose }) {
const state = reactive({
showPopup: false,
showWrapper: false,
})
const { parent } = useInject<{
props : MenuProps
offset : Ref<number>
}>(MENU_KEY)
const classes = computed(() => {
return getMainClass(props, componentName, {
'nut-hidden': !state.showWrapper,
})
})
const styles = computed(() => {
if (parent?.props.offset > 0) {
const obj = parent?.props.direction === 'down'
? { top: `${parent?.props.offset}px` }
: { bottom: `${parent?.offset.value}px` }
return getMainStyle(props, obj)
} else {
const obj = parent?.props.direction === 'down'
? { top: `${parent?.offset.value}px` }
: { bottom: `${parent?.offset.value}px` }
return getMainStyle(props, obj)
}
})
const placeholderElementStyle = computed(() => {
const heightStyle = { height: `${parent?.offset.value}px` }
if (parent?.props.direction === 'down')
return { ...heightStyle, top: 0 }
return { ...heightStyle, top: 'auto' }
})
const open = () => {
// TODO 触发更新offset
state.showPopup = true
state.showWrapper = true
}
const close = () => {
state.showPopup = false
}
const toggle = (show = !state.showPopup) => {
if (show === state.showPopup)
return
if (show)
open()
else
close()
}
const change = (value : MenuItemOption['value']) => {
if (value === props.modelValue)
return
emit('update:modelValue', value)
emit('change', value)
}
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
emit('itemClick', option)
change(option.value)
}
const handleClose = () => {
state.showWrapper = false
}
const handleClickOutside = () => {
state.showPopup = false
}
const handleVisible = (visible : boolean) => {
if (visible)
emit(OPEN_EVENT)
else
emit(CLOSE_EVENT)
}
expose({
change,
open,
close,
toggle,
})
return {
classes,
styles,
placeholderElementStyle,
renderTitle,
state,
parent,
toggle,
onClick,
handleClose,
handleVisible,
handleClickOutside,
}
},
})
</script>
<template>
<view :class="classes" :style="styles">
<view class="nut-menu-item-placeholder-element"
:class="{ 'nut-hidden': !state.showPopup, 'placeholder-element-up': parent?.props.direction === 'up' }"
:style="placeholderElementStyle" @click="handleClickOutside" />
<PopUp v-bind="$attrs" v-model:visible="state.showPopup" :custom-style="{ position: 'absolute' }"
:overlay-style="{ position: 'absolute' }" :position="parent?.props.direction === 'down' ? 'top' : 'bottom'"
:duration="parent?.props.duration" pop-class="nut-menu__pop" :destroy-on-close="false"
:safe-area-inset-top="false" :overlay="parent?.props.overlay" :lock-scroll="parent?.props.lockScroll"
:close-on-click-overlay="parent?.props.closeOnClickOverlay" @closed="handleClose"
@open="handleVisible(true)" @close="handleVisible(false)">
<scroll-view :scroll-y="true">
<view id="nut-menu-item__content" 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)">
<view v-if="option.value === modelValue" class="nut-menu-item__span"
:class="[option.value === modelValue ? activeTitleClass : inactiveTitleClass]">
<!-- #ifndef MP-WEIXIN -->
<slot name="icon">
<Icon name="Check" :custom-color="parent?.props.activeColor" />
</slot>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<Icon :name="optionIcon" :custom-color="parent?.props.activeColor" />
<!-- #endif -->
</view>
<view :class="[option.value === modelValue ? activeTitleClass : inactiveTitleClass]"
:style="{ color: option.value === modelValue ? parent?.props.activeColor : '' }">
{{ option.text }}
</view>
</view>
<slot />
</view>
</scroll-view>
</PopUp>
</view>
</template>
<style lang="scss">
@import './index';
</style>