Toast.vue 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. <template>
  2. <view
  3. v-if="renderState"
  4. class="nana-toast-container"
  5. :style="{
  6. ...selectStyleType<ViewStyle, IToastPosition>(position, 'center', {
  7. center: {
  8. justifyContent: 'center',
  9. },
  10. top: {
  11. justifyContent: 'flex-start',
  12. paddingTop: theme.resolveThemeSize('ToastMarginWhenTop', 200),
  13. },
  14. bottom: {
  15. justifyContent: 'flex-end',
  16. paddingBottom: theme.resolveThemeSize('ToastMarginWhenBottom', 200),
  17. },
  18. }),
  19. pointerEvents: forbidClick ? 'auto' : 'none',
  20. ...maskStyle,
  21. }"
  22. >
  23. <view
  24. :class="[
  25. 'nana-toast',
  26. showState ? 'show' : 'hide',
  27. ]"
  28. :style="{
  29. pointerEvents: forbidClick ? 'auto' : 'none',
  30. ...themeStyles.toastStyle.value,
  31. ...toastStyle,
  32. }"
  33. @click="handleClick"
  34. >
  35. <ActivityIndicator
  36. v-if="showProps.type === 'loading'"
  37. :color="textStyle?.color as string || themeStyles.toastTextStyle.value.color"
  38. :size="themeStyles.toastIconStyle.value.size"
  39. />
  40. <Icon
  41. v-else-if="showProps.type !== 'text' || showProps.icon"
  42. :icon="showProps.icon || iconType[showProps.type!]"
  43. :color="textStyle?.color as string || themeStyles.toastTextStyle.value.color"
  44. :size="themeStyles.toastIconStyle.value.size"
  45. v-bind="iconProps"
  46. />
  47. <Height v-if="showProps.type !== 'text'" :size="themeStyles.toastIconStyle.value.marginBottom" />
  48. <slot name="content">
  49. <text :style="{
  50. ...themeStyles.toastTextStyle.value,
  51. ...textStyle
  52. }">{{ showProps.content }}</text>
  53. </slot>
  54. </view>
  55. </view>
  56. </template>
  57. <script setup lang="ts">
  58. import { nextTick, ref } from 'vue';
  59. import ActivityIndicator from '../basic/ActivityIndicator.vue';
  60. import type { IconProps } from '../basic/Icon.vue';
  61. import { useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
  62. import { DynamicColor, DynamicSize, DynamicSize2, DynamicVar, selectStyleType } from '../theme/ThemeTools';
  63. import Icon from '../basic/Icon.vue';
  64. import Height from '../layout/space/Height.vue';
  65. export type IToastPosition = 'top'|'bottom'|'center';
  66. export type IToastType = 'text'|'loading'|'success'|'fail'|'info'|'offline';
  67. export interface ToastProps {
  68. /**
  69. * 图标自定义属性
  70. */
  71. iconProps?: IconProps;
  72. /**
  73. * 提示显示位置
  74. */
  75. position?: IToastPosition;
  76. /**
  77. * 是否禁止背景点击
  78. */
  79. forbidClick?: boolean;
  80. /**
  81. * 是否在点击后关闭
  82. */
  83. closeOnClick?: boolean;
  84. /**
  85. * 土司背景的自定义样式
  86. */
  87. maskStyle?: ViewStyle;
  88. /**
  89. * 土司提示容器的自定义样式
  90. */
  91. toastStyle?: ViewStyle;
  92. /**
  93. * 土司提示文字的自定义样式
  94. */
  95. textStyle?: TextStyle;
  96. }
  97. export interface ToastShowProps {
  98. /**
  99. * 自动关闭的延时,单位ms。
  100. * * 0:根据内容长度自动设置关闭时间,最小3000ms,最大15000ms
  101. * * -1:一直显示,直到手动关闭
  102. */
  103. duration?: number;
  104. /**
  105. * 图标类型
  106. */
  107. type?: IToastType;
  108. /**
  109. * 自定义图标
  110. */
  111. icon?: string;
  112. /**
  113. * 土司内容
  114. */
  115. content?: string;
  116. /**
  117. * 关闭后回调
  118. */
  119. onClose?: () => void;
  120. }
  121. const props = withDefaults(defineProps<ToastProps>(), {
  122. position: 'center',
  123. forbidClick: false,
  124. closeOnClick: true,
  125. })
  126. const theme = useTheme();
  127. const iconType: {
  128. [key: string]: string
  129. } = {
  130. success: theme.getVar('ToastIconSuccess', 'success'),
  131. fail: theme.getVar('ToastIconError', 'error'),
  132. offline: theme.getVar('ToastIconOffline', 'cry'),
  133. info: theme.getVar('ToastIconInfo', 'prompt'),
  134. };
  135. const themeStyles = theme.useThemeStyles({
  136. toastStyle: {
  137. backgroundColor: DynamicColor('ToastBackgroundColor', 'background.toast'),
  138. padding: DynamicSize2('ToastPaddingVertical', 'ToastPaddingHorizontal', 25, 30),
  139. borderRadius: DynamicSize('ToastRadius', 16),
  140. },
  141. toastTextStyle: {
  142. color: DynamicColor('ToastTextColor', 'white'),
  143. },
  144. toastIconStyle: {
  145. size: DynamicVar('ToastIconSize', 85),
  146. marginBottom: DynamicSize('ToastIconMarginBottom', 12),
  147. },
  148. })
  149. const renderState = ref(false);
  150. const showState = ref(false);
  151. const showProps = ref<ToastShowProps>({
  152. duration: 0,
  153. type: 'text',
  154. icon: '',
  155. content: '',
  156. onClose: () => {},
  157. });
  158. let showTimer = 0;
  159. function show(options: ToastShowProps|string) {
  160. if (typeof options === 'string')
  161. options = { content: options };
  162. const {
  163. duration = 0,
  164. type = 'text',
  165. icon = '',
  166. content = '',
  167. onClose = () => {},
  168. } = options;
  169. showState.value = false;
  170. renderState.value = true;
  171. showProps.value = {
  172. duration,
  173. type,
  174. icon,
  175. content,
  176. onClose,
  177. };
  178. if (showTimer)
  179. clearTimeout(showTimer);
  180. setTimeout(() => showState.value = true, 10);
  181. let duration2 = duration;
  182. if (duration2 === 0)
  183. duration2 = Math.min(Math.max((content.length / 10) * 1000, 3000), 15000);
  184. if (duration2 > 0)
  185. showTimer = setTimeout(hide, duration2) as unknown as number;
  186. }
  187. function showWithType(type: IToastType, options: ToastShowProps|string) {
  188. show({
  189. type,
  190. content: typeof options === 'string' ? options : options.content,
  191. ...(typeof options === 'object' ? options : {}),
  192. })
  193. }
  194. function updateProps(options?: ToastShowProps) {
  195. showProps.value = {
  196. ...showProps.value,
  197. ...options,
  198. }
  199. }
  200. function hide() {
  201. if (showTimer)
  202. clearTimeout(showTimer);
  203. showProps.value.onClose?.();
  204. showState.value = false;
  205. showTimer = 0;
  206. showTimer = setTimeout(() => {
  207. renderState.value = false;
  208. showTimer = 0;
  209. }, 500) as unknown as number;
  210. }
  211. function handleClick() {
  212. if (props.closeOnClick)
  213. hide();
  214. }
  215. export interface ToastInstance {
  216. show(options: ToastShowProps|string) : void;
  217. info(options?: ToastShowProps|string): void;
  218. success(options?: ToastShowProps|string): void;
  219. fail(options?: ToastShowProps|string): void;
  220. offline(options?: ToastShowProps|string): void;
  221. loading(options?: ToastShowProps|string): void;
  222. text(options?: ToastShowProps|string): void;
  223. hide(): void;
  224. close(): void;
  225. updateProps(options?: ToastShowProps): void;
  226. }
  227. defineExpose<ToastInstance>({
  228. show,
  229. updateProps,
  230. info(options?: ToastShowProps|string) { showWithType('info', options as ToastShowProps); },
  231. success(options?: ToastShowProps|string) { showWithType('success', options as ToastShowProps); },
  232. fail(options?: ToastShowProps|string) { showWithType('fail', options as ToastShowProps); },
  233. offline(options?: ToastShowProps|string) { showWithType('offline', options as ToastShowProps); },
  234. loading(options?: ToastShowProps|string) { showWithType('loading', options as ToastShowProps); },
  235. text(options?: ToastShowProps|string) { showWithType('text', options as ToastShowProps); },
  236. hide,
  237. close: hide,
  238. })
  239. </script>
  240. <style lang="scss">
  241. .nana-toast-container {
  242. position: fixed;
  243. left: 0;
  244. right: 0;
  245. top: 0;
  246. bottom: 0;
  247. display: flex;
  248. flex-direction: column;
  249. align-items: center;
  250. justify-content: center;
  251. z-index: 140;
  252. .nana-toast {
  253. display: flex;
  254. flex-direction: column;
  255. align-items: center;
  256. justify-content: center;
  257. opacity: 0;
  258. transition: opacity ease-in-out 0.3s;
  259. &.show {
  260. opacity: 1;
  261. }
  262. }
  263. }
  264. </style>