Badge.vue 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. <template>
  2. <view
  3. :class="['nana-badge', standalone ? 'standalone' : '']"
  4. :style="containerStyle"
  5. >
  6. <slot />
  7. <SimpleTransition name="badge" :show="showBadge" :duration="3000">
  8. <template #show="{ classNames }">
  9. <view :class="['nana-badge-inner',...classNames]" :style="currentStyle">
  10. <VerticalScrollText v-if="anim" center :innerStyle="textStyle" :numberString="content2" />
  11. <Text v-else :innerStyle="textStyle" :text="content2" />
  12. </view>
  13. </template>
  14. </SimpleTransition>
  15. </view>
  16. </template>
  17. <script setup lang="ts">
  18. import { computed } from 'vue';
  19. import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
  20. import { selectStyleType } from '../theme/ThemeTools';
  21. import VerticalScrollText from '../typography/VerticalScrollText.vue';
  22. import Text from '../basic/Text.vue';
  23. import SimpleTransition from '../anim/SimpleTransition.vue';
  24. type BadgePositionTypes = 'topRight'|'topLeft'|'bottomRight'|'bottomLeft';
  25. export interface BadgeProps {
  26. /**
  27. * 控制是否显示
  28. * @default true
  29. */
  30. show?: boolean,
  31. /**
  32. * 独立显示
  33. */
  34. standalone?: boolean;
  35. /**
  36. * 徽标内容
  37. */
  38. content?: string|number,
  39. /**
  40. * 徽标数字最大值,如果徽标内容是数字,并且超过最大值,则显示 xx+。
  41. */
  42. maxCount?: number,
  43. /**
  44. * 徽标颜色
  45. * @default danger
  46. */
  47. color?: string,
  48. /**
  49. * 是否增加描边
  50. * @default false
  51. */
  52. border?: boolean;
  53. /**
  54. * 描边宽度
  55. * @default 2
  56. */
  57. borderWidth?: number;
  58. /**
  59. * 描边颜色
  60. * @default white
  61. */
  62. borderColor?: string;
  63. /**
  64. * 徽标在父级所处位置
  65. * @default 'top-right'
  66. */
  67. position?: BadgePositionTypes;
  68. /**
  69. * 是否在切换时有动画效果
  70. * @default false
  71. */
  72. anim?: boolean,
  73. /**
  74. * 徽标定位偏移
  75. */
  76. offset?: { x: number, y: number };
  77. /**
  78. * 徽标自定义样式
  79. */
  80. badgeStyle?: ViewStyle;
  81. /**
  82. * 徽标文字自定义样式
  83. */
  84. textStyle?: TextStyle;
  85. /**
  86. * 外层容器样式
  87. */
  88. containerStyle?: ViewStyle;
  89. /**
  90. * 徽标无文字情况下的徽标大小
  91. * @default 10
  92. */
  93. badgeSize?: number;
  94. /**
  95. * 字号
  96. * @default 12.5
  97. */
  98. fontSize?: number,
  99. /**
  100. * 圆角的大小
  101. * @default 20
  102. */
  103. radius?: number;
  104. /**
  105. * 徽标有文字情况下的垂直内边距
  106. * @default 2
  107. */
  108. paddingVertical?: number,
  109. /**
  110. * 徽标有文字情况下的水平内边距
  111. * @default 4
  112. */
  113. paddingHorizontal?: number,
  114. /**
  115. * 如果 content===0 是否隐藏红点
  116. * @default true
  117. */
  118. hiddenIfZero?: boolean;
  119. }
  120. const themeContext = useTheme();
  121. const props = withDefaults(defineProps<BadgeProps>(), {
  122. color: 'danger',
  123. show: true,
  124. anim: () => propGetThemeVar('BadgeAnim', false),
  125. content: '',
  126. border: () => propGetThemeVar('BadgeBorder', false),
  127. borderWidth: () => propGetThemeVar('BadgeBorderWidth', 4),
  128. borderColor: () => propGetThemeVar('BadgeBorderColor', 'white'),
  129. position: () => propGetThemeVar('BadgePosition', 'topRight'),
  130. offset: () => propGetThemeVar('BadgeOffset', { x: 0, y: 0 }),
  131. badgeSize: () => propGetThemeVar('BadgeSize', 18),
  132. radius: () => propGetThemeVar('BadgeRadius', 20),
  133. fontSize: () => propGetThemeVar('BadgeFontSize', 24),
  134. paddingVertical: () => propGetThemeVar('BadgePaddingVertical', 4),
  135. paddingHorizontal: () => propGetThemeVar('BadgePaddingHorizontal', 8),
  136. hiddenIfZero: true,
  137. });
  138. //样式生成
  139. const currentStyle = computed(() => {
  140. const size = themeContext.resolveThemeSize(props.badgeSize);
  141. return {
  142. backgroundColor: themeContext.resolveThemeColor(props.color),
  143. borderRadius: themeContext.resolveThemeSize(props.radius),
  144. border: props.border ? `${themeContext.resolveThemeSize(props.borderWidth)} solid ${themeContext.resolveThemeColor(props.borderColor)}` : undefined,
  145. minWidth: props.content ? themeContext.resolveThemeSize(props.fontSize) : undefined,
  146. padding: `${themeContext.resolveThemeSize(props.paddingVertical)} ${themeContext.resolveThemeSize(props.paddingHorizontal)}`,
  147. ...props.badgeStyle,
  148. ...(props.standalone ? {} : selectStyleType<TextStyle, BadgePositionTypes>(props.position, 'topRight', {
  149. topRight: {
  150. transform: `translate(50%, -50%)`,
  151. top: themeContext.resolveSize(props.offset.y),
  152. right: themeContext.resolveSize(props.offset.x),
  153. },
  154. topLeft: {
  155. transform: `translate(-50%, -50%)`,
  156. top: themeContext.resolveSize(props.offset.y),
  157. left: themeContext.resolveSize(props.offset.x),
  158. },
  159. bottomRight: {
  160. transform: `translate(50%, 50%)`,
  161. bottom: themeContext.resolveSize(props.offset.y),
  162. right: themeContext.resolveSize(props.offset.x),
  163. },
  164. bottomLeft: {
  165. transform: `translate(-50%, 50%)`,
  166. bottom: themeContext.resolveSize(props.offset.y),
  167. left: themeContext.resolveSize(props.offset.x),
  168. },
  169. })),
  170. ...(
  171. props.content ? {} : {
  172. width: size,
  173. height: size,
  174. borderRadius: '50%',
  175. padding: undefined,
  176. }
  177. ),
  178. }
  179. });
  180. const textStyle = computed(() => {
  181. const fontSize = themeContext.resolveThemeSize(props.fontSize, 24);
  182. return {
  183. fontSize: fontSize,
  184. color: themeContext.resolveThemeColor('BadgeTextColor', 'white'),
  185. textAlign: 'center',
  186. ...props.textStyle,
  187. }
  188. })
  189. const content2 = computed(() => {
  190. if (typeof props.content !== 'undefined') {
  191. if (typeof props.content === 'number') {
  192. return (props.maxCount && props.content > props.maxCount) ? `${props.maxCount}+` : props.content.toString();
  193. } else
  194. return props.content;
  195. }
  196. return '';
  197. });
  198. const showBadge = computed(() =>
  199. props.show && (!props.hiddenIfZero ||
  200. (props.hiddenIfZero && props.content !== 0 && props.content !== '0'))
  201. );
  202. </script>
  203. <style lang="scss">
  204. .nana-badge {
  205. position: relative;
  206. display: inline-flex;
  207. width: auto;
  208. height: auto;
  209. overflow: visible;
  210. .badge-enter-active,
  211. .badge-leave-active {
  212. opacity: 1;
  213. transform: scale(1);
  214. transition: all 0.3s ease-in-out;
  215. }
  216. .badge-enter-from,
  217. .badge-leave-to {
  218. transform: scale(0);
  219. opacity: 0;
  220. }
  221. &.standalone {
  222. position: relative;
  223. .nana-badge-inner {
  224. position: relative;
  225. z-index: 100;
  226. }
  227. }
  228. }
  229. .nana-badge-inner {
  230. position: absolute;
  231. z-index: 100;
  232. display: flex;
  233. justify-content: center;
  234. align-items: center;
  235. text-align: center;
  236. align-self: flex-start;
  237. overflow: hidden;
  238. flex-shrink: 0;
  239. }
  240. </style>