From cca573e39fd1f25eeaaa7d37311166817689c268 Mon Sep 17 00:00:00 2001 From: EricWXY Date: Mon, 8 Jul 2024 08:30:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Notification=20=E5=8A=A0=E5=85=A5=20pos?= =?UTF-8?q?ition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/Message/Message.vue | 19 +++--- packages/components/Message/methods.ts | 21 ++++--- packages/components/Message/types.ts | 2 +- .../components/Notification/Notification.vue | 35 +++++++---- packages/components/Notification/methods.ts | 60 +++++++++++-------- packages/components/Notification/style.css | 20 +++++-- packages/components/Notification/types.ts | 9 +++ packages/docs/demo/notification/Basic.vue | 2 + packages/hooks/index.ts | 4 +- packages/hooks/useOffset.ts | 29 +++++++++ 10 files changed, 139 insertions(+), 62 deletions(-) create mode 100644 packages/hooks/useOffset.ts diff --git a/packages/components/Message/Message.vue b/packages/components/Message/Message.vue index 5b3dab5..1401b37 100644 --- a/packages/components/Message/Message.vue +++ b/packages/components/Message/Message.vue @@ -2,8 +2,8 @@ import type { MessageProps } from "./types"; import { computed, onMounted, ref, watch } from "vue"; import { getLastBottomOffset } from "./methods"; -import { delay } from "lodash-es"; -import { useEventListener } from "@eric-ui/hooks"; +import { delay, bind } from "lodash-es"; +import { useEventListener, useOffset } from "@eric-ui/hooks"; import { RenderVnode, typeIconMap } from "@eric-ui/utils"; import ErIcon from "../Icon/Icon.vue"; @@ -21,17 +21,16 @@ const props = withDefaults(defineProps(), { const visible = ref(false); const messageRef = ref(); +const iconName = computed(() => typeIconMap.get(props.type) ?? "circle-info"); + // div 的高度 const boxHeight = ref(0); -const iconName = computed(() => typeIconMap.get(props.type) ?? "circle-info"); - -// 上一个实例最下面的坐标,第一个是0 -const lastBottomOffset = computed(() => getLastBottomOffset(props.id)); -// 本元素应该的 top -const topOffset = computed(() => props.offset + lastBottomOffset.value); -// 为下一个实例预留的底部 offset -const bottomOffset = computed(() => boxHeight.value + topOffset.value); +const { topOffset, bottomOffset } = useOffset({ + getLastBottomOffset: bind(getLastBottomOffset, props), + offset: props.offset, + boxHeight, +}); const cssStyle = computed(() => ({ top: topOffset.value + "px", diff --git a/packages/components/Message/methods.ts b/packages/components/Message/methods.ts index 4f16a67..f6f5ef5 100644 --- a/packages/components/Message/methods.ts +++ b/packages/components/Message/methods.ts @@ -5,16 +5,15 @@ import type { Message, MessageParams, MessageHandler, + MessageProps, messageType, } from "./types"; import { messageTypes } from "./types"; import { render, h, shallowReactive, isVNode } from "vue"; import { findIndex, get, each, set, isString } from "lodash-es"; -import { useZIndex } from "@eric-ui/hooks"; +import { useZIndex, useId } from "@eric-ui/hooks"; import MessageConstructor from "./Message.vue"; -let seed = 0; - const instances: MessageInstance[] = shallowReactive([]); const { nextZIndex } = useZIndex(); @@ -25,7 +24,7 @@ export const messageDefaults = { transitionName: "fade-up", } as const; -const normalizeOptions = (options: MessageParams): CreateMessageProps => { +function normalizeOptions(options: MessageParams): CreateMessageProps { const result = !options || isVNode(options) || isString(options) ? { @@ -34,10 +33,10 @@ const normalizeOptions = (options: MessageParams): CreateMessageProps => { : options; return { ...messageDefaults, ...result } as CreateMessageProps; -}; +} -const createMessage = (props: CreateMessageProps): MessageInstance => { - const id = `message_${seed++}`; +function createMessage(props: CreateMessageProps): MessageInstance { + const id = useId().value; const container = document.createElement("div"); const destory = () => { const idx = findIndex(instances, { id }); @@ -75,17 +74,17 @@ const createMessage = (props: CreateMessageProps): MessageInstance => { instances.push(instance); return instance; -}; +} -export const message: MessageFn & Partial = (options = {}) => { +export const message: MessageFn & Partial = function (options = {}) { const normalized = normalizeOptions(options); const instance = createMessage(normalized); return instance.handler; }; -export function getLastBottomOffset(id: string) { - const idx = findIndex(instances, { id }); +export function getLastBottomOffset(this: MessageProps) { + const idx = findIndex(instances, { id: this.id }); if (idx <= 0) return 0; return get(instances, [idx - 1, "vm", "exposed", "bottomOffset", "value"]); diff --git a/packages/components/Message/types.ts b/packages/components/Message/types.ts index 57a3115..cd0f332 100644 --- a/packages/components/Message/types.ts +++ b/packages/components/Message/types.ts @@ -30,7 +30,7 @@ export interface Message extends MessageFn { export interface MessageProps { id: string; - message?: string | VNode; + message?: string | VNode | (() => VNode); duration?: number; showClose?: boolean; center?: boolean; diff --git a/packages/components/Notification/Notification.vue b/packages/components/Notification/Notification.vue index ab4c517..ba780a5 100644 --- a/packages/components/Notification/Notification.vue +++ b/packages/components/Notification/Notification.vue @@ -2,8 +2,9 @@ import type { NotificationProps } from "./types"; import { ref, computed, onMounted } from "vue"; import { getLastBottomOffset } from "./methods"; -import { delay, isString } from "lodash-es"; +import { bind, delay, isString } from "lodash-es"; import { RenderVnode, typeIconMap } from "@eric-ui/utils"; +import { useOffset } from "@eric-ui/hooks"; import ErIcon from "../Icon/Icon.vue"; @@ -17,30 +18,40 @@ const props = withDefaults(defineProps(), { const visible = ref(false); const notifyRef = ref(); +// 这个 div 的高度 +const boxHeight = ref(0); + +const { topOffset, bottomOffset } = useOffset({ + getLastBottomOffset: bind(getLastBottomOffset, props), + offset: props.offset, + boxHeight, +}); + const iconName = computed(() => { if (isString(props.icon)) return props.icon; return typeIconMap.get(props.type); }); -// 这个 div 的高度 -const boxHeight = ref(0); -// 上一个实例的最下面的坐标数字,第一个是 0 -const lastBottomOffset = computed(() => getLastBottomOffset(props.id)); -// 这个元素应该使用的 top -const topOffset = computed(() => props.offset + lastBottomOffset.value); -// 这个元素为下一个元素预留的 offset,也就是它最低端 bottom 的 值 -const bottomOffset = computed(() => boxHeight.value + topOffset.value); +const horizontalClass = computed(() => + props.position.endsWith("right") ? "right" : "left" +); + +const verticalProperty = computed(() => + props.position.startsWith("top") ? "top" : "bottom" +); + const cssStyle = computed(() => ({ - top: topOffset.value + "px", + [verticalProperty.value]: topOffset.value + "px", zIndex: props.zIndex, })); -let timer: any; +let timer: number; function startTimer() { if (props.duration === 0) return; timer = delay(close, props.duration); } + function clearTimer() { clearTimeout(timer); } @@ -54,6 +65,7 @@ onMounted(() => { visible.value = true; startTimer(); }); + defineExpose({ close, bottomOffset, @@ -72,6 +84,7 @@ defineExpose({ :class="{ [`er-notification--${type}`]: type, 'show-close': showClose, + [horizontalClass]: true, }" :style="cssStyle" v-show="visible" diff --git a/packages/components/Notification/methods.ts b/packages/components/Notification/methods.ts index 6e9ab97..491b127 100644 --- a/packages/components/Notification/methods.ts +++ b/packages/components/Notification/methods.ts @@ -5,46 +5,54 @@ import type { Notification, NotificationParams, NotificationHandler, + NotificationProps, notificationType, } from "./types"; -import { notificationTypes } from "./types"; +import { notificationTypes, notificationPosition } from "./types"; import { shallowReactive, isVNode, render, h } from "vue"; import { each, findIndex, isString, set, get } from "lodash-es"; -import { useZIndex } from "@eric-ui/hooks"; +import { useZIndex, useId } from "@eric-ui/hooks"; import NotificationConstructor from "./Notification.vue"; -let seed = 0; - -const instances: NotificationInstance[] = shallowReactive([]); const { nextZIndex } = useZIndex(); export const notificationDefaults = { type: "info", + position: "top-right", duration: 3000, offset: 20, transitionName: "fade", showClose: true, } as const; -const normalizeOptions = ( +const instancesMap: Map = + new Map(); +each(notificationPosition, (key) => instancesMap.set(key, shallowReactive([]))); + +const getInstancesByPosition = ( + position: NotificationProps["position"] +): NotificationInstance[] => instancesMap.get(position)!; + +function normalizeOptions( options: NotificationParams -): CreateNotificationProps => { +): CreateNotificationProps { const result = !options || isVNode(options) || isString(options) ? { message: options } : options; return { ...notificationDefaults, ...result } as CreateNotificationProps; -}; +} -export const createNotification = ( +function createNotification( props: CreateNotificationProps -): NotificationInstance => { - const id = `message_${seed++}`; +): NotificationInstance { + const id = useId().value; const container = document.createElement("div"); - + const instances = getInstancesByPosition(props.position || "top-right"); const destory = () => { const idx = findIndex(instances, { id }); + if (idx === -1) return; instances.splice(idx, 1); @@ -77,32 +85,36 @@ export const createNotification = ( }; instances.push(instance); return instance; -}; +} -export const notification: NotificationFn & Partial = ( +export const notification: NotificationFn & Partial = function ( options = {} -) => { +) { const normalized = normalizeOptions(options); const instance = createNotification(normalized); return instance.handler; }; export function closeAll(type?: notificationType) { - each(instances, (instance) => { - if (type) { - instance.props.type === type && instance.handler.close(); - return; - } - instance.handler.close(); + instancesMap.forEach((instances) => { + each(instances, (instance) => { + if (type) { + instance.props.type === type && instance.handler.close(); + return; + } + instance.handler.close(); + }); }); } -export const getLastBottomOffset = (id: string) => { - const idx = findIndex(instances, { id }); +export function getLastBottomOffset(this: NotificationProps) { + const instances = getInstancesByPosition(this.position || "top-right"); + const idx = findIndex(instances, { id: this.id }); + if (idx <= 0) return 0; return get(instances, [idx - 1, "vm", "exposed", "bottomOffset", "value"]); -}; +} each(notificationTypes, (type) => { set(notification, type, (options: NotificationParams) => { diff --git a/packages/components/Notification/style.css b/packages/components/Notification/style.css index 31bae9f..b5e10b3 100644 --- a/packages/components/Notification/style.css +++ b/packages/components/Notification/style.css @@ -27,8 +27,14 @@ overflow-wrap: anywhere; overflow: hidden; z-index: 9999; - right: 10px; - top: 0; + + &.right { + right: 10px; + } + + &.left { + left: 10px; + } .er-notification__text { margin: 0 10px; @@ -75,8 +81,14 @@ } .er-notification-fade-enter-from { - right: 0; - transform: translate(100%); + &.right{ + right: 0; + transform: translate(100%); + } + &.left{ + left:0; + transform: translate(-100%); + } } .er-notification-fade-leave-to { opacity: 0; diff --git a/packages/components/Notification/types.ts b/packages/components/Notification/types.ts index caf4ca9..7fc15c7 100644 --- a/packages/components/Notification/types.ts +++ b/packages/components/Notification/types.ts @@ -8,10 +8,19 @@ export const notificationTypes = [ ] as const; export type notificationType = (typeof notificationTypes)[number]; +export const notificationPosition = [ + "top-right", + "top-left", + "bottom-right", + "bottom-left", +] as const; +export type NotificationPosition = (typeof notificationPosition)[number]; + export interface NotificationProps { title: string; id: string; zIndex: number; + position: NotificationPosition; type?: "success" | "info" | "warning" | "danger" | "error"; message?: string | VNode; duration?: number; diff --git a/packages/docs/demo/notification/Basic.vue b/packages/docs/demo/notification/Basic.vue index aa34deb..64f72e0 100644 --- a/packages/docs/demo/notification/Basic.vue +++ b/packages/docs/demo/notification/Basic.vue @@ -6,6 +6,7 @@ function openNotify1() { ErNotification({ title: "Title", message: h("i", { style: "color:teal" }, "This is a remider"), + position:'bottom-right' }); } @@ -14,6 +15,7 @@ function openNotify2() { title: "Prompt", message: "This is a message that does not auto close", duration: 0, + position:'top-left' }); } diff --git a/packages/hooks/index.ts b/packages/hooks/index.ts index 5b588f7..faa9290 100644 --- a/packages/hooks/index.ts +++ b/packages/hooks/index.ts @@ -5,7 +5,8 @@ import useZIndex from "./useZIndex"; import useProp from "./useProp"; import useDisabledStyle from "./useDisabledStyle"; import useId from "./useId"; -import useLocale from './useLocale' +import useLocale from "./useLocale"; +import useOffset from "./useOffset"; export { useClickOutside, @@ -15,5 +16,6 @@ export { useFocusController, useDisabledStyle, useLocale, + useOffset, useId, }; diff --git a/packages/hooks/useOffset.ts b/packages/hooks/useOffset.ts new file mode 100644 index 0000000..b3eff19 --- /dev/null +++ b/packages/hooks/useOffset.ts @@ -0,0 +1,29 @@ +import { type Ref, computed } from "vue"; + +interface useOffsetOptions { + // id: string; + offset: number; + boxHeight: Ref; + getLastBottomOffset: () => number; +} + +interface useOffsetResult { + topOffset: Ref; + bottomOffset: Ref; +} + +export function useOffset(opts: useOffsetOptions): useOffsetResult { + // 上一个实例最下面的坐标,第一个是0 + const lastBottomOffset = computed(() => opts.getLastBottomOffset()); + // 本元素应该的 top + const topOffset = computed(() => opts.offset + lastBottomOffset.value); + // 为下一个实例预留的底部 offset + const bottomOffset = computed(() => topOffset.value + opts.boxHeight.value); + + return { + topOffset, + bottomOffset, + }; +} + +export default useOffset;