| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398 |
- <template>
- <view
- class="nana-bubble-box"
- :style="outerStyle"
- @mouseenter="handleHover(true)"
- @mouseleave="handleHover(false)"
- >
- <view v-if="trigger === 'click'" @click.native.capture="handleClick">
- <slot />
- </view>
- <slot v-else />
- <SimpleTransition name="bubble-box" :show="showState" :duration="200">
- <template #show="{ classNames }">
- <view v-if="clickOutSideClose" class="nana-bubble-box-popup-mask" @click="hide" />
- <view class="nana-bubble-box-holder-position" @click="hideAndEmitClickOnHolder" />
- <FlexView
- position="absolute"
- :direction="direction"
- :backgroundColor="backgroundColor"
- :radius="radius"
- :gap="10"
- :padding="10"
- :zIndex="1001"
- :margin="selectObjectByType(position, 'left', {
- top: [arrowWidth,0],
- bottom: [arrowWidth,0],
- left: [0,arrowWidth],
- right: [0,arrowWidth],
- })"
- :innerClass="['nana-bubble-box-popup',position,classNames]"
- shadow="light"
- v-bind="innerProps"
- :innerStyle="innerStyle"
- >
- <view
- class="nana-bubble-box-arrow"
- :style="{
- marginTop: theme.resolveThemeSize(arrowOffsetY),
- marginLeft: theme.resolveThemeSize(arrowOffsetX),
- borderWidth: theme.resolveThemeSize(arrowWidth),
- borderColor: backgroundColor,
- borderRightColor: 'transparent',
- borderBottomColor: 'transparent',
- borderLeftColor: 'transparent',
- }"
- />
- <slot name="content">
- <Touchable
- v-for="item in items"
- :key="item.text"
- direction="row"
- align-items="center"
- :gap="10"
- :padding="[5, 20]"
- v-bind="itemProps"
- @click="handleItemClick(item)"
- >
- <Icon
- :name="item.icon"
- :size="44"
- :color="item.textColor || itemTextColor"
- v-bind="{ ...itemIconProps, ...item.iconProps }"
- />
- <Text
- :wrap="false"
- v-bind="itemTextProps"
- :color="item.textColor || itemTextColor"
- :text="item.text"
- />
- </Touchable>
- </slot>
- </FlexView>
- </template>
- </SimpleTransition>
- </view>
- </template>
- <script setup lang="ts">
- import { computed, ref } from 'vue';
- import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
- import { selectObjectByType, selectStyleType } from '../theme/ThemeTools';
- import type { FlexProps } from '../layout/FlexView.vue';
- import type { TextProps } from '../basic/Text.vue';
- import Icon, { type IconProps } from '../basic/Icon.vue';
- import FlexView from '../layout/FlexView.vue';
- import Text from '../basic/Text.vue';
- import Touchable from './Touchable.vue';
- import SimpleTransition from '../anim/SimpleTransition.vue';
- export interface BubbleBoxItem {
- text: string,
- textColor?: string,
- icon?: string,
- iconProps?: IconProps,
- onClick: () => void,
- }
- export interface BubbleBoxProps {
- /**
- * 气泡框位置
- * @default top
- */
- position?: 'left' | 'right' | 'top' | 'bottom',
- /**
- * 气泡在横轴上的对齐位置,默认居中
- * @default center
- */
- crossPosition?: 'left' | 'center' | 'right',
- /**
- * 触发点击事件模式
- * @default click
- */
- trigger?: 'click'|'hover'|'none',
- /**
- * 气泡框按钮排列方向
- * @default column
- */
- direction?: 'column' | 'row',
- /**
- * 是否禁用
- * @default false
- */
- disabled?: boolean,
- /**
- * 气泡框按钮数组
- */
- items?: BubbleBoxItem[],
- /**
- * 气泡框按钮文本颜色
- * @default 'text.content'
- */
- itemTextColor?: string,
- /**
- * 气泡框按钮文本样式
- */
- itemTextProps?: TextProps,
- /**
- * 气泡框按钮图标样式
- */
- itemIconProps?: IconProps,
- /**
- * 气泡框按钮样式
- */
- itemProps?: FlexProps,
- /**
- * 气泡框外层容器样式
- */
- outerStyle?: Record<string, string>,
- /**
- * 气泡框背景颜色
- * @default white
- */
- backgroundColor?: string,
- /**
- * 气泡框箭头宽度
- * @default 12
- */
- arrowWidth?: number,
- /**
- * 气泡框箭头X轴偏移
- * @default 0
- */
- arrowOffsetX?: number|string,
- /**
- * 气泡框箭头X轴偏移
- * @default 0
- */
- arrowOffsetY?: number|string,
- /**
- * 是否允许点击外部自动关闭
- * @default true
- */
- clickOutSideClose?: boolean,
- /**
- * 气泡框圆角半径
- * @default 12
- */
- radius?: number,
- /**
- * 气泡框内层容器样式
- */
- innerProps?: FlexProps,
- /**
- * 气泡框内层容器样式
- */
- innerStyle?: Record<string, string>,
- }
- export interface BubbleBoxExpose {
- show: () => void,
- hide: () => void,
- }
- const theme = useTheme();
- const props = withDefaults(defineProps<BubbleBoxProps>(), {
- position: 'top',
- trigger: 'click',
- direction: 'column',
- clickOutSideClose: true,
- arrowWidth: () => propGetThemeVar('BubbleBoxArrowWidth', 12),
- items: () => [],
- itemTextColor: () => propGetThemeVar('BubbleBoxItemTextColor', 'text.content'),
- backgroundColor: () => propGetThemeVar('BubbleBoxBackgroundColor', 'white'),
- radius: () => propGetThemeVar('BubbleBoxRadius', 12),
- });
- const emit = defineEmits(['show', 'hide', 'clickOnHolder']);
- const backgroundColor = computed(() => theme.resolveThemeColor(props.backgroundColor));
- const showState = ref(false);
- const lock = ref(false);
- function handleItemClick(item: BubbleBoxItem) {
- hide();
- item.onClick();
- }
- function handleClick() {
- if (lock.value) return;
- if (props.trigger === 'click' && !props.disabled) {
- enterLock();
- showState.value = !showState.value;
- emit(showState.value ? 'show' : 'hide');
- }
- }
- function handleHover(show: boolean) {
- if (lock.value) return;
- if (props.trigger === 'hover' && !props.disabled) {
- showState.value = show;
- emit(show ? 'show' : 'hide');
- }
- }
- function enterLock() {
- lock.value = true;
- setTimeout(() => {
- lock.value = false;
- }, 300);
- }
- function show() {
- showState.value = true;
- emit('show');
- enterLock();
- }
- function hide() {
- showState.value = false;
- emit('hide');
- enterLock();
- }
- function hideAndEmitClickOnHolder() {
- hide();
- emit('clickOnHolder');
- }
- const innerStyle = computed(() => {
- const horzLayout = selectStyleType(props.crossPosition, 'center', {
- left: {
- k: 'top',
- y: '0%',
- t: 'translateY(0%)',
- },
- center: {
- k: 'top',
- y: '50%',
- t: 'translateY(-50%)',
- },
- right: {
- k: 'bottom',
- y: '0',
- t: 'translateY(0%)',
- }
- });
- const vertLayout = selectStyleType(props.crossPosition, 'center', {
- left: {
- k: 'left',
- x: '0%',
- t: 'translateX(0%)',
- },
- center: {
- k: 'left',
- x: '50%',
- t: 'translateX(-50%)',
- },
- right: {
- k: 'right',
- x: '0%',
- t: 'translateX(0%)',
- }
- });
- return {
- ...props.innerStyle,
- ...selectStyleType(props.position, 'top', {
- left: {
- [horzLayout.k]: horzLayout.y,
- right: '100%',
- transform: horzLayout.t + ' translateX(0)',
- },
- right: {
- [horzLayout.k]: horzLayout.y,
- left: '100%',
- transform: horzLayout.t + '',
- },
- top: {
- top: '0%',
- [vertLayout.k]: vertLayout.x,
- transform: vertLayout.t + ' translateY(-100%)',
- },
- bottom: {
- bottom: '0%',
- [vertLayout.k]: vertLayout.x,
- transform: vertLayout.t + ' translateY(100%)',
- }
- })
- }
- })
- defineExpose<BubbleBoxExpose>({
- show,
- hide,
- })
- defineOptions({
- options: {
- virtualHost: true,
- styleIsolation: "shared",
- }
- })
- </script>
- <style lang="scss">
- .nana-bubble-box {
- position: relative;
- overflow: visible;
- .nana-bubble-box-popup {
- position: absolute;
- transition: opacity ease-in-out 0.2s;
- &.left {
- .nana-bubble-box-arrow {
- top: 50%;
- left: 100%;
- transform: translateY(-50%) rotate(-90deg);
- }
- }
- &.right {
- .nana-bubble-box-arrow {
- top: 50%;
- left: 0;
- transform: translateX(-100%) translateY(-50%) rotate(90deg);
- }
- }
- &.top {
- .nana-bubble-box-arrow {
- top: 100%;
- left: 50%;
- transform: translateX(-50%) rotate(0);
- }
- }
- &.bottom {
- .nana-bubble-box-arrow {
- top: 0;
- left: 50%;
- transform: translateX(-50%) translateY(-100%) rotate(180deg);
- }
- }
-
- &.bubble-box-enter-active,
- &.bubble-box-leave-active {
- opacity: 1;
- }
- &.bubble-box-enter-from,
- &.bubble-box-leave-to {
- opacity: 0;
- }
- }
- .nana-bubble-box-holder-position {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 1001;
- }
- .nana-bubble-box-popup-mask {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 1000;
- }
- .nana-bubble-box-arrow {
- position: absolute;
- width: 0;
- height: 0;
- border-style: solid;
- }
- }
- </style>
|