Toast.vue 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  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) {
  160. const {
  161. duration = 0,
  162. type = 'text',
  163. icon = '',
  164. content = '',
  165. onClose = () => {},
  166. } = options;
  167. showState.value = false;
  168. renderState.value = true;
  169. showProps.value = {
  170. duration,
  171. type,
  172. icon,
  173. content,
  174. onClose,
  175. };
  176. if (showTimer)
  177. clearTimeout(showTimer);
  178. setTimeout(() => showState.value = true, 10);
  179. let duration2 = duration;
  180. if (duration2 === 0)
  181. duration2 = Math.min(Math.max((content.length / 10) * 1000, 3000), 15000);
  182. if (duration2 > 0)
  183. showTimer = setTimeout(hide, duration2) as unknown as number;
  184. }
  185. function showWithType(type: IToastType, options: ToastShowProps|string) {
  186. show({
  187. type,
  188. content: typeof options === 'string' ? options : options.content,
  189. ...(typeof options === 'object' ? options : {}),
  190. })
  191. }
  192. function updateProps(options?: ToastShowProps) {
  193. showProps.value = {
  194. ...showProps.value,
  195. ...options,
  196. }
  197. }
  198. function hide() {
  199. if (showTimer)
  200. clearTimeout(showTimer);
  201. showProps.value.onClose?.();
  202. showState.value = false;
  203. showTimer = 0;
  204. showTimer = setTimeout(() => {
  205. renderState.value = false;
  206. showTimer = 0;
  207. }, 500) as unknown as number;
  208. }
  209. function handleClick() {
  210. if (props.closeOnClick)
  211. hide();
  212. }
  213. export interface ToastInstance {
  214. show(options: ToastShowProps) : void;
  215. info(options?: ToastShowProps|string): void;
  216. success(options?: ToastShowProps|string): void;
  217. fail(options?: ToastShowProps|string): void;
  218. offline(options?: ToastShowProps|string): void;
  219. loading(options?: ToastShowProps|string): void;
  220. text(options?: ToastShowProps|string): void;
  221. hide(): void;
  222. close(): void;
  223. updateProps(options?: ToastShowProps): void;
  224. }
  225. defineExpose<ToastInstance>({
  226. show,
  227. updateProps,
  228. info(options?: ToastShowProps|string) { showWithType('info', options as ToastShowProps); },
  229. success(options?: ToastShowProps|string) { showWithType('success', options as ToastShowProps); },
  230. fail(options?: ToastShowProps|string) { showWithType('fail', options as ToastShowProps); },
  231. offline(options?: ToastShowProps|string) { showWithType('offline', options as ToastShowProps); },
  232. loading(options?: ToastShowProps|string) { showWithType('loading', options as ToastShowProps); },
  233. text(options?: ToastShowProps|string) { showWithType('text', options as ToastShowProps); },
  234. hide,
  235. close: hide,
  236. })
  237. </script>
  238. <style lang="scss">
  239. .nana-toast-container {
  240. position: fixed;
  241. left: 0;
  242. right: 0;
  243. top: 0;
  244. bottom: 0;
  245. display: flex;
  246. flex-direction: column;
  247. align-items: center;
  248. justify-content: center;
  249. z-index: 140;
  250. .nana-toast {
  251. display: flex;
  252. flex-direction: column;
  253. align-items: center;
  254. justify-content: center;
  255. opacity: 0;
  256. transition: opacity ease-in-out 0.3s;
  257. &.show {
  258. opacity: 1;
  259. }
  260. }
  261. }
  262. </style>