BubbleBox.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  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. position="absolute"
  17. :direction="direction"
  18. :backgroundColor="backgroundColor"
  19. :radius="radius"
  20. :gap="10"
  21. :padding="10"
  22. :zIndex="1001"
  23. :margin="selectObjectByType(position, 'left', {
  24. top: [arrowWidth,0],
  25. bottom: [arrowWidth,0],
  26. left: [0,arrowWidth],
  27. right: [0,arrowWidth],
  28. })"
  29. :innerClass="['nana-bubble-box-popup',position,classNames]"
  30. shadow="light"
  31. v-bind="innerProps"
  32. :innerStyle="innerStyle"
  33. >
  34. <view
  35. class="nana-bubble-box-arrow"
  36. :style="{
  37. borderWidth: theme.resolveThemeSize(arrowWidth),
  38. borderColor: backgroundColor,
  39. borderRightColor: 'transparent',
  40. borderBottomColor: 'transparent',
  41. borderLeftColor: 'transparent',
  42. }"
  43. />
  44. <slot name="content">
  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. </slot>
  69. </FlexView>
  70. </template>
  71. </SimpleTransition>
  72. </view>
  73. </template>
  74. <script setup lang="ts">
  75. import { computed, ref } from 'vue';
  76. import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
  77. import { selectObjectByType, selectStyleType } from '../theme/ThemeTools';
  78. import type { FlexProps } from '../layout/FlexView.vue';
  79. import type { TextProps } from '../basic/Text.vue';
  80. import Icon, { type IconProps } from '../basic/Icon.vue';
  81. import FlexView from '../layout/FlexView.vue';
  82. import Text from '../basic/Text.vue';
  83. import Touchable from './Touchable.vue';
  84. import SimpleTransition from '../anim/SimpleTransition.vue';
  85. export interface BubbleBoxItem {
  86. text: string,
  87. textColor?: string,
  88. icon?: string,
  89. iconProps?: IconProps,
  90. onClick: () => void,
  91. }
  92. export interface BubbleBoxProps {
  93. /**
  94. * 气泡框位置
  95. * @default top
  96. */
  97. position?: 'left' | 'right' | 'top' | 'bottom',
  98. /**
  99. * 气泡在横轴上的对齐位置,默认居中
  100. * @default center
  101. */
  102. crossPosition?: 'left' | 'center' | 'right',
  103. /**
  104. * 触发点击事件模式
  105. * @default click
  106. */
  107. trigger?: 'click'|'hover'|'none',
  108. /**
  109. * 气泡框按钮排列方向
  110. * @default column
  111. */
  112. direction?: 'column' | 'row',
  113. /**
  114. * 是否禁用
  115. * @default false
  116. */
  117. disabled?: boolean,
  118. /**
  119. * 气泡框按钮数组
  120. */
  121. items?: BubbleBoxItem[],
  122. /**
  123. * 气泡框按钮文本颜色
  124. * @default 'text.content'
  125. */
  126. itemTextColor?: string,
  127. /**
  128. * 气泡框按钮文本样式
  129. */
  130. itemTextProps?: TextProps,
  131. /**
  132. * 气泡框按钮图标样式
  133. */
  134. itemIconProps?: IconProps,
  135. /**
  136. * 气泡框按钮样式
  137. */
  138. itemProps?: FlexProps,
  139. /**
  140. * 气泡框外层容器样式
  141. */
  142. outerStyle?: Record<string, string>,
  143. /**
  144. * 气泡框背景颜色
  145. * @default white
  146. */
  147. backgroundColor?: string,
  148. /**
  149. * 气泡框箭头宽度
  150. * @default 12
  151. */
  152. arrowWidth?: number,
  153. /**
  154. * 气泡框圆角半径
  155. * @default 12
  156. */
  157. radius?: number,
  158. /**
  159. * 气泡框内层容器样式
  160. */
  161. innerProps?: FlexProps,
  162. /**
  163. * 气泡框内层容器样式
  164. */
  165. innerStyle?: Record<string, string>,
  166. }
  167. export interface BubbleBoxExpose {
  168. show: () => void,
  169. hide: () => void,
  170. }
  171. const theme = useTheme();
  172. const props = withDefaults(defineProps<BubbleBoxProps>(), {
  173. position: 'top',
  174. trigger: 'click',
  175. direction: 'column',
  176. arrowWidth: () => propGetThemeVar('BubbleBoxArrowWidth', 12),
  177. items: () => [],
  178. itemTextColor: () => propGetThemeVar('BubbleBoxItemTextColor', 'text.content'),
  179. backgroundColor: () => propGetThemeVar('BubbleBoxBackgroundColor', 'white'),
  180. radius: () => propGetThemeVar('BubbleBoxRadius', 12),
  181. });
  182. const backgroundColor = computed(() => theme.resolveThemeColor(props.backgroundColor));
  183. const showState = ref(false);
  184. const lock = ref(false);
  185. function handleItemClick(item: BubbleBoxItem) {
  186. hide();
  187. item.onClick();
  188. }
  189. function handleClick() {
  190. if (lock.value) return;
  191. if (props.trigger === 'click' && !props.disabled) {
  192. enterLock();
  193. showState.value = !showState.value;
  194. }
  195. }
  196. function handleHover(show: boolean) {
  197. if (lock.value) return;
  198. if (props.trigger === 'hover' && !props.disabled)
  199. showState.value = show;
  200. }
  201. function enterLock() {
  202. lock.value = true;
  203. setTimeout(() => {
  204. lock.value = false;
  205. }, 300);
  206. }
  207. function show() {
  208. showState.value = true;
  209. enterLock();
  210. }
  211. function hide() {
  212. showState.value = false;
  213. enterLock();
  214. }
  215. const innerStyle = computed(() => {
  216. const horzLayout = selectStyleType(props.crossPosition, 'left', {
  217. left: {
  218. k: 'top',
  219. y: '0%',
  220. t: 'translateY(0%)',
  221. },
  222. center: {
  223. k: 'top',
  224. y: '50%',
  225. t: 'translateY(-50%)',
  226. },
  227. right: {
  228. k: 'bottom',
  229. y: '0',
  230. t: 'translateY(0%)',
  231. }
  232. });
  233. const vertLayout = selectStyleType(props.crossPosition, 'left', {
  234. left: {
  235. k: 'left',
  236. x: '0%',
  237. t: 'translateX(0%)',
  238. },
  239. center: {
  240. k: 'left',
  241. x: '50%',
  242. t: 'translateX(-50%)',
  243. },
  244. right: {
  245. k: 'right',
  246. x: '0%',
  247. t: 'translateX(0%)',
  248. }
  249. });
  250. return {
  251. ...props.innerStyle,
  252. ...selectStyleType(props.position, 'top', {
  253. left: {
  254. [horzLayout.k]: horzLayout.y,
  255. right: '100%',
  256. transform: horzLayout.t + ' translateX(0)',
  257. },
  258. right: {
  259. [horzLayout.k]: horzLayout.y,
  260. left: '100%',
  261. transform: horzLayout.t + '',
  262. },
  263. top: {
  264. top: '0%',
  265. [vertLayout.k]: vertLayout.x,
  266. transform: vertLayout.t + ' translateY(-100%)',
  267. },
  268. bottom: {
  269. bottom: '0%',
  270. [vertLayout.k]: vertLayout.x,
  271. transform: vertLayout.t + ' translateY(100%)',
  272. }
  273. })
  274. }
  275. })
  276. defineExpose<BubbleBoxExpose>({
  277. show,
  278. hide,
  279. })
  280. defineOptions({
  281. options: {
  282. virtualHost: true,
  283. styleIsolation: "shared",
  284. }
  285. })
  286. </script>
  287. <style lang="scss">
  288. .nana-bubble-box {
  289. position: relative;
  290. overflow: visible;
  291. .nana-bubble-box-popup {
  292. position: absolute;
  293. transition: opacity ease-in-out 0.2s;
  294. &.left {
  295. .nana-bubble-box-arrow {
  296. top: 50%;
  297. left: 100%;
  298. transform: translateY(-50%) rotate(-90deg);
  299. }
  300. }
  301. &.right {
  302. .nana-bubble-box-arrow {
  303. top: 50%;
  304. left: 0;
  305. transform: translateX(-100%) translateY(-50%) rotate(90deg);
  306. }
  307. }
  308. &.top {
  309. .nana-bubble-box-arrow {
  310. top: 100%;
  311. left: 50%;
  312. transform: translateX(-50%) rotate(0);
  313. }
  314. }
  315. &.bottom {
  316. .nana-bubble-box-arrow {
  317. top: 0;
  318. left: 50%;
  319. transform: translateX(-50%) translateY(-100%) rotate(180deg);
  320. }
  321. }
  322. &.bubble-box-enter-active,
  323. &.bubble-box-leave-active {
  324. opacity: 1;
  325. }
  326. &.bubble-box-enter-from,
  327. &.bubble-box-leave-to {
  328. opacity: 0;
  329. }
  330. }
  331. .nana-bubble-box-popup-mask {
  332. position: fixed;
  333. top: 0;
  334. left: 0;
  335. width: 100%;
  336. height: 100%;
  337. z-index: 1000;
  338. }
  339. .nana-bubble-box-arrow {
  340. position: absolute;
  341. width: 0;
  342. height: 0;
  343. border-style: solid;
  344. }
  345. }
  346. </style>