BubbleBox.vue 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. <template>
  2. <view
  3. class="nana-bubble-box"
  4. :style="outerStyle"
  5. @mouseenter="handleHover(true)"
  6. @mouseleave="handleHover(false)"
  7. >
  8. <view v-if="trigger === 'click'" @click.native.capture="handleClick">
  9. <slot />
  10. </view>
  11. <slot v-else />
  12. <SimpleTransition name="bubble-box" :show="showState" :duration="200">
  13. <template #show="{ classNames }">
  14. <view class="nana-bubble-box-popup-mask" @click="hide" />
  15. <FlexView
  16. v-if="items?.length"
  17. position="absolute"
  18. :direction="direction"
  19. :backgroundColor="backgroundColor"
  20. :radius="radius"
  21. :gap="10"
  22. :padding="10"
  23. :zIndex="1001"
  24. :margin="selectObjectByType(position, 'left', {
  25. top: [arrowWidth,0],
  26. bottom: [arrowWidth,0],
  27. left: [0,arrowWidth],
  28. right: [0,arrowWidth],
  29. })"
  30. :innerClass="['nana-bubble-box-popup',position,classNames]"
  31. shadow="light"
  32. v-bind="innerProps"
  33. :innerStyle="innerStyle"
  34. >
  35. <view
  36. class="nana-bubble-box-arrow"
  37. :style="{
  38. borderWidth: theme.resolveThemeSize(arrowWidth),
  39. borderColor: backgroundColor,
  40. borderRightColor: 'transparent',
  41. borderBottomColor: 'transparent',
  42. borderLeftColor: 'transparent',
  43. }"
  44. />
  45. <Touchable
  46. v-for="item in items"
  47. :key="item.text"
  48. direction="row"
  49. align-items="center"
  50. :gap="10"
  51. :padding="[5, 20]"
  52. v-bind="itemProps"
  53. @click="handleItemClick(item)"
  54. >
  55. <Icon
  56. :name="item.icon"
  57. :size="44"
  58. :color="item.textColor || itemTextColor"
  59. v-bind="{ ...itemIconProps, ...item.iconProps }"
  60. />
  61. <Text
  62. :wrap="false"
  63. v-bind="itemTextProps"
  64. :color="item.textColor || itemTextColor"
  65. :text="item.text"
  66. />
  67. </Touchable>
  68. </FlexView>
  69. </template>
  70. </SimpleTransition>
  71. </view>
  72. </template>
  73. <script setup lang="ts">
  74. import { computed, ref } from 'vue';
  75. import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
  76. import { selectObjectByType } from '../theme/ThemeTools';
  77. import type { FlexProps } from '../layout/FlexView.vue';
  78. import type { TextProps } from '../basic/Text.vue';
  79. import Icon, { type IconProps } from '../basic/Icon.vue';
  80. import FlexView from '../layout/FlexView.vue';
  81. import Text from '../basic/Text.vue';
  82. import Touchable from './Touchable.vue';
  83. import SimpleTransition from '../anim/SimpleTransition.vue';
  84. export interface BubbleBoxItem {
  85. text: string,
  86. textColor?: string,
  87. icon?: string,
  88. iconProps?: IconProps,
  89. onClick: () => void,
  90. }
  91. export interface BubbleBoxProps {
  92. /**
  93. * 气泡框位置
  94. * @default top
  95. */
  96. position?: 'left' | 'right' | 'top' | 'bottom',
  97. /**
  98. * 触发点击事件模式
  99. * @default click
  100. */
  101. trigger?: 'click'|'hover'|'none',
  102. /**
  103. * 气泡框按钮排列方向
  104. * @default column
  105. */
  106. direction?: 'column' | 'row',
  107. /**
  108. * 是否禁用
  109. * @default false
  110. */
  111. disabled?: boolean,
  112. /**
  113. * 气泡框按钮数组
  114. */
  115. items?: BubbleBoxItem[],
  116. /**
  117. * 气泡框按钮文本颜色
  118. * @default 'text.content'
  119. */
  120. itemTextColor?: string,
  121. /**
  122. * 气泡框按钮文本样式
  123. */
  124. itemTextProps?: TextProps,
  125. /**
  126. * 气泡框按钮图标样式
  127. */
  128. itemIconProps?: IconProps,
  129. /**
  130. * 气泡框按钮样式
  131. */
  132. itemProps?: FlexProps,
  133. /**
  134. * 气泡框外层容器样式
  135. */
  136. outerStyle?: Record<string, string>,
  137. /**
  138. * 气泡框背景颜色
  139. * @default white
  140. */
  141. backgroundColor?: string,
  142. /**
  143. * 气泡框箭头宽度
  144. * @default 12
  145. */
  146. arrowWidth?: number,
  147. /**
  148. * 气泡框圆角半径
  149. * @default 12
  150. */
  151. radius?: number,
  152. /**
  153. * 气泡框内层容器样式
  154. */
  155. innerProps?: FlexProps,
  156. /**
  157. * 气泡框内层容器样式
  158. */
  159. innerStyle?: Record<string, string>,
  160. }
  161. export interface BubbleBoxExpose {
  162. show: () => void,
  163. hide: () => void,
  164. }
  165. const theme = useTheme();
  166. const props = withDefaults(defineProps<BubbleBoxProps>(), {
  167. position: 'top',
  168. trigger: 'click',
  169. direction: 'column',
  170. arrowWidth: () => propGetThemeVar('BubbleBoxArrowWidth', 12),
  171. items: () => [],
  172. itemTextColor: () => propGetThemeVar('BubbleBoxItemTextColor', 'text.content'),
  173. backgroundColor: () => propGetThemeVar('BubbleBoxBackgroundColor', 'white'),
  174. radius: () => propGetThemeVar('BubbleBoxRadius', 12),
  175. });
  176. const backgroundColor = computed(() => theme.resolveThemeColor(props.backgroundColor));
  177. const showState = ref(false);
  178. const lock = ref(false);
  179. function handleItemClick(item: BubbleBoxItem) {
  180. hide();
  181. item.onClick();
  182. }
  183. function handleClick() {
  184. if (lock.value) return;
  185. if (props.trigger === 'click' && !props.disabled) {
  186. enterLock();
  187. showState.value = !showState.value;
  188. }
  189. }
  190. function handleHover(show: boolean) {
  191. if (lock.value) return;
  192. if (props.trigger === 'hover' && !props.disabled)
  193. showState.value = show;
  194. }
  195. function enterLock() {
  196. lock.value = true;
  197. setTimeout(() => {
  198. lock.value = false;
  199. }, 300);
  200. }
  201. function show() {
  202. showState.value = true;
  203. enterLock();
  204. }
  205. function hide() {
  206. showState.value = false;
  207. enterLock();
  208. }
  209. defineExpose<BubbleBoxExpose>({
  210. show,
  211. hide,
  212. })
  213. defineOptions({
  214. options: {
  215. virtualHost: true,
  216. styleIsolation: "shared",
  217. }
  218. })
  219. </script>
  220. <style lang="scss">
  221. .nana-bubble-box {
  222. position: relative;
  223. overflow: visible;
  224. .nana-bubble-box-popup {
  225. position: absolute;
  226. transition: opacity ease-in-out 0.2s;
  227. &.left {
  228. top: 50%;
  229. right: 100%;
  230. transform: translateY(-50%) translateX(0);
  231. .nana-bubble-box-arrow {
  232. top: 50%;
  233. left: 100%;
  234. transform: translateY(-50%) rotate(-90deg);
  235. }
  236. }
  237. &.right {
  238. top: 50%;
  239. left: 100%;
  240. transform: translateY(-50%);
  241. .nana-bubble-box-arrow {
  242. top: 50%;
  243. left: 0;
  244. transform: translateX(-100%) translateY(-50%) rotate(90deg);
  245. }
  246. }
  247. &.top {
  248. bottom: -100%;
  249. left: 50%;
  250. transform: translateX(-50%) translateY(-100%);
  251. .nana-bubble-box-arrow {
  252. top: 100%;
  253. left: 50%;
  254. transform: translateX(-50%) rotate(0);
  255. }
  256. }
  257. &.bottom {
  258. top: 100%;
  259. left: 50%;
  260. transform: translateX(-50%);
  261. .nana-bubble-box-arrow {
  262. top: 0;
  263. left: 50%;
  264. transform: translateX(-50%) translateY(-100%) rotate(180deg);
  265. }
  266. }
  267. &.bubble-box-enter-active,
  268. &.bubble-box-leave-active {
  269. opacity: 1;
  270. }
  271. &.bubble-box-enter-from,
  272. &.bubble-box-leave-to {
  273. opacity: 0;
  274. }
  275. }
  276. .nana-bubble-box-popup-mask {
  277. position: fixed;
  278. top: 0;
  279. left: 0;
  280. width: 100%;
  281. height: 100%;
  282. z-index: 1000;
  283. }
  284. .nana-bubble-box-arrow {
  285. position: absolute;
  286. width: 0;
  287. height: 0;
  288. border-style: solid;
  289. }
  290. }
  291. </style>