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,74 @@
.nut-theme-dark {
.nut-menu {
.nut-menu__bar {
background-color: $dark-background;
.nut-menu__item {
color: $dark-color;
}
}
}
}
.nut-menu {
&.scroll-fixed {
position: fixed;
top: $menu-scroll-fixed-top;
z-index: $menu-scroll-fixed-z-index;
width: 100%;
}
.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__item {
flex: 1;
min-width: 0;
font-size: $menu-item-font-size;
color: $menu-item-text-color;
text-align: center;
&.active {
color: $menu-item-active-text-color;
}
&.disabled {
color: $menu-item-disabled-color;
}
.nut-menu__title-icon {
display: flex;
transition: all 0.2s linear;
}
.nut-menu__title {
display: flex;
align-items: center;
justify-content: center;
max-width: 100%;
.nut-menu__title-text {
display: block;
padding-right: $menu-title-text-padding-right;
padding-left: $menu-title-text-padding-left;
@include text-ellipsis;
}
&.active .nut-menu__title-icon {
transform: rotate(180deg);
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,65 @@
import type { ExtractPropTypes } from 'vue'
import { commonProps, makeNumberProp, makeStringProp, truthProp } from '../_utils'
export const MENU_KEY = Symbol('nut-menu')
export const menuProps = {
...commonProps,
/**
* @description 选项的选中态图标颜色
*/
activeColor: makeStringProp(''),
/**
* @description 是否显示遮罩
*/
overlay: truthProp,
/**
* @description 是否锁定滚动
*/
lockScroll: truthProp,
/**
* @description 动画时长
*/
duration: {
type: [Number, String],
default: 300,
},
/**
* @description 标题图标
*/
titleIcon: String,
/**
* @description 是否在点击遮罩层后关闭菜单
*/
closeOnClickOverlay: truthProp,
/**
* @description 展开方向
*/
direction: makeStringProp<'down' | 'up'>('down'),
/**
* @description 滚动后是否固定,可设置固定位置(需要配合 `scrollTop` 使用)
*/
scrollFixed: {
type: [Boolean, String, Number],
default: false,
},
/**
* @description 页面的滚动距离,通过 `onPageScroll` 获取
*/
scrollTop: makeNumberProp(0),
/**
* @description 标题样式类名
*/
titleClass: [String],
/**
* @description 收起的图标
*/
upIcon: makeStringProp('rect-up'),
/**
* @description 展开时的图标
*/
downIcon: makeStringProp('rect-down'),
offset: Number,
}
export type MenuProps = ExtractPropTypes<typeof menuProps>

View File

@@ -0,0 +1,128 @@
<script lang="ts">
import type { ComponentInternalInstance } from 'vue'
import { computed, defineComponent, getCurrentInstance, ref } from 'vue'
import { PREFIX } from '../_constants'
import { useProvide, useRect } from '../_hooks'
import { getMainClass, getRandomId } from '../_utils'
import Icon from '../icon/icon.vue'
import { MENU_KEY, menuProps } from './menu'
const componentName = `${PREFIX}-menu`
export default defineComponent({
name: componentName,
components: { Icon },
props: menuProps,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
setup(props) {
const barId = `nut-menu__bar${getRandomId()}`
const offset = ref(0)
const instance = getCurrentInstance() as ComponentInternalInstance
const { children } = useProvide(MENU_KEY)({ props, offset })
const opened = computed(() => children.some(item => item?.state?.showWrapper))
const isScrollFixed = computed(() => {
const { scrollFixed, scrollTop } = props
if (!scrollFixed)
return false
return scrollTop > (typeof scrollFixed === 'boolean' ? 30 : Number(scrollFixed))
})
const classes = computed(() => {
return getMainClass(props, componentName, {
'scroll-fixed': isScrollFixed.value,
})
})
function updateOffset(children: any) {
setTimeout(() => {
useRect(barId, instance).then((rect) => {
if (props.direction === 'down')
offset.value = rect.bottom! + uni.getSystemInfoSync().windowTop!
else offset.value = uni.getSystemInfoSync().windowHeight - rect.top!
children.toggle()
})
}, 100)
}
function toggleItem(active: number) {
children.forEach((item, index) => {
if (index === active)
updateOffset(item)
else if (item.state.showPopup)
item.toggle(false, { immediate: true })
})
}
function getClasses(showPopup: boolean) {
let str = ''
const { titleClass } = props
if (showPopup)
str += 'active'
if (titleClass)
str += ` ${titleClass}`
return str
}
return {
barId,
toggleItem,
children,
opened,
classes,
getClasses,
}
},
})
</script>
<template>
<view :class="classes" :style="customStyle">
<view :id="barId" class="nut-menu__bar" :class="{ opened }">
<template v-for="(item, index) in children" :key="index">
<view
class="nut-menu__item"
:class="{ disabled: item.disabled, active: item.state.showPopup }"
:style="{ color: item.state.showPopup ? activeColor : '' }"
@click="!item.disabled && toggleItem(index)"
>
<view class="nut-menu__title" :class="getClasses(item.state.showPopup)">
<view class="nut-menu__title-text">
{{ item.renderTitle() }}
</view>
<view class="nut-menu__title-icon">
<!-- #ifdef MP-WEIXIN -->
<Icon v-if="direction === 'up'" :name="upIcon" />
<Icon v-else :name="downIcon" />
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<slot name="icon">
<Icon v-if="direction === 'up'" name="rect-up" />
<Icon v-else name="rect-down" />
</slot>
<!-- #endif -->
</view>
</view>
</view>
</template>
</view>
<slot />
</view>
</template>
<style lang="scss">
@import './index';
</style>