全局通知组件

全局通知组件

  • Notify
    • _src
      • index.vue
      • instances.ts
      • notify.ts
      • props.ts
    • index.ts

1.index.vue

<template>
  <Teleport :to="appendTo">
    <div
      v-show="visible"
      absolute
      w-800px
      h-30px
      mx-auto
      left-0
      right-0
      text="16px #ECAE4F"
      class="font-YOUHEI wrapper"
      :style="{ top: `${offset}px` }"
    >
      <SvgIcon name="fa6-solid:bullhorn" :size="20" mr-10px />
      <ScrollTransform
        w-600px
        origin="h"
        :duration="duration"
        :data="message"
        timing-func="linear"
        :ani-count="repeatNum"
        @finish-ani="onHidden"
      >
        <template #default="{ data }">
          <div whitespace-nowrap h-full>
            <div inline-block w-600px></div>
            <div inline-block text-nowrap>{{ data }}</div>
            <div inline-block w-600px></div>
          </div>
        </template>
      </ScrollTransform>
      <div ml-20px>>></div>
    </div>
  </Teleport>
</template>

<script setup lang="ts">
import type { NotifyProps } from "./props";
const visible = ref(false);
function hidden() {
  visible.value = false;
}
function show() {
  visible.value = true;
}

withDefaults(defineProps<NotifyProps>(), {
  appendTo: ".layout-main",
  message: "请输入文字",
  repeatNum: 3,
  offset: 180,
  duration: 10,
  onHidden: () => {},
});

onMounted(() => {
  show();
});
defineExpose({
  hidden,
  show,
  visible,
});
</script>

<style lang="scss" scoped>
.wrapper {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    linear-gradient(
      to right,
      rgb(50 20 0 / 0%) 5%,
      rgb(50 20 0 / 30%) 30%,
      rgb(50 20 0 / 40%) 50%,
      rgb(50 20 0 / 30%) 70%,
      rgb(50 20 0 / 0%) 95%
    );
}
</style>

2.instances.ts

import type { VNode } from 'vue'
import type { NotifyProps } from './props'
import type { NotifyHandler } from './notify'

export type NotifyContext = {
    vnode: VNode
    handler: NotifyHandler
    props: NotifyProps
}

const instance = shallowRef<NotifyContext | null>(null)

export const getInstance = (): NotifyContext | null => {
    return instance.value
}

export const setInstance = (inst: NotifyContext): NotifyContext => {
    instance.value = inst
    return instance.value
}

export const clearInstance = () => {
    instance.value = null
}


3.notify.ts

import { createVNode, render } from "vue"
import NotifyConstructor from './index.vue'
import { getInstance, setInstance,clearInstance } from './instances'

import type { NotifyContext } from './instances'
import type { UserNotifyProps } from './props'
interface NotifyHandler {
    hidden: () => void,
    destroy: () => void,
    show: () => void,
}

const DEFAULT_USER_OPTIONS = {
    appendTo: ".layout-main",
    message: "请输入通知",
    repeatNum: 3,
    offset: 180,
    duration: 10,
}

const normalizeOptions = (userOptions: UserNotifyProps): Required<UserNotifyProps> => {
    const options = {
        ...DEFAULT_USER_OPTIONS,
        ...userOptions,
    }
    return options
}


const hiddenNotify = () => {
    const cur = getInstance()
    if (!cur) return;
    const { handler } = cur
    handler.hidden()
}

const createNotify = (userOptions: UserNotifyProps) => {
    let cur = getInstance()
    const options = normalizeOptions(userOptions)
    if (cur) {
        cur.props.message = options.message
        cur.props.duration = options.duration
        cur.props.offset = options.offset
        cur.props.repeatNum = options.repeatNum
        cur.props.appendTo = options.appendTo
        cur.handler.show()
    } else {
        const container = document.createElement('div')
        const props = {
            ...options,
            onHidden: () => {
                hiddenNotify()
            }
        }
        const vnode = createVNode(NotifyConstructor, props, null)
        render(vnode, container)

        const handler: NotifyHandler = {
            show: () => {
                instance.vnode.component!.exposed!.show()
            },
            hidden: () => {
                instance.vnode.component!.exposed!.hidden()
            },
            destroy: () => {
                clearInstance()
                render(null, container)
            }
        }
        const instance: NotifyContext = {
            vnode,
            handler,
            props: (vnode.component as any).props,
        }
        cur = setInstance(instance)
    }

    return cur
}


const notify = (options: UserNotifyProps) => {
    const instance = createNotify(options)
    const userHandler = instance.handler.destroy
    return { destory: userHandler }
}


export default notify
export type {
    NotifyHandler
}

4.props.ts

interface UserNotifyProps {
    message: string;    
    appendTo?: string;   // 挂载dom位置
    repeatNum?: number;  // 循环次数,循环结束会隐藏组件
    offset?: number;     // 距离top距离
    duration?:number;    // 单次循环动画的时间
}


interface NotifyProps extends Required<UserNotifyProps> {
    onHidden: () => void;
}

