Notify.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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, ArrayUtils } from '@imengyu/imengyu-utils';
  93. import { useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
  94. import { DynamicColor, DynamicSize, DynamicSize2, DynamicVar, selectStyleType } from '../theme/ThemeTools';
  95. import type { IconProps } from '../basic/Icon.vue';
  96. import ActivityIndicator from '../basic/ActivityIndicator.vue';
  97. import Icon from '../basic/Icon.vue';
  98. import Height from '../layout/space/Height.vue';
  99. import type { ButtonProp } from '../basic/Button.vue';
  100. import Button from '../basic/Button.vue';
  101. export type INotifyPosition = 'top'|'bottom'|'center';
  102. export type INotifyType = 'text'|'loading'|'success'|'fail'|'info'|'offline';
  103. export interface NotifyProps {
  104. /**
  105. * 提示显示位置
  106. * @default 'top'
  107. */
  108. position?: INotifyPosition;
  109. }
  110. export interface NotifyShowProps {
  111. /**
  112. * 用于自定义渲染
  113. */
  114. id?: number,
  115. /**
  116. * 用于自定义渲染
  117. */
  118. tag?: string,
  119. /**
  120. * 自动关闭的延时,单位ms。为0不会自动关闭
  121. * @default 4000
  122. */
  123. duration?: number;
  124. /**
  125. * 图标类型
  126. */
  127. type?: INotifyType;
  128. /**
  129. * 自定义图标
  130. */
  131. icon?: string;
  132. /**
  133. * 内容
  134. */
  135. content?: string;
  136. /**
  137. * 按钮文字
  138. */
  139. button?: string;
  140. /**
  141. * 按钮属性
  142. */
  143. buttonProps?: ButtonProp;
  144. /**
  145. * 图标自定义属性
  146. */
  147. iconProps?: IconProps;
  148. /**
  149. * 是否在点击后关闭
  150. * @default false
  151. */
  152. closeOnClick?: boolean;
  153. /**
  154. * 提示容器的自定义样式
  155. */
  156. notifyStyle?: ViewStyle;
  157. /**
  158. * 提示文字的自定义样式
  159. */
  160. textStyle?: TextStyle;
  161. /**
  162. * 按钮点击回调
  163. */
  164. onButtonClick?: (e: any) => void;
  165. /**
  166. * 关闭后回调
  167. */
  168. onClose?: () => void;
  169. /**
  170. * 点击回调
  171. */
  172. onClick?: () => void;
  173. }
  174. const props = withDefaults(defineProps<NotifyProps>(), {
  175. position: 'top',
  176. forbidClick: false,
  177. closeOnClick: false,
  178. })
  179. const theme = useTheme();
  180. const iconType: {
  181. [key: string]: string
  182. } = {
  183. success: theme.getVar('NotifyIconSuccess', 'success'),
  184. fail: theme.getVar('NotifyIconError', 'error'),
  185. offline: theme.getVar('NotifyIconOffline', 'cry'),
  186. info: theme.getVar('NotifyIconInfo', 'prompt'),
  187. };
  188. const themeStyles = theme.useThemeStyles({
  189. notifyContainerStyle: {
  190. gap: DynamicSize('NotifyContainerGap', 15),
  191. },
  192. notifyStyle: {
  193. backgroundColor: DynamicColor('NotifyBackgroundColor', 'background.notify'),
  194. padding: DynamicSize2('NotifyPaddingVertical', 'NotifyPaddingHorizontal', 25, 30),
  195. borderRadius: DynamicSize('NotifyRadius', 16),
  196. boxShadow: '0 0 10px 0 rgba(0, 0, 0, 0.05)',
  197. gap: DynamicSize('NotifyGap', 21),
  198. },
  199. notifyTextStyle: {
  200. color: DynamicColor('NotifyTextColor', 'text.content'),
  201. },
  202. notifyIconStyle: {
  203. size: DynamicVar('NotifyIconSize', 45),
  204. marginBottom: DynamicSize('NotifyIconMarginBottom', 12),
  205. },
  206. })
  207. interface NotifyItemData extends NotifyShowProps {
  208. id: number;
  209. showState: boolean;
  210. showTimer: SimpleDelay,
  211. hideTimer: SimpleDelay,
  212. updateProps(options: NotifyShowProps): void;
  213. close() : void;
  214. }
  215. const items = ref<NotifyItemData[]>([]);
  216. let ids = 0;
  217. function show(options: NotifyShowProps) {
  218. const {
  219. duration = 4000,
  220. type = 'text',
  221. } = options;
  222. const item = reactive<NotifyItemData>({
  223. ...options,
  224. id: options.id ?? ids++,
  225. duration,
  226. type,
  227. updateProps(options: NotifyShowProps) {
  228. for (const key in options) {
  229. const v = (options as any)[key];
  230. if (v !== undefined)
  231. (this as any)[key] = v;
  232. }
  233. },
  234. close() {
  235. this.showState = false;
  236. this.hideTimer.start();
  237. this.onClose?.();
  238. },
  239. showState: false,
  240. showTimer: new SimpleDelay(undefined, () => {
  241. item.close()
  242. }, duration),
  243. hideTimer: new SimpleDelay(undefined, () => {
  244. ArrayUtils.remove(items.value, item);
  245. }, 300),
  246. });
  247. if (duration > 0)
  248. item.showTimer.start();
  249. items.value.push(item);
  250. setTimeout(() => item.showState = true, 20);
  251. return item;
  252. }
  253. function itemClick(item: NotifyItemData) {
  254. item.onClick?.();
  255. if (props.closeOnClick)
  256. item.close();
  257. }
  258. function closeAll() {
  259. ArrayUtils.clear(items.value);
  260. }
  261. export interface NotifyItemInstance {
  262. updateProps(options?: Omit<NotifyShowProps, 'duration'|'onClose'>): void;
  263. close() : void;
  264. }
  265. export interface NotifyInstance {
  266. show(options: NotifyShowProps) : NotifyItemInstance;
  267. closeAll(): void;
  268. }
  269. defineExpose<NotifyInstance>({
  270. show,
  271. closeAll,
  272. })
  273. </script>
  274. <style lang="scss">
  275. .nana-notify-container {
  276. position: fixed;
  277. left: 0;
  278. right: 0;
  279. top: 0;
  280. bottom: 0;
  281. display: flex;
  282. flex-direction: column;
  283. align-items: center;
  284. justify-content: center;
  285. z-index: 100;
  286. .nana-notify {
  287. display: flex;
  288. flex-direction: row;
  289. align-items: center;
  290. justify-content: center;
  291. opacity: 0;
  292. transition: opacity ease-in-out 0.3s;
  293. &.show {
  294. opacity: 1;
  295. }
  296. }
  297. }
  298. </style>