Notify.vue 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. <template>
  2. <view
  3. class="nana-notify-container"
  4. :style="{
  5. ...selectStyleType<ViewStyle, INotifyPosition>(position, 'center', {
  6. center: {
  7. justifyContent: 'center',
  8. },
  9. top: {
  10. justifyContent: 'flex-start',
  11. paddingTop: theme.resolveThemeSize('NotifyMarginWhenTop', 100),
  12. },
  13. bottom: {
  14. justifyContent: 'flex-end',
  15. paddingBottom: theme.resolveThemeSize('NotifyMarginWhenBottom', 100),
  16. },
  17. }),
  18. pointerEvents: 'none',
  19. ...themeStyles.notifyContainerStyle.value,
  20. }"
  21. >
  22. <view
  23. v-for="item in items"
  24. :key="item.id"
  25. :class="[
  26. 'nana-notify',
  27. item.showState ? 'show' : 'hide',
  28. ]"
  29. :style="{
  30. pointerEvents: 'auto',
  31. ...themeStyles.notifyStyle.value,
  32. ...selectStyleType(item.type, 'text', {
  33. text: {},
  34. loading: {},
  35. success: {
  36. backgroundColor: theme.resolveThemeColor('NotifySuccessBackgroundColor', 'background.success'),
  37. },
  38. fail: {
  39. backgroundColor: theme.resolveThemeColor('NotifyFailBackgroundColor', 'background.danger'),
  40. },
  41. info: {
  42. backgroundColor: theme.resolveThemeColor('NotifyInfoBackgroundColor', 'background.info'),
  43. },
  44. offline: {
  45. backgroundColor: theme.resolveThemeColor('NotifyOfflineBackgroundColor', 'background.warning'),
  46. },
  47. }),
  48. ...item.notifyStyle,
  49. }"
  50. @click="itemClick(item as NotifyItemData)"
  51. >
  52. <slot name="item"
  53. :id="item.id"
  54. :item="item"
  55. :tag="item.tag"
  56. >
  57. <ActivityIndicator
  58. v-if="item.type === 'loading'"
  59. :color="item.textStyle?.color as string || themeStyles.notifyTextStyle.value.color"
  60. :size="themeStyles.notifyIconStyle.value.size"
  61. />
  62. <Icon
  63. v-else-if="item.type !== 'text' || item.icon"
  64. :icon="item.icon ?? iconType[item.type!]"
  65. :color="item.textStyle?.color as string || themeStyles.notifyTextStyle.value.color"
  66. :size="themeStyles.notifyIconStyle.value.size"
  67. v-bind="item.iconProps"
  68. />
  69. <Height v-if="item.type !== 'text'" :size="themeStyles.notifyIconStyle.value.marginBottom" />
  70. <slot name="content">
  71. <text :style="{
  72. ...themeStyles.notifyTextStyle.value,
  73. ...item.textStyle
  74. }">
  75. {{ item.content }}
  76. </text>
  77. </slot>
  78. <Button
  79. v-if="item.button"
  80. type="text"
  81. size="small"
  82. :text="item.button"
  83. v-bind="item.buttonProps"
  84. @click="item.onButtonClick"
  85. />
  86. </slot>
  87. </view>
  88. </view>
  89. </template>
  90. <script setup lang="ts">
  91. import { reactive, ref } from 'vue';
  92. import { SimpleDelay } from '../utils/Timer';
  93. import { ArrayUtils } from '@imengyu/imengyu-utils';
  94. import { useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
  95. import { DynamicColor, DynamicSize, DynamicSize2, DynamicVar, selectStyleType } from '../theme/ThemeTools';
  96. import type { IconProps } from '../basic/Icon.vue';
  97. import ActivityIndicator from '../basic/ActivityIndicator.vue';
  98. import Icon from '../basic/Icon.vue';
  99. import Height from '../layout/space/Height.vue';
  100. import type { ButtonProp } from '../basic/Button.vue';
  101. import Button from '../basic/Button.vue';
  102. export type INotifyPosition = 'top'|'bottom'|'center';
  103. export type INotifyType = 'text'|'loading'|'success'|'fail'|'info'|'offline';
  104. export interface NotifyProps {
  105. /**
  106. * 提示显示位置
  107. * @default 'top'
  108. */
  109. position?: INotifyPosition;
  110. }
  111. export interface NotifyShowProps {
  112. /**
  113. * 用于自定义渲染
  114. */
  115. id?: number,
  116. /**
  117. * 用于自定义渲染
  118. */
  119. tag?: string,
  120. /**
  121. * 自动关闭的延时,单位ms。为0不会自动关闭
  122. * @default 4000
  123. */
  124. duration?: number;
  125. /**
  126. * 图标类型
  127. */
  128. type?: INotifyType;
  129. /**
  130. * 自定义图标
  131. */
  132. icon?: string;
  133. /**
  134. * 内容
  135. */
  136. content?: string;
  137. /**
  138. * 按钮文字
  139. */
  140. button?: string;
  141. /**
  142. * 按钮属性
  143. */
  144. buttonProps?: ButtonProp;
  145. /**
  146. * 图标自定义属性
  147. */
  148. iconProps?: IconProps;
  149. /**
  150. * 是否在点击后关闭
  151. * @default false
  152. */
  153. closeOnClick?: boolean;
  154. /**
  155. * 提示容器的自定义样式
  156. */
  157. notifyStyle?: ViewStyle;
  158. /**
  159. * 提示文字的自定义样式
  160. */
  161. textStyle?: TextStyle;
  162. /**
  163. * 按钮点击回调
  164. */
  165. onButtonClick?: (e: any) => void;
  166. /**
  167. * 关闭后回调
  168. */
  169. onClose?: () => void;
  170. /**
  171. * 点击回调
  172. */
  173. onClick?: () => void;
  174. }
  175. const props = withDefaults(defineProps<NotifyProps>(), {
  176. position: 'top',
  177. forbidClick: false,
  178. closeOnClick: false,
  179. })
  180. const theme = useTheme();
  181. const iconType: {
  182. [key: string]: string
  183. } = {
  184. success: theme.getVar('NotifyIconSuccess', 'success'),
  185. fail: theme.getVar('NotifyIconError', 'error'),
  186. offline: theme.getVar('NotifyIconOffline', 'cry'),
  187. info: theme.getVar('NotifyIconInfo', 'prompt'),
  188. };
  189. const themeStyles = theme.useThemeStyles({
  190. notifyContainerStyle: {
  191. gap: DynamicSize('NotifyContainerGap', 15),
  192. },
  193. notifyStyle: {
  194. backgroundColor: DynamicColor('NotifyBackgroundColor', 'background.notify'),
  195. padding: DynamicSize2('NotifyPaddingVertical', 'NotifyPaddingHorizontal', 25, 30),
  196. borderRadius: DynamicSize('NotifyRadius', 16),
  197. boxShadow: '0 0 10px 0 rgba(0, 0, 0, 0.05)',
  198. gap: DynamicSize('NotifyGap', 21),
  199. },
  200. notifyTextStyle: {
  201. color: DynamicColor('NotifyTextColor', 'text.content'),
  202. },
  203. notifyIconStyle: {
  204. size: DynamicVar('NotifyIconSize', 45),
  205. marginBottom: DynamicSize('NotifyIconMarginBottom', 12),
  206. },
  207. })
  208. interface NotifyItemData extends NotifyShowProps {
  209. id: number;
  210. showState: boolean;
  211. showTimer: SimpleDelay,
  212. hideTimer: SimpleDelay,
  213. updateProps(options: NotifyShowProps): void;
  214. close() : void;
  215. }
  216. const items = ref<NotifyItemData[]>([]);
  217. let ids = 0;
  218. function show(options: NotifyShowProps) {
  219. const {
  220. duration = 4000,
  221. type = 'text',
  222. } = options;
  223. const item = reactive<NotifyItemData>({
  224. ...options,
  225. id: options.id ?? ids++,
  226. duration,
  227. type,
  228. updateProps(options: NotifyShowProps) {
  229. for (const key in options) {
  230. const v = (options as any)[key];
  231. if (v !== undefined)
  232. (this as any)[key] = v;
  233. }
  234. },
  235. close() {
  236. this.showState = false;
  237. this.hideTimer.start();
  238. this.onClose?.();
  239. },
  240. showState: false,
  241. showTimer: new SimpleDelay(undefined, () => {
  242. item.close()
  243. }, duration),
  244. hideTimer: new SimpleDelay(undefined, () => {
  245. ArrayUtils.remove(items.value, item);
  246. }, 300),
  247. });
  248. if (duration > 0)
  249. item.showTimer.start();
  250. items.value.push(item);
  251. setTimeout(() => item.showState = true, 20);
  252. return item;
  253. }
  254. function itemClick(item: NotifyItemData) {
  255. item.onClick?.();
  256. if (props.closeOnClick)
  257. item.close();
  258. }
  259. function closeAll() {
  260. ArrayUtils.clear(items.value);
  261. }
  262. export interface NotifyItemInstance {
  263. updateProps(options?: Omit<NotifyShowProps, 'duration'|'onClose'>): void;
  264. close() : void;
  265. }
  266. export interface NotifyInstance {
  267. show(options: NotifyShowProps) : NotifyItemInstance;
  268. closeAll(): void;
  269. }
  270. defineExpose<NotifyInstance>({
  271. show,
  272. closeAll,
  273. })
  274. </script>
  275. <style lang="scss">
  276. .nana-notify-container {
  277. position: fixed;
  278. left: 0;
  279. right: 0;
  280. top: 0;
  281. bottom: 0;
  282. display: flex;
  283. flex-direction: column;
  284. align-items: center;
  285. justify-content: center;
  286. z-index: 100;
  287. .nana-notify {
  288. display: flex;
  289. flex-direction: row;
  290. align-items: center;
  291. justify-content: center;
  292. opacity: 0;
  293. transition: opacity ease-in-out 0.3s;
  294. &.show {
  295. opacity: 1;
  296. }
  297. }
  298. }
  299. </style>