export type {
    UserNotifyProps,
    NotifyProps
};

5.index.ts

import notify from './_src/notify'

export default notify

export type * from './_src/props'

/**
 * *  调用方法
 * *  引入   
 * *  import Notify from "@/components/Notify";
 * *  其他参数可以参考源码 notify.ts
 * *  调用方法 
 * *  const { destroy } =Notify({
 * *    message: "xxxxxxxxxxxxxxxx",
 * *  });  
 * *  离开当前大屏页面时请注意调用destroy函数
 * * 
 */

6.ScrollTransform.vue

<script setup lang="ts" generic="T extends Object">
const props = withDefaults(
  defineProps<{
    origin?: "v" | "h";
    duration?: number;
    data: T;
    timingFunc?: string;
    aniCount?: number | "infinite";
  }>(),
  {
    origin: "v",
    duration: 3,
    timingFunc: "ease-in-out",
    aniCount: "infinite",
  }
);
const emits = defineEmits<{
  (eventName: "finishAni"): void;
}>();

const viewer = ref<HTMLDivElement | null>();
const container = ref<HTMLDivElement | null>();

const aniListenerManage = (() => {
  let aniListenerDom: HTMLDivElement | null = null;
  const aniListener = () => {
    emits("finishAni");
  };
  const add = (dom: HTMLDivElement, aniCount: number | "infinite") => {
    if (!aniListenerDom && aniCount !== "infinite") {
      dom.addEventListener("animationend", aniListener);
      aniListenerDom = dom;
    }
  };

  const remove = () => {
    if (aniListenerDom) {
      aniListenerDom.removeEventListener("animationend", aniListener);
      aniListenerDom = null;
    }
  };
  return {
    add,
    remove,
  };
})();

function renderScroll() {
  nextTick(() => {
    // 视窗的高度
    const viewerDom: NonNullable<typeof viewer.value> = viewer.value!;
    const containerDom: NonNullable<typeof container.value> = container.value!;
    const { scrollWidth, scrollHeight, clientWidth, clientHeight } = viewerDom;

    const { duration, timingFunc, aniCount } = props;

    aniListenerManage.remove();

    if (
      (clientHeight >= scrollHeight && props.origin === "v") ||
      (clientWidth >= scrollWidth && props.origin === "h")
    ) {
      return;
    }

    viewerDom.style.setProperty("--cur-animation-duration", `${duration}s`);
    viewerDom.style.setProperty("--cur-animation-count", `${aniCount}`);

    if (props.origin === "v" && clientHeight < scrollHeight) {
      // 开始滑动
      const height = clientHeight - scrollHeight;
      viewerDom.style.setProperty("--cur-animation-axis", `translate(0, ${height}px)`);
      containerDom.style.setProperty("animation-timing-function", timingFunc);
      containerDom.classList.add("scroll");
    } else if (props.origin === "h" && clientWidth < scrollWidth) {
      // 开始滑动
      const width = clientWidth - scrollWidth;
      viewerDom.style.setProperty("--cur-animation-axis", ` translate(${width}px, 0)`);
      containerDom.style.setProperty("animation-timing-function", timingFunc);
      containerDom.classList.add("scroll");
    }

    aniListenerManage.add(containerDom, aniCount);
  });
}

onBeforeUnmount(() => {
  aniListenerManage.remove();
});

watch(
  () => props.data,
  () => {
    // * 当数据更新时,重新计算动画
    renderScroll();
  },
  {
    immediate: true,
  }
);
</script>
<template>
  <div overflow-hidden ref="viewer">
    <div ref="container">
      <slot v-bind="{ data }" />
    </div>
  </div>
</template>
<style lang="scss" scoped>
$-cur-animation-axis: var(--cur-animation-axis);

@keyframes translate-animate {
  0% {
    transform: translate(0, 0);
  }

  100% {
    transform: $-cur-animation-axis;
  }
}

@mixin active {
  animation-play-state: running;
}

@mixin paused {
  animation-play-state: paused;
}

.scroll {
  animation:
    translate-animate var(--cur-animation-duration) linear
    var(--cur-animation-count);

  &:hover {
    @include paused;
  }

  &:not(:hover) {
    @include active;
  }
}
</style>

posted @ 2024-06-07 17:26  DAmarkday  阅读(9)  评论(0)    收藏  举报
编辑推荐:
· MySQL索引完全指南:让你的查询速度飞起来
· 一个字符串替换引发的性能血案:正则回溯与救赎之路
· 为什么说方法的参数最好不要超过4个?
· C#.Net 筑基-优雅 LINQ 的查询艺术
· 一个自认为理想主义者的程序员,写了5年公众号、博客的初衷
阅读排行:
· MySQL索引完全指南:让你的查询速度飞起来
· 本地搭建一个对嘴AI工具
· HarmonyOS NEXT仓颉开发语言实现画板案例
· 20. Java JUC源码分析系列笔记-CompletableFuture
· 我用这13个工具,让开发效率提升了5倍!
点击右上角即可分享
微信分享提示