git远程仓库

---使用git

  1. 初始化仓库:

    git init
    
  2. 配置提交者身份信息:

    git config user.name  "提交者的「用户名」(可自定义,通常是你的昵称 / 账号名)"
    git config user.email  "提交者的「邮箱」(建议和 GitHub/GitLab 等平台的绑定邮箱一致)"
    

    ** 或者设置全局用身份信息 **

    git config --global user.name "提交者的「用户名」(可自定义,通常是你的昵称 / 账号名)"
    设置全局邮箱(替换成你的邮箱,比如 "zhangsan@example.com")
    git config --global user.email "提交者的「邮箱」(建议和 GitHub/GitLab 等平台的绑定邮箱一致)"
    
  3. 暂存文件变更,把当前项目里所有改动过的、新增的、删掉的文件,都标记为 “准备好提交” 的状态:

    git add .
    
  4. 提交暂存区变更并添加提交说明,用于描述这次提交做了什么修改:

    git commit -m "提交/修改描述"
    
  5. 把本地仓库和远程仓库进行绑定:

    git remote add origin "创库地址"
    
  6. 把本地 master 分支的代码推送到 origin 仓库:

    git push -u origin master
    
  7. 【可选】查看当前 Git 仓库配置了哪些远程仓库地址

    git remote -v
    
  8. 【可选】修改 fetch 地址(同时默认 push 地址)【拉取地址】

    git remote set-url origin "创库地址"
    
  9. 【可选】清空所有 推送地址,只保留这个新的

    git remote set-url --push origin "创库地址"
    
  10. 【可选】添加多个仓库地址进行推送,可添加多个【双推】,命令如下:

    git remote set-url --add --push origin "创库地址"
    
  11. 【可选】删除某个仓库推送地址

    git remote set-url --delete --push origin "创库地址"
    

---其他关键点

vue3

vue3语法

1)create vue3

// 新建vue3项目
npm create vue@latest
或
pnpm create vue@latest
或
yarn dlx create-vue@latest
或
bun create vue@latest

2)setup语法糖

--- defineProps (不需要导入即可使用)
  ** 接收由父组件传递的数据
  const props = defineProps({
      // 属性配置
      属性名: {
        // 该属性值的类型
        type: [String, Number, ...],或则type: Boolean,
        // 该属性的默认值
        default: true,
      },
    ············
  })
--- defineEmits (不需要导入即可使用)
  ** 自定义事件
  const emit = defineEmits(["自定义事件名","自定义事件名",·······]);

  // 示例事件
  const click = ()=>{
    console.log("该事件由页面元素点击触发");

    // 添加自定义事件
    emit("自定义事件名", '携带参数(可以是数组,字符串,对象,函数等等)')
  }
--- defineExpose (不需要导入即可使用)
  ** 子组件对父组件暴露的数据以及方法
  // 属性
  const state = reactive({
    name: "defineExpose",
    message: "使用成功"
  })

  // 示例事件
  const click = ()=>{
    console.log("事件触发");
  }

  defineExpose({
    // 暴露的属性
    name: state.name,
    message: state.message,
    // 暴露的方法
    click,
  })

3)创建本地静态访问链接 (用于读取静态资源)

export const getAssetsFile = (url: string) => {
    return new URL(`@/assets/icon/${url}`, import.meta.url).href; // 只能在本文件的js中拼接url前面的路径,`@/assets/icon/`,其它页面使用只能是名称加后缀,url不能为: '/icon/t.png', 正确的是't.png'
}

4)router路由

  1. 下载路由:

    npm install vue-router@4
    或
    yarn add vue-router@4
    或
    pnpm install vue-router@4
    
  2. 在router.js中的配置路由(包括一些示例):

    // 导入路由依赖
    import { createRouter, createWebHistory } from 'vue-router';
    // 导入进度条进度(可用可不用)
    import nprogress from 'nprogress';
    
    // 导入用户路由(可动态添加)
    import { userRouterInfo } from './userRouter';
    
    // 配置路由规则
    const routes = [
        // 域名重定向
        {
            path: '/',
            redirect: (to: any) => {
                // 方法接收目标路由作为参数
                // return 重定向的字符串路径/路径对象
                return { name: '定向的路由名,如:FileList', query: {} }
            },
        },
        {
            path: '/login',
            name: 'login',
            component: () => import("@/page/login/index.vue"),
            meta: {
                // 页面标题
                title: "立即授权",
                // 是否不需要校验当前账号是否过期 (true表示不需要验证,false表示需要验证)
                isCheckToken: true,
            }
        },
        {
            
            // path: '/:catchAll(.*)*', // 通配符路由匹配所有未匹配到的路径[两个都一样]
            path: '/:pathMatch(.*)*', // 通配符路由匹配所有未匹配到的路径[两个都一样]
            name: '404',
            component: () => import("@/page/404/index.vue"),
            meta: {
                // 页面标题
                title: "页面错误404",
            }
        }
    ];
    
    // 创建路由
    const router = createRouter({
     // 路由的模式 哈希值
        history: createWebHistory(),
        // 路由规则
        routes,
        //滚动行为
        scrollBehavior(to, from, savedPosition) {
            // 如果路径相同,则不需要生效
            if(to.name === from.name) return;
    
            // 优先处理浏览器前进/后退:恢复历史滚动位置 【为啥要延迟300毫秒呢,因为我路由离开动画设置的是300ms】
            // if (savedPosition) return new Promise((resolve) => setTimeout(() => resolve(savedPosition), 300));
    
            // 新页面导航:延迟300ms返回顶部(支持锚点定位)
            return new Promise((resolve) => {
                setTimeout(() => {
                    // 处理锚点定位(如 #/detail#section-1)
                    if (to.hash) {
                        resolve({
                            el: to.hash,
                            behavior: 'smooth',
                            top: 76 // 避开固定导航栏(根据实际高度调整)
                        });
                    } else {
                        resolve({ left: 0, top: 0 });
                    }
                }, 300); // 【为啥要延迟300毫秒呢,因为我路由离开动画设置的是300ms】
            });
        }
    });
    
    // 添加用户路由(动态添加)
    userRouterInfo.forEach((item: any) => router.addRoute(item));
    
    // 前置路由守卫
    router.beforeEach((to, from, next) => {
        // 开启进度条进度
        nprogress.start();
    
        // 获取当前账号的token
        let token = localStorage.getItem("token");
    
        // 判断登录成功后又进入登录页
        if (token && to.name === 'login') return next({ path: "/" });
    
        // 是否需要校验当前账号是否过期 (true表示不需要验证,false表示需要验证)
        if (!to.meta.isCheckToken) {
            // 验证逻辑...
        }
        // 允许放行
        next();
    });
    
    // 后置路由守卫
    router.afterEach((to, form) => {
        // 关闭进度条进度
        nprogress.done();
    
        // 修改页面标题
        document.title = to.meta.title as string;
    
        // 当前主题是哪一个
        let theme = localStorage.getItem("themeType");
        // 判断当前主题是否存在
        theme = ['light', 'dark'].some((item: string)=> item === theme) ? theme : 'light';
    
        // 修改主题样式
        document.documentElement.dataset.theme = theme!;
    
        // 保存当前主题
        localStorage.setItem("themeType", theme!);
    });
    
    // 暴露router路由
    export const onRouter = (app: { use: Function }) => app.use(router);
    // 暴露默认路由
    export default router;
    
  3. 动态路由表'./userRouter'的路由规则作为示例展示:

    // 创建用户路由规则信息
    export const userRouterInfo = [
        {
            path: "/FileList",
            name: "FileList",
            component: () => import('@/view/FileList/index.vue'),
            meta: {
                title: "文件",
                // 加入导航栏的配置项
                NavigationOptions:{
                    // 标题
                    title:'文件列表',
                    // 图标
                    icon:"icon-sucai",
                },
            },
        },
        {
            path: "/",
            component: () => import('@/page/station/index.vue'),
            children:[
                {
                    path: "/tools",
                    name: "tools",
                    component: () => import(`@/view/tools/index.vue`),
                    meta: {
                        title: "Tools-Web-一个轻量的工具",
                        // 加入导航栏的配置项
                        NavigationOptions:{
                            // 标题
                            title:'Tools-web 工具',
                            // 图标
                            icon:"icon-guanligongju",
                        },
                    }
                },
                {
                    path: "Unit",
                    name: "Unit",
                    component: () => import(`@/view/tools/tool-page/Unit.vue`),
                    meta: {
                        title: "单位转换",
                    }
                },
                ......
            ]
        },
        {
            path: "/highQualityWebsite",
            name: "highQualityWebsite",
            component: () => import(`@/view/highQualityWebsite/index.vue`),
            meta: {
                // 标题
                title: '高质量网站分享',
                // 加入导航栏的配置项
                NavigationOptions:{
                    // 标题
                    title:'站外-优质网站',
                    // 图标
                    icon:"icon-profileapp",
                },
            }
        },
        {
            path: "/version",
            name: "version",
            component: () => import(`@/view/version/index.vue`),
            meta: {
                // 标题
                title: '系统日志',
                // 加入导航栏的配置项
                NavigationOptions:{
                    // 标题
                    title:'更新日志',
                    // 图标
                    icon:"icon-wms-icon-",
                },
            }
        },
        ......
    ];
    

5)Pinia

--- Pinia状态管理库

** 在页面或组件修改仓库属性的方法

  1. 下载pinia:

    npm i pinia
    或
    yarn add pinia
    或
    pnpm i pinia
    
  2. store.js中的配置pinia:

    // 导入pinia
    import { createPinia } from 'pinia';
    
    // 创建pinia实例
    const pinia = createPinia();
    
    // 暴露store
    export const store = (app: { use: Function }) => (app.use(pinia));
    
  3. 创库配置以及ts推衍示例:

    /** 仓库配置 */
    import { defineStore } from 'pinia';
    // api请求 (可有可无)
    import { api } from '@/api/index';
    // 导入全局属性以及方法 (可有可无)
    import { appInst } from '@/app/index';
    // 类型推衍
    import type { TYPE_FILELIST } from "@/store/type";
    // 暴露仓库
    export const useFileListStore = defineStore("FileList", {
        // 属性
        state: (): TYPE_FILELIST => {
            return { }
        },
        // 计算属性
        getters: { },
        // 事件,方法
        actions: {
            // 默认加载 (示例)
            onLoad(this: TYPE_FILELIST) { },
        },
    });
    
    // ts推衍示例
    export interface TYPEPARAMETER {
        title: string | number;
        options: Array<any>;
        placeholder: boolean;
        defineStore: Function,
        appInst: {
            $api: object,
            [name: string]: any
        }
    }
    
    export interface TYPE_FILELIST {
        // 文件列表
        files: any;
    
        // 我的账号
        $myAccount: object;
    
        // 头像http头部链接
        $avatarImgHttp: string;
    
        // 头像框http链接
        $avatarFrameImgHttp: string;
    
        // 产品图片地址
        $productFileImgHttp: string;
    }
    
--- Pinia持久化存储
  1. 下载第三方插件“pinia-plugin-persistedstate”:

    npm i pinia-plugin-persistedstate
    或
    yarn add pinia-plugin-persistedstate
    或
    pnpm i pinia-plugin-persistedstate
    
  2. 在store.js入口文件中导入:

    // 导入pinia
    import { createPinia } from 'pinia';
    
    // 持久化存储依赖
    import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
    
    // 创建pinia实例
    const pinia = createPinia();
    
    // 使用持久化存储依赖
    pinia.use(piniaPluginPersistedstate);
    
    // 暴露store
    export const store = (app: { use: Function }) => (app.use(pinia));
    
  3. 在pinia store仓库中使用持久化存储:

    /** 文件列表仓库 */
    import { defineStore } from 'pinia';
    // 导入全局属性以及方法
    import { appInst } from '@/app/index';
    // 类型
    import type { TYPE_FILELIST } from "@/store/type";
    // 暴露仓库
    export const useFileListStore = defineStore("FileList", {
        state: (): TYPE_FILELIST => {
            return {
                // 文件列表
                files: [],
                // 我的账号
                $myAccount: {},
                // 头像http头部链接
                $avatarImgHttp: '',
                // 头像框http链接
                $avatarFrameImgHttp: '',
                // 产品图片地址
                $productFileImgHttp: '',
            }
        },
    
        // ----------------------------
        // 其他代码
        // ----------------------------
        
        
        // persist为true,表示该仓库整个 Store 将使用默认持久化配置保存。
        persist: true, 
     
        // 用于指定 state 中哪些数据需要被持久化。[] 表示不持久化任何状态,undefined 或 null 表示持久化整个 state。
        persist: {
            // 表示$myAccount和files则被持久存储
            paths: ['$myAccount', 'files'],
        },
     
        // 方法
        persist: {
            // 该 beforeRestore 将在从 storage 中恢复数据之前触发,并且它可以访问整个 PiniaPluginContext,这可用于在恢复数据之前强制地执行特定的操作。
            beforeRestore: (ctx: any) => {
                console.log(`即将恢复 '${ctx.store.$myAccount}'`)
            },
            // 该 afterRestore 将在从 storage 中恢复数据之后触发,并且它可以访问整个 PiniaPluginContext,这可用于在恢复数据之后强制地执行特定的操作。
            afterRestore: (ctx: any) => {
                console.log(`刚刚恢复完 '${ctx.store.$myAccount}'`)
            },
        },
     
        persist: {
            // 保存的key名
            key: 'USERAVATAR',
            // 存储方式
            storage: localStorage,
            // 当前的属性进行持久化存储
            pick: ['$myAccount.avatar', '$myAccount.avatarFrame', "$myAccount.endTime", "$avatarImgHttp", "$avatarFrameImgHttp", "$productFileImgHttp"],
            // 自定义序列化程序,用于在持久化之前序列化数据,并在解除冻结存储之前反序列化数据。
            serializer: {
                // 持久化数据之前执行
                serialize(state) {  // state是上面指定保存的值 如 pick: ['$myAccount.avatar', '$myAccount.avatarFrame', ...]
                    // 没有账户不进行存储任何数据
                    if(!state.$myAccount.endTime) return '';
                    // 转为json字符串进行保存
                    const result = JSON.stringify(state);
                    // 加密数据
                    return appInst.$Crypto.AES.encrypt(result, appInst.decode(appInst.AES_Password)).toString().replace(/=/g, '');
                },
                // 恢复数据之前执行
                deserialize(state) {
                    // 解密数据
                    const result = appInst.$Crypto.AES.decrypt(state, appInst.decode(appInst.AES_Password)).toString(appInst.$Crypto.enc.Utf8);
                    // 转换对象并返回数据
                    return JSON.parse(result);
                }
            }
        },
    });
    

*** 持久化存储官网

vue3第三方插件

--- 批量检测并更新依赖

npm-check-updates 批量检测并更新依赖工具 【npm专用】
说明:
	npm-check-updates(简称 ncu)是批量检测并更新依赖的工具,它可以检查并更新package.json中的依赖到最新版本。

1. 安装 npm-check-updates(全局或项目本地):
	npm install -g npm-check-updates // 全局安装(推荐)
	或
	npm install -D npm-check-updates // 项目本地安装(需在项目中使用 npx)
2. 扫描并更新 package.json中的依赖:
	ncu -u  // -u 表示直接修改 package.json 中的版本号为最新
3. 安装更新后的依赖:
	npm install // 建议把node_modules文件删除掉,再执行安装命令
yarn upgrade-interactive 【Yarn专用】

yarn upgrade-interactive --latest

说明:勾选需要更新的包(重点勾选 vue、vite、@vitejs/plugin-vue等),确认后自动升级。

--- less

  1. 下载less以及less-loader:

    npm i less less-loader --save // 由于vite内置less-loader则不需要再次安装
    // 只需安装less即可
    npm i less -D
    或
    yarn add less -D
    或
    pnpm i less -D
    

--- scss

  1. 下载sass以及sass-loader:

    npm i sass sass-loader -D // 由于vite内置sass-loader则不需要再次安装
    // 只需安装scss即可
    npm i sass -D
    或
    pnpm add sass -D
    或
    yarn add sass -D
    
  2. 为什么用的scss却要安装sass?

    答:我们通常使用scss作为预处理器,但安装的包是sass。这是因为Sass有两种语法:

    SCSS(Sassy CSS)使用.scss扩展名,语法和CSS类似,使用大括号和分号。

    缩进语法(Sass)使用.sass扩展名,使用缩进和换行而不是大括号和分号。

    在Vue项目中,我们通常使用SCSS语法,因为更接近CSS,更容易上手。而安装的包sass是两种语法的实现。

--- mitt轻量事件总线库

  1. 安装mitt:

    npm install mitt
    或
    yarn add mitt
    
  2. 核心 API

    API作用示例
    emitter.on(name, fn)监听指定事件emitter.on('msg', (data) => {})
    emitter.emit(name, data)触发指定事件并传递数据emitter.emit('msg', { content: 'hi' })
    emitter.off(name, fn)取消指定事件的监听emitter.off('msg', handleMsg)
    emitter.on('*', (type, data) => {})监听所有事件 
    emitter.all.clear()清空所有事件监听(慎用) 
  3. 封装全局事件总线

    // 导入mitt
    import mitt from 'mitt';
    
    // 定义所有事件的类型(键:事件名,值:参数类型)
    type Events = {
        // 'totalContainerScrollbar'事件名,'void':值的类型,void表示没有值,无参数的事件
        'totalContainerScrollbar': void;
        // 'scrollbar-endReached'事件名,'string': 表示传递的值类型为字符串
        'scrollbar-endReached': string;
    
        // [key: string]: any
        // 'cross-page-msg': { content: string; time: string } // 带参数的事件
        // 'refresh-data': void // 无参数的事件
        // 'user-login': { userId: string; name: string } // 复杂参数事件
    }
    
    // 创建类型约束的 mitt 实例, 用于【绑定事件】和【触发事件】
    const emitter = mitt<Events>();
    
    // 导出实例,供全项目使用
    export default emitter;
    
  4. 触发事件(发送方:页面 / 组件 A)

    关键点:通过 emitter.emit(事件名, 数据) 发送事件,支持任意类型数据:

    import { ref } from 'vue';
    import emitter from '@/utils/emitter';
    
    const inputVal = ref('');
    
    // 触发事件(可在点击、接口回调等场景调用)
    const sendEvent = () => {
      // 第一个参数:事件名称(自定义,需与监听方一致)
      // 第二个参数:要传递的数据(任意类型:字符串、对象、数组等)
      emitter.emit('cross-page-msg', {
        content: inputVal.value,
        time: new Date().toLocaleTimeString()
      })
      
      // 也可触发无参数事件
      // emitter.emit('refresh-data')
    }
    
  5. 监听 / 取消监听事件(接收方:页面 / 组件 B)

    关键点:通过 emitter.on(事件名, 回调函数) 监听,必须在组件卸载时取消监听(避免内存泄漏)

    import { onMounted, onUnmounted, ref } from 'vue'
    import emitter from '@/utils/emitter'
    
    // 响应式数据,用于实时展示接收的事件数据
    const receivedData = ref(null)
    
    // 定义事件处理函数(参数为触发方传递的数据)
    const handleEvent = (data) => {
      console.log('接收到事件:', data)
      receivedData.value = data // 实时更新视图
    }
    
    // 组件挂载时,开始监听事件
    onMounted(() => {
      emitter.on('cross-page-msg', handleEvent) // 事件名必须与触发方一致
    })
    
    // 组件卸载时,取消监听(核心!防止内存泄漏)
    onUnmounted(() => {
      emitter.off('cross-page-msg', handleEvent) // 需传入同一个处理函数
    })
    
  6. 监听所有事件(调试 / 全局日志):

    // 监听所有事件的触发,type 是事件名,data 是传递的数据
    emitter.on('*', (type, data) => {
      console.log(`触发事件:${type},数据:`, data)
    })
    
  7. 一次性事件(监听一次后自动移除):

    // 手动实现一次性监听(mitt 本身无 once 方法,可通过 off 实现)
    const handleOnce = (data) => {
      console.log('仅执行一次:', data)
      emitter.off('once-event', handleOnce) // 执行后立即取消监听
    }
    emitter.on('once-event', handleOnce)
    
  8. 清空所有监听(慎用,会移除所有事件的监听):

    emitter.all.clear()
    

--- axios(http请求)

  1. 下载axios:

    npm i axios
    或
    pnpm i axios
    或
    yarn add axios
    或
    bun add axios
    
  2. 在src根目录下创建新的文件,复制以下代码即可:

    // 导入axios
    import axios from 'axios';
    // 导入进度
    import nprogress from 'nprogress'
    
    // 创建新的axios
    const $axios = axios.create({
        // 开发板请求地址,需要vite配置跨域
        baseURL: "http://localhost:9999/api",
        // 正式版请求地址,例如:
        baseURL: "https://fc-mp-ef.bspapp.com",
        // 请求时间
        timeout: 8000
    });
    
    // 枚举错误信息
    enum error {
        "请求成功" = 200,
        '路径错误' = 404,
        "客户端请求的语法错误,服务器无法理解" = 400,
        "请求要求用户的身份认证" = 401,
        "服务器拒绝执行此请求" = 403,
        "服务器无法根据客户端的请求找到资源" = 404,
        "客客户端请求中的方法被禁止" = 405,
        "服务器无法根据客户端请求的内容特性完成请求" = 406,
        "请求要求代理的身份认证,未授权" = 407,
        "请求超时" = 408,
        "服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突" = 409,
        "服务器错误" = 500,
        "服务器不支持请求的功能,无法完成请求" = 501,
        "作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应" = 502,
        "由于超载或系统维护,服务器暂时的无法处理客户端的请求" = 503,
        "充当网关或代理的服务器,未及时从远端服务器获取请求" = 504,
        "服务器不支持请求的HTTP协议的版本,无法完成处理" = 505,
    }
    
    // 创建请求拦截器
    $axios.interceptors.request.use((config) => {
    // 进度条进度开启
    progress.start();
    ---------------
    // 用于添加token等等
    ---------------
     // 一定要返回相应头,否则无法请求
        return config;
    });
    
    // 创建相应拦截器
    $axios.interceptors.response.use((response) => {
    
        // 进度条进度关闭
        nprogress.done();
    
        // 返回的数据没有错误
        if (response.status == 200) {
            return response.data;
        }
    
        // 暴露提示信息
        console.log(new Error(error[response.status]));
    
        // 用于处理数据
        return response;
    }, err => {
        // 错误信息
        console.log(new Error(err));
        console.log(error[err.response.status]);
        // 错误提示
        return new Error(error[err.response.status]);
    });
    
    // 暴露数据($http就是axios)
    export const $http = $axios;
    
    // get请求
    export const $get = (url: string) => $axios.get(url);
    
    // post请求
    export const $post = (obj: { url: string, data?: object }) => $axios({  method: "POST", ...obj });
    
  3. 导出封装的方法:

    import { $http, $post, $get } from './request/index';
    
    // get请求使用方法
    export const login = () => $get('/message.json');  // "/message.json"为请求地址
    
    // post请求使用方法
    export const getFilesHttp = (parameter) => $post({
        // 请求地址
        url: "/getFile",
        // 请求体
        data: parameter
    });
    
    

--- vue3-lazyload图片懒加载

  1. 下载第三方插件“vue3-lazyload”:

    npm i vue3-lazyload
    或
    yarn add vue3-lazyload
    或
    pnpm i vue3-lazyload
    
  2. main.js入口文件注册插件:

    //引入图片懒加载插件
    import Lazyload from "vue3-lazyload";
    app.use(Lazyload);
    
    //注册插件
    app.use(Lazyload, {
       //可以指定加载中的图像
       loading: "@/assets/images/default.png",
       //可以指定加载失败的图像
       error: "@/assets/images/err.png",
    });
    
    备注:自定义指令为"v-lazy", v-lazy == src。
    
  3. 在页面/组件中使用懒加载:

     <img v-lazy="图片地址" />
    

--- nprogress进度条

  1. 下载第三方插件“nprogress”:

    npm i nprogress
    或
    pnpm i nprogress
    
  2. main.js入口文件导入nprogress以及nprogress样式:

    // 进度条进度
    import nprogress from 'nprogress';
    // 导入进度条进度
    import 'nprogress/nprogress.css';
    
  3. 在js或ts中使用nprogress:

    // 进度条进度开启
    nprogress.start();
    // 进度条进度关闭
    nprogress.done();
    

--- animate动画库

  1. 下载第三方插件“animate.css”:

    npm i animate.css
    或
    pnpm i animate.css
    
  2. main.js入口文件导入animate:

    // 导入animate动画库
    import 'animate.css';
    
  3. 在页面/组件中使用懒加载:

    1. 单个html元素时使用transition效果更好:
     <transition
         <!-- animate.css名称,不需要修改 -->
         name="custom-classes"
         <!-- 进入时的该元素的动画效果,animate__animated是固定不变的,修改animate__animated后面即可 -->
         enter-active-class="animate__animated animate__bounceIn"
         <!-- 离开时的该元素的动画效果 -->
         leave-active-class="animate__animated animate__zoomOutRight">
         <div></div>
     </transition>
    
     -------------------------------------------------------------
    
    2. 多个列表时使用TransitionGroup效果更好:
     <TransitionGroup
         <!-- TransitionGroup在编译时,编译成的html标签  tag="ul"时,编译为“ul”标签 -->
         tag="ul"
         <!-- animate.css名称,不需要修改 -->
         name="custom-classes"
         <!-- 进入时的该元素的动画效果,animate__animated是固定不变的,修改animate__animated后面即可 -->
         enter-active-class="animate__animated animate__bounceIn"
         <!-- 离开时的该元素的动画效果 -->
         leave-active-class="animate__animated animate__zoomOutRight">
         <li></li>
     </TransitionGroup>
    

    *** 更多animate.css动画类型

--- echarts(v3)

  1. 下载第三方插件“echarts”:

    npm i echarts
    或
    pnpm i echarts
    
  2. 在全局或组件中导入echarts(或在main.js/main.ts中):

    // 导入echarts
    import * as echarts from 'echarts';
    
    // 或全局注册 --- 可设置echarts全局属性9(通过vue3组合式api"getCurrentInstance"中获取)
    app.config.globalProperties.echarts = echarts;
    
  3. 在页面/组件中使用:

    /* 在组件或页面中 */
    // 第一步,初始化
    const myChart = appInst.echarts.init(html元素/html标签(div标签即可));
    
    myChart.setOption({
        // echarts官网查询配置项即可
    })
    

    Echarts官网

  4. 示例(柱状图):

    *** 注:测试环境 vue3
    
    // html元素(并设置容器的宽度和高度)
    <div ref="columnarShape" style="width: 600px;height: 200px;"></div>
    
    // 获取柱状图的容器(div即可)
    const columnarShape = ref(null);
    
    // 柱形图
    function columnar() {
        // 创建图标实例
        const myChart = echarts.init(columnarShape.value);
    
        // 配置项
        myChart.setOption({
            // echarts官网查询配置项即可
            // width: "85%",
            // height: "250px",
            // 背景颜色
            backgroundColor: "rgba(0,0,0,0)",
            // backgroundColor: "#2f3349",
            tooltip: {
                trigger: "axis",
                axisPointer: {
                    type: "shadow",
                },
            },
            // 标题
            legend: {
                // 可也添加多个
                data: ["Files", "Click"],
                textStyle: {
                    color: '#000',
                },
            },
            // x轴
            xAxis: {
                data: ['2024-1-21', '2024-1-20', '2024-1-19', '2024-1-18', '2024-1-17', '2024-1-16', '2024-1-15'],
                axisLine: {
                    lineStyle: {
                        color: '#000',
                    },
                },
            },
            // y轴
            yAxis: {
                splitLine: { show: false },
                axisLine: {
                    lineStyle: {
                        color: '#000',
                    },
                },
            },
            series: [
                // 线条
                {
                    name: "Click",
                    type: "line",
                    smooth: true,
                    showAllSymbol: true,
                    symbol: "emptyCircle",
                    symbolSize: 15,
                    data: [80, 40, 50, 80, 40, 50, 10],
                },
                //   实色背景
                {
                    name: "Files",
                    type: "bar",
                    barWidth: 10,
                    itemStyle: {
                        borderRadius: 5,
                        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                            { offset: 0, color: "#14c8d4" },
                            { offset: 1, color: "#43eec6" },
                        ]),
                    },
                    data: [80, 40, 50, 80, 40, 50, 10],
                },
    
                {
                    name: "downLoad",
                    type: "pictorialBar",
                    symbol: "rect",
                    itemStyle: {
                        color: "#0f375f",
                    },
                    symbolRepeat: true,
                    symbolSize: [12, 4],
                    symbolMargin: 1,
                    z: -10,
                    data: [80, 40, 50, 80, 40, 50, 10],
                },
            ],
        });
    }
    
  5. 示例(饼状图):

    *** 注:测试环境 vue3
    
    // html元素(并设置容器的宽度和高度)
    <div ref="pieChart" style="width: 200px;height: 200px;"></div>
    
    // 获取饼状图的容器(div即可)
    const pieChart = ref(null);
    
    // 饼状图
    function cakeShaped() {
        // 图标实例
        const myChart = echarts.init(pieChart.value);
        // 配置项
        myChart.setOption({
            // width: "100%",
            // height: "150px",
             // 背景颜色
            backgroundColor: "#fff",
            // echarts官网查询配置项即可
            legend: {
                top: "bottom",
                textStyle: {
                    color: "#000",
                },
            },
    
            textStyle: {
                 color: "#000",
                fontFamily: "宋体",
            },
    
            // 圆盘颜色
            color: ["#fc8452", "#73c0de", "#5470c6", "#fac858"],
            // 工具菜单
            toolbox: {
                show: false,
                feature: {
                    mark: { show: false },
                    saveAsImage: { show: true },
                },
            },
            series: [
                {
                    name: "Nightingale Chart",
                    type: "pie",
                    radius: [0, 50],
                    center: ["50%", "50%"],
                    roseType: "area",
                    itemStyle: {
                        borderRadius: 8,
                    },
                    data: [
                        { value: 40, name: "Click" },
                        { value: 38, name: "Files" },
                        { value: 32, name: "DownLoad" },
                        { value: 10, name: "People" },
                    ],
                },
            ],
        });
    }
    

--- 代码高亮

安装依赖:npm i highlight.js

在main.js/main.ts中配置如下:
// 引入代码高亮,并进行全局注册
import 'highlight.js/styles/atom-one-dark.css';
//导入代码高亮文件
import hljs from 'highlight.js';
//自定义一个代码高亮指令
app.directive('highlight', function (el) {
    const blocks = el.querySelectorAll('pre code');
    blocks.forEach((block: any) => {
        hljs.highlightBlock(block)
    })
});

组件/页面中:
<div v-highlight>
    <pre style="padding: 0; height: 0">
        <code class="当前语言的名称,如(js,java,md,json,ts...)">{{ 代码富文本 }}</code>
    </pre>
</div>

--- 使用bootstarp 5

--- pako高性能的压缩 / 解压库

  1. pako是什么库?

    答:pako 是一个用于数据压缩/解压的 JavaScript 库(基于 zlib)。

  2. 下载第三方插件“pako”:

    npm install pako
    或
    pnpm add pako
    或
    yarn add pako
    
  3. 完整导入 和 按需导入:

    // 完整导入
    import * as pako from 'pako';
    
    // 按需导入(只导入需要的即可)
    import { ungzip, ungzip, deflate, inflate } from 'pako';
    
  4. TypeScript 支持(可选)

    pako 自带类型声明,TS 项目无需额外安装 @types/pako,直接导入即可使用。

  5. 核心 API 说明:pako主要处理 gzipdeflate 两种压缩格式

    方法作用适用场景
    pako.gzip()压缩数据为 gzip 格式网络传输、大文件压缩
    pako.ungzip()解压 gzip 格式数据解析服务端压缩响应
    pako.deflate()压缩数据为 deflate 格式本地存储(体积更小)
    pako.inflate()解压 deflate 格式数据读取本地压缩存储
  6. gzip压缩和ungzip解压

    // 文本
    const text = "Hello, Vue 3 with Pako!";
    // 压缩
    const gzipResult = pako.gzip(text);
    console.log('压缩结果:', gzipResult);
    // 解压
    const ungzipResult = pako.ungzip(gzipResult, { to: "string" });
    console.log('解压结果:', ungzipResult);
    
    *******************************************************************************
    
    // 对象
    const obj = { msg: "Hello, Vue 3 with Pako", id: 17, ars: [1 ,'2', true] };
    // 压缩
    const gzipResult = pako.gzip(JSON.stringify(obj));
    console.log('压缩结果:', gzipResult);
    // 解压
    const ungzipResult = pako.ungzip(gzipResult, { to: "string" });
    console.log('解压结果:', ungzipResult);
    
  7. deflate压缩和inflate解压

    // 文本
    const text = "Hello, Vue 3 with Pako!";
    
    // 压缩字符串
    const deflateResult = pako.deflate(text);
    console.log('压缩结果:', deflateResult);
    
    // 解压回字符串
    const inflateReResult = pako.inflate(deflateResult, { to: 'string' });
    console.log('解压结果:', inflateReResult);
    
    *******************************************************************************
    
    // 压缩 ArrayBuffer
    const input = new Uint8Array([1, 2, 3]);
    const compressed = pako.deflate(input);
    
    // 解压回 ArrayBuffer
    const output = pako.inflate(compressed);
    
  8. 场景 1:压缩 / 解压 JSON 数据(本地存储优化)

    <template>
      <div>
        <button @click="compressAndSave">压缩并存储数据</button>
        <button @click="decompressAndRead">解压并读取数据</button>
      </div>
    </template>
    
    <script setup>
    import pako from 'pako';
    
    // 模拟大体积JSON数据
    const bigData = {
      list: Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `测试项${i}`, value: Math.random() })),
      time: new Date().getTime()
    };
    
    // 压缩并存储到localStorage
    const compressAndSave = () => {
      // 1. JSON转字符串
      const jsonStr = JSON.stringify(bigData);
      // 2. 字符串转Uint8Array(UTF-8编码)
      const uint8Array = new TextEncoder().encode(jsonStr);
      // 3. pako压缩(deflate格式,体积更小)
      const compressed = pako.deflate(uint8Array);
      // 4. 转Base64存储(避免二进制乱码)
      const base64Str = btoa(String.fromCharCode(...compressed));
      localStorage.setItem('compressedData', base64Str);
      
      console.log('压缩完成:', `原大小${jsonStr.length}字节 → 压缩后${base64Str.length}字节`);
    };
    
    // 解压并读取数据
    const decompressAndRead = () => {
      // 1. 读取Base64数据
      const base64Str = localStorage.getItem('compressedData');
      if (!base64Str) return alert('无压缩数据');
      
      // 2. Base64转Uint8Array
      const compressed = new Uint8Array(atob(base64Str).split('').map(char => char.charCodeAt(0)));
      try {
        // 3. pako解压
        const decompressed = pako.inflate(compressed);
        // 4. Uint8Array转JSON字符串
        const jsonStr = new TextDecoder().decode(decompressed);
        // 5. 转JSON对象
        const data = JSON.parse(jsonStr);
        
        console.log('解压后数据:', data);
        alert(`解压成功,数据条数:${data.list.length}`);
      } catch (err) {
        console.error('解压失败:', err);
        alert('数据解压失败');
      }
    };
    </script>
    
  9. 场景 2:压缩文本后上传(减少网络传输体积)

    <template>
      <textarea v-model="textContent" placeholder="输入大文本内容"></textarea>
      <button @click="uploadCompressedText">压缩并上传</button>
    </template>
    
    <script setup>
    import pako from 'pako';
    import axios from 'axios';
    
    const textContent = ref(''); // 绑定文本框内容
    
    // 压缩文本并上传
    const uploadCompressedText = async () => {
      if (!textContent.value) return alert('请输入文本');
      
      try {
        // 1. 文本转Uint8Array
        const uint8Array = new TextEncoder().encode(textContent.value);
        // 2. gzip压缩(网络传输推荐gzip,服务端兼容性更好)
        const compressed = pako.gzip(uint8Array);
        // 3. 转Blob(方便FormData上传)
        const blob = new Blob([compressed], { type: 'application/gzip' });
        
        // 4. 构建FormData上传
        const formData = new FormData();
        formData.append('compressedText', blob, 'text.gz'); // 命名为.gz便于服务端识别
        
        // 5. 上传到后端
        const res = await axios.post('/api/upload-compressed', formData, {
          headers: { 'Content-Type': 'multipart/form-data' }
        });
        console.log('上传成功:', res.data);
        alert('上传成功');
      } catch (err) {
        console.error('上传失败:', err);
        alert('上传失败');
      }
    };
    </script>
    
  10. 场景 3:解压服务端返回的 gzip 数据

    <script setup>
    import pako from 'pako';
    import axios from 'axios';
    
    // 请求并解压服务端gzip数据
    const fetchAndDecompress = async () => {
      try {
        // 1. 请求二进制数据(responseType设为arraybuffer)
        const res = await axios.get('/api/get-compressed-data', {
          responseType: 'arraybuffer'
        });
        
        // 2. ArrayBuffer转Uint8Array
        const uint8Array = new Uint8Array(res.data);
        // 3. 解压gzip数据
        const decompressed = pako.ungzip(uint8Array);
        // 4. 转字符串/JSON
        const dataStr = new TextDecoder().decode(decompressed);
        const data = JSON.parse(dataStr);
        
        console.log('解压后接口数据:', data);
      } catch (err) {
        console.error('请求/解压失败:', err);
      }
    };
    </script>
    
  11. 注意事项

    1. Vite/Webpack 兼容
    	Vite:无需额外配置,直接导入 pako 即可(Vite 会自动处理 ES 模块);
    	Webpack:pako 是纯 JS 库,无依赖,webpack 打包时无需配置 externals。
    2. 处理中文 / 特殊字符
    	压缩前用 TextEncoder.encode()(UTF-8 编码),解压后用 TextDecoder.decode(),避免中文乱码;
    	禁止直接用 unescape/encodeURI 处理二进制数据,会导致压缩 / 解压失败。
    	localStorage 存储二进制数据乱码→ 压缩后转 Base64 存储(btoa/atob),不要直接存储 Uint8Array。
    3. 大文件处理
    	pako 适合处理 10MB 以内 的数据,超大文件(>50MB)建议分片压缩,避免主线程阻塞;
    4. 错误处理
        压缩 / 解压失败(如数据损坏)会抛出异常,务必用 try/catch 包裹;
        空数据压缩前需判空,避免 pako 处理空 Uint8Array 报错。
    
  12. Web Worker 异步压缩 / 解压,不阻塞 Vue 组件渲染:

    // 示例:Web Worker中压缩(避免页面卡顿)
    // worker.js
    import pako from 'pako';
    self.onmessage = (e) => {
      const { data } = e;
      const compressed = pako.deflate(new TextEncoder().encode(data));
      self.postMessage(compressed);
    };
    
    // 组件中使用
    const worker = new Worker(new URL('./worker.js', import.meta.url));
    worker.postMessage(bigDataStr);
    worker.onmessage = (e) => {
      console.log('Worker压缩完成:', e.data);
    };
    

--- crypto-js加密/解密库

  1. crypto-js是什么插件?

    答:crypto-js 是一个 JavaScript 加密库,封装了常见的加密算法,如 AES、MD5、SHA 系列、DES 等。

  2. 下载第三方插件“pako”:

    npm install crypto-js
    或
    pnpm add crypto-js
    或
    yarn add crypto-js
    
  3. 完整导入 和 按需导入:

    // 完整导入
    import CryptoJS from 'crypto-js';
    
    // 按需导入,只引入需要的模块
    import AES from 'crypto-js/aes';
    import encUtf8 from 'crypto-js/enc-utf8';
    import encBase64 from 'crypto-js/enc-base64';
    import MD5 from 'crypto-js/md5';
    import SHA256 from 'crypto-js/sha256';
    // 其他加密算法...
    
  4. 简单示例

    加密方法:

    /**
     * 加密数据
     * @param obj 需要加密的数据
     * @returns 
     */
    function encryption(obj: any) {
      // 设置密钥(建议至少16个字符)
      const secretKey = "mySecretKey507298738";
    
      // 1. 压缩:JSON字符串 → GZIP压缩
      const jsonString = JSON.stringify(obj);
    //   const compressedData = pako.gzip(jsonString);
      // console.log('压缩成功,数据长度:', compressedData.length, compressedData);
    
      // 2. 将Uint8Array直接转换为CryptoJS可识别的格式
      // 关键:正确转换Uint8Array到WordArray
      const wordArray = Crypto.lib.WordArray.create(compressedData);
    
      // 3. 加密
      const encrypted = Crypto.AES.encrypt(wordArray, secretKey);
      // 返回结果
      return encrypted.toString();
    }
    

    解密方法:

    /**
     * 解密函数
     * @param encryptedText 需要解密的数据
     * @param secretKey 解密的密钥
     * @returns 
     */
    function decrypt(encryptedText: String, secretKey: String) {
      // 解密
      // 4. 解密
      const decrypted = Crypto.AES.decrypt(encryptedText, secretKey);
    
      // 5. 将WordArray转回Uint8Array
      const decryptedWords = decrypted.words;
      const decryptedBytes = new Uint8Array(decryptedWords.length * 4);
    
      for (let i = 0; i < decryptedWords.length; i++) {
        const word = decryptedWords[i];
        decryptedBytes[i * 4] = (word >> 24) & 0xff;
        decryptedBytes[i * 4 + 1] = (word >> 16) & 0xff;
        decryptedBytes[i * 4 + 2] = (word >> 8) & 0xff;
        decryptedBytes[i * 4 + 3] = word & 0xff;
      }
    
      // 6. 去除可能的多余字节(CryptoJS填充)
    //   const actualLength = decrypted.sigBytes;
    //   const finalBytes = decryptedBytes.slice(0, actualLength);
    
      // 7. 解压
    //   const decompressedData = pako.ungzip(finalBytes, { to: "string" });
    
      // 判断当前返回的结构是否为空,为空的话返回基本的数据结构
    //   if (Boolean(decompressedData)) {
    //     // 返回数据
    //     return JSON.parse(decompressedData);
    //   } else {
    //     // 默认数据
    //     return {
    //       // 用户所有账号
    //       account: [],
    //       // 文件数据列表
    //       fileDataList: [],
    //     };
    //   }
    
        return decryptedBytes
    }
    
  5. TypeScript 支持

    declare module 'crypto-js' {
      export interface CipherParams {
        ciphertext: WordArray;
        salt?: string;
        iv?: WordArray;
        algorithm?: string;
        mode?: any;
        padding?: any;
        blockSize?: number;
        formatter?: any;
      }
    
      export class WordArray {
        constructor(words?: number[], sigBytes?: number);
        words: number[];
        sigBytes: number;
        toString(encoder?: any): string;
      }
    
      // 核心加密函数
      export function AES: {
        encrypt(
          message: string | WordArray,
          key: string | WordArray,
          cfg?: any
        ): CipherParams;
        decrypt(
          ciphertext: string | CipherParams,
          key: string | WordArray,
          cfg?: any
        ): WordArray;
      };
    
      // 哈希函数
      export function MD5(message: string | WordArray): WordArray;
      export function SHA1(message: string | WordArray): WordArray;
      export function SHA256(message: string | WordArray): WordArray;
      export function HmacSHA256(
        message: string | WordArray,
        key: string | WordArray
      ): WordArray;
    
      // 编码器
      export const enc: {
        Utf8: {
          parse(str: string): WordArray;
          stringify(wordArray: WordArray): string;
        };
        Base64: {
          parse(str: string): WordArray;
          stringify(wordArray: WordArray): string;
        };
        Hex: {
          parse(str: string): WordArray;
          stringify(wordArray: WordArray): string;
        };
      };
    
      // 模式和填充
      export const mode: {
        CBC: any;
        ECB: any;
      };
      
      export const pad: {
        Pkcs7: any;
      };
    }
    
  6. 封装加密工具函数1

    import CryptoJS from 'crypto-js';
    
    // 加密密钥(在生产环境中应该从环境变量读取)
    const CRYPTO_KEY = import.meta.env.VITE_CRYPTO_KEY || 'your-secret-key-123456';
    const IV = CryptoJS.enc.Utf8.parse('1234567890123456'); // 16位偏移量
    
    /**
     * 加密工具类
     */
    class CryptoUtils {
      /**
       * AES 加密
       * @param {string} data - 要加密的数据
       * @param {string} key - 密钥
       * @returns {string} 加密后的字符串
       */
      static aesEncrypt(data, key = CRYPTO_KEY) {
        if (!data) return '';
        
        const keyHex = CryptoJS.enc.Utf8.parse(key);
        const encrypted = CryptoJS.AES.encrypt(data, keyHex, {
          iv: IV,
          mode: CryptoJS.mode.CBC,
          padding: CryptoJS.pad.Pkcs7
        });
        
        return encrypted.toString();
      }
    
      /**
       * AES 解密
       * @param {string} encrypted - 加密后的字符串
       * @param {string} key - 密钥
       * @returns {string} 解密后的字符串
       */
      static aesDecrypt(encrypted, key = CRYPTO_KEY) {
        if (!encrypted) return '';
        
        try {
          const keyHex = CryptoJS.enc.Utf8.parse(key);
          const decrypted = CryptoJS.AES.decrypt(encrypted, keyHex, {
            iv: IV,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
          });
          
          return decrypted.toString(CryptoJS.enc.Utf8);
        } catch (error) {
          console.error('AES解密失败:', error);
          return '';
        }
      }
    
      /**
       * Base64 加密
       * @param {string} data - 要加密的数据
       * @returns {string} Base64字符串
       */
      static base64Encode(data) {
        if (!data) return '';
        const wordArray = CryptoJS.enc.Utf8.parse(data);
        return CryptoJS.enc.Base64.stringify(wordArray);
      }
    
      /**
       * Base64 解密
       * @param {string} data - Base64字符串
       * @returns {string} 解密后的字符串
       */
      static base64Decode(data) {
        if (!data) return '';
        try {
          const parsedWordArray = CryptoJS.enc.Base64.parse(data);
          return parsedWordArray.toString(CryptoJS.enc.Utf8);
        } catch (error) {
          console.error('Base64解密失败:', error);
          return '';
        }
      }
    
      /**
       * MD5 哈希
       * @param {string} data - 要哈希的数据
       * @returns {string} MD5字符串
       */
      static md5(data) {
        if (!data) return '';
        return CryptoJS.MD5(data).toString();
      }
    
      /**
       * SHA256 哈希
       * @param {string} data - 要哈希的数据
       * @returns {string} SHA256字符串
       */
      static sha256(data) {
        if (!data) return '';
        return CryptoJS.SHA256(data).toString();
      }
    
      /**
       * SHA1 哈希
       * @param {string} data - 要哈希的数据
       * @returns {string} SHA1字符串
       */
      static sha1(data) {
        if (!data) return '';
        return CryptoJS.SHA1(data).toString();
      }
    
      /**
       * HMAC SHA256 签名
       * @param {string} data - 要签名的数据
       * @param {string} secret - 密钥
       * @returns {string} 签名
       */
      static hmacSha256(data, secret) {
        if (!data || !secret) return '';
        return CryptoJS.HmacSHA256(data, secret).toString();
      }
    
      /**
       * 生成随机字符串
       * @param {number} length - 长度
       * @returns {string} 随机字符串
       */
      static generateRandomString(length = 16) {
        const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        let result = '';
        const randomValues = new Uint8Array(length);
        crypto.getRandomValues(randomValues);
        
        for (let i = 0; i < length; i++) {
          result += chars[randomValues[i] % chars.length];
        }
        
        return result;
      }
    
      /**
       * 对象加密
       * @param {object} obj - 要加密的对象
       * @param {string} key - 密钥
       * @returns {string} 加密后的字符串
       */
      static encryptObject(obj, key = CRYPTO_KEY) {
        if (!obj) return '';
        const jsonString = JSON.stringify(obj);
        return this.aesEncrypt(jsonString, key);
      }
    
      /**
       * 对象解密
       * @param {string} encrypted - 加密的字符串
       * @param {string} key - 密钥
       * @returns {object} 解密后的对象
       */
      static decryptObject(encrypted, key = CRYPTO_KEY) {
        if (!encrypted) return null;
        
        try {
          const decrypted = this.aesDecrypt(encrypted, key);
          return JSON.parse(decrypted);
        } catch (error) {
          console.error('对象解密失败:', error);
          return null;
        }
      }
    }
    
    export default CryptoUtils;
    

    组件中使用示例:

    <template>
      <div class="crypto-demo">
        <h2>Crypto-JS 加密演示</h2>
        
        <!-- 输入区域 -->
        <div class="input-section">
          <textarea v-model="inputText" placeholder="输入要加密的文本" rows="4" />
          <input v-model="secretKey" placeholder="加密密钥(可选)" type="password" />
        </div>
    
        <!-- 操作按钮 -->
        <div class="actions">
          <button @click="handleAES">AES 加密/解密</button>
          <button @click="handleBase64">Base64 编码/解码</button>
          <button @click="handleHash">哈希计算</button>
          <button @click="handleObject">对象加密</button>
        </div>
    
        <!-- 结果显示 -->
        <div class="results">
          <div v-if="aesResult">
            <h3>AES 结果:</h3>
            <p>加密: {{ aesResult.encrypted }}</p>
            <p>解密: {{ aesResult.decrypted }}</p>
          </div>
    
          <div v-if="base64Result">
            <h3>Base64 结果:</h3>
            <p>编码: {{ base64Result.encoded }}</p>
            <p>解码: {{ base64Result.decoded }}</p>
          </div>
    
          <div v-if="hashResult">
            <h3>哈希结果:</h3>
            <p>MD5: {{ hashResult.md5 }}</p>
            <p>SHA256: {{ hashResult.sha256 }}</p>
            <p>SHA1: {{ hashResult.sha1 }}</p>
          </div>
    
          <div v-if="objectResult">
            <h3>对象加密结果:</h3>
            <p>原始对象: {{ JSON.stringify(objectResult.original) }}</p>
            <p>加密后: {{ objectResult.encrypted }}</p>
            <p>解密后: {{ JSON.stringify(objectResult.decrypted) }}</p>
          </div>
        </div>
      </div>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    import CryptoUtils from '@/utils/crypto';
    
    // 响应式数据
    const inputText = ref('Hello Crypto-JS!');
    const secretKey = ref('my-secret-key');
    const aesResult = ref(null);
    const base64Result = ref(null);
    const hashResult = ref(null);
    const objectResult = ref(null);
    
    // AES 加密解密
    const handleAES = () => {
      if (!inputText.value.trim()) {
        alert('请输入文本');
        return;
      }
    
      const encrypted = CryptoUtils.aesEncrypt(inputText.value, secretKey.value);
      const decrypted = CryptoUtils.aesDecrypt(encrypted, secretKey.value);
      
      aesResult.value = { encrypted, decrypted };
      console.log('AES 加密:', encrypted);
      console.log('AES 解密:', decrypted);
    };
    
    // Base64 编码解码
    const handleBase64 = () => {
      if (!inputText.value.trim()) {
        alert('请输入文本');
        return;
      }
    
      const encoded = CryptoUtils.base64Encode(inputText.value);
      const decoded = CryptoUtils.base64Decode(encoded);
      
      base64Result.value = { encoded, decoded };
    };
    
    // 哈希计算
    const handleHash = () => {
      if (!inputText.value.trim()) {
        alert('请输入文本');
        return;
      }
    
      hashResult.value = {
        md5: CryptoUtils.md5(inputText.value),
        sha256: CryptoUtils.sha256(inputText.value),
        sha1: CryptoUtils.sha1(inputText.value)
      };
    };
    
    // 对象加密
    const handleObject = () => {
      const testObject = {
        id: 1,
        name: '测试用户',
        email: 'user@example.com',
        timestamp: Date.now(),
        data: {
          role: 'admin',
          permissions: ['read', 'write', 'delete']
        }
      };
    
      const encrypted = CryptoUtils.encryptObject(testObject, secretKey.value);
      const decrypted = CryptoUtils.decryptObject(encrypted, secretKey.value);
      
      objectResult.value = {
        original: testObject,
        encrypted,
        decrypted
      };
    };
    </script>
    
    <style scoped>
    .crypto-demo {
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    
    .input-section {
      margin-bottom: 20px;
    }
    
    textarea, input {
      width: 100%;
      margin-bottom: 10px;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 16px;
    }
    
    .actions {
      display: flex;
      gap: 10px;
      margin-bottom: 20px;
      flex-wrap: wrap;
    }
    
    button {
      padding: 10px 20px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 16px;
      transition: background-color 0.3s;
    }
    
    button:hover {
      background-color: #45a049;
    }
    
    .results > div {
      margin-top: 20px;
      padding: 15px;
      background-color: #f5f5f5;
      border-radius: 4px;
      border-left: 4px solid #4CAF50;
    }
    
    h3 {
      margin-top: 0;
      color: #333;
    }
    
    p {
      margin: 5px 0;
      word-break: break-all;
      font-family: monospace;
      background-color: #fff;
      padding: 5px;
      border-radius: 3px;
    }
    </style>
    

    敏感数据本地存储

    // 加密存储到 localStorage
    const saveSecureData = (key, data) => {
      const encrypted = CryptoUtils.aesEncrypt(
        JSON.stringify(data), 
        'your-secret-key'
      );
      localStorage.setItem(key, encrypted);
    };
    
    // 从 localStorage 解密读取
    const getSecureData = (key) => {
      const encrypted = localStorage.getItem(key);
      if (!encrypted) return null;
      
      return CryptoUtils.decryptObject(encrypted, 'your-secret-key');
    };
    
  7. 封装加密工具函数2

    // src/utils/crypto.js
    import CryptoJS from 'crypto-js'
    
    // 🔒 密钥和偏移量(建议从环境变量读取,避免硬编码!)
    // AES-128 需 16 位密钥,AES-256 需 32 位密钥;CBC 模式必须 16 位 iv
    const AES_KEY = CryptoJS.enc.Utf8.parse(import.meta.env.VITE_AES_KEY || '1234567890abcdef') // 16位
    const AES_IV = CryptoJS.enc.Utf8.parse(import.meta.env.VITE_AES_IV || 'abcdef1234567890')  // 16位
    
    /**
     * AES 加密(CBC 模式 + PKCS7 填充,前端后端需一致)
     * @param {string} data - 待加密的原始数据(字符串)
     * @returns {string} 加密后的 Base64 字符串(便于传输)
     */
    export function aesEncrypt(data) {
      if (!data || typeof data !== 'string') return ''
      const src = CryptoJS.enc.Utf8.parse(data)
      const encrypted = CryptoJS.AES.encrypt(src, AES_KEY, {
        iv: AES_IV,
        mode: CryptoJS.mode.CBC, // 加密模式(常用 CBC/ECB,ECB 无需 iv)
        padding: CryptoJS.pad.Pkcs7 // 填充方式(与后端匹配!)
      })
      return encrypted.ciphertext.toString(CryptoJS.enc.Base64)
    }
    
    /**
     * AES 解密(与加密配置完全对应,否则解密失败)
     * @param {string} encryptedData - 加密后的 Base64 字符串
     * @returns {string} 解密后的原始数据
     */
    export function aesDecrypt(encryptedData) {
      if (!encryptedData) return ''
      const encryptedHex = CryptoJS.enc.Base64.parse(encryptedData)
      const src = CryptoJS.enc.Base64.stringify(encryptedHex)
      const decrypted = CryptoJS.AES.decrypt(src, AES_KEY, {
        iv: AES_IV,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
      })
      // 转换为 UTF-8 字符串并去除多余填充
      return decrypted.toString(CryptoJS.enc.Utf8).trim()
    }
    
    /**
     * MD5 加密(不可逆,适用于密码存储、数据校验)
     * @param {string} data - 待加密数据
     * @returns {string} 32位小写 MD5 哈希值
     */
    export function md5Encrypt(data) {
      if (!data) return ''
      return CryptoJS.MD5(data).toString()
    }
    
    /**
     * SHA256 加密(不可逆,安全性高于 MD5)
     * @param {string} data - 待加密数据
     * @returns {string} SHA256 哈希值
     */
    export function sha256Encrypt(data) {
      if (!data) return ''
      return CryptoJS.SHA256(data).toString()
    }
    

    在 Vue3 中,环境变量需以 VITE_ 前缀开头(Vite 构建工具要求),避免密钥硬编码到代码中(防止泄露)。

    在项目根目录创建 .env.development(开发环境)和 .env.production(生产环境)文件:

    # .env.development(开发环境)
    VITE_AES_KEY=1234567890abcdef # 16位密钥(AES-128)
    VITE_AES_IV=abcdef1234567890  # 16位偏移量(CBC模式用)
    
    # .env.production(生产环境)
    VITE_AES_KEY=prod1234567890abc # 生产环境密钥(需与后端一致)
    VITE_AES_IV=prodabcdef12345678 # 生产环境偏移量
    

    Vue3 组件中使用(Composition API)在需要加密 / 解密的组件中,直接导入工具函数使用,示例如下:

    <!-- src/components/CryptoDemo.vue -->
    <template>
      <div class="crypto-demo">
        <h3>Vue3 + Crypto-JS 示例</h3>
        
        <!-- AES 加密/解密 -->
        <div class="aes-section">
          <input 
            v-model="rawData" 
            placeholder="输入待加密数据(AES)"
            class="input"
          />
          <button @click="handleAesEncrypt" class="btn">AES 加密</button>
          <button @click="handleAesDecrypt" class="btn">AES 解密</button>
          <p>加密后:<code>{{ aesEncrypted }}</code></p>
          <p>解密后:<code>{{ aesDecrypted }}</code></p>
        </div>
    
        <!-- MD5 加密(不可逆) -->
        <div class="md5-section">
          <input 
            v-model="pwdData" 
            placeholder="输入密码(MD5 加密)"
            class="input"
          />
          <button @click="handleMd5Encrypt" class="btn">MD5 加密</button>
          <p>MD5 结果:<code>{{ md5Result }}</code></p>
        </div>
      </div>
    </template>
    
    <script setup>
    import { ref } from 'vue'
    // 导入封装的加密函数
    import { aesEncrypt, aesDecrypt, md5Encrypt } from '@/utils/crypto'
    
    // 响应式数据
    const rawData = ref('hello crypto-js') // AES 原始数据
    const aesEncrypted = ref('') // AES 加密结果
    const aesDecrypted = ref('') // AES 解密结果
    const pwdData = ref('123456') // MD5 原始密码
    const md5Result = ref('') // MD5 加密结果
    
    // AES 加密触发
    const handleAesEncrypt = () => {
      aesEncrypted.value = aesEncrypt(rawData.value)
      aesDecrypted.value = '' // 清空解密结果
    }
    
    // AES 解密触发
    const handleAesDecrypt = () => {
      aesDecrypted.value = aesDecrypt(aesEncrypted.value)
    }
    
    // MD5 加密触发
    const handleMd5Encrypt = () => {
      md5Result.value = md5Encrypt(pwdData.value)
    }
    </script>
    
    <style scoped>
    .crypto-demo {
      padding: 20px;
      max-width: 800px;
      margin: 0 auto;
    }
    .input {
      width: 300px;
      padding: 8px;
      margin-right: 10px;
      margin-bottom: 15px;
    }
    .btn {
      padding: 8px 16px;
      margin-right: 10px;
      cursor: pointer;
      background: #42b983;
      color: white;
      border: none;
      border-radius: 4px;
    }
    code {
      color: #e53e3e;
      background: #f5f5f5;
      padding: 2px 4px;
      border-radius: 4px;
    }
    section {
      margin-bottom: 30px;
    }
    </style>
    
  8. 9

--- file-saver文件下载核心插件

  1. file-saver是什么插件?

    答:FileSaver.js是一个流行的 JavaScript 库,用于在浏览器中保存文件/本地下载(如文本、图片、PDF 、JSON、Blob等)到用户本地设备。

  2. 下载第三方插件“file-saver”:

    npm install file-saver
    或
    pnpm add file-saver
    或
    yarn add file-saver
    
  3. TypeScript 支持(可选)

    npm install @types/file-saver --save-dev
    
  4. 核心 API 与基础用法

    saveAs(blob/文件对象/URL, 文件名, 可选配置);
    
  5. 封装复用

    // 导入
    import { saveAs } from 'file-saver';
    
    /**
     * 通用文件下载函数
     * @param {string/object} content 下载内容(文本/JSON 对象)
     * @param {string} fileName 文件名(含后缀,如:data.json)
     * @param {string} type 文件 MIME 类型,默认:text/plain
     */
    export const useFileDownload = (content, fileName, type = 'text/plain;charset=utf-8') => {
      let blob;
      // 区分 JSON 对象和普通文本
      if (typeof content === 'object') {
        blob = new Blob([JSON.stringify(content, null, 2)], {
          type: 'application/json;charset=utf-8'
        });
      } else {
        blob = new Blob([content], { type });
      }
      // 处理中文文件名乱码(可选)
      const safeFileName = encodeURIComponent(fileName);
      saveAs(blob, safeFileName);
    };
    

    组件中调用封装函数:

    <template>
      <button @click="downloadDemo">调用封装函数下载</button>
    </template>
    
    <script setup>
    import { useFileDownload } from '@/composables/useFileDownload';
    
    const downloadDemo = () => {
      // 下载 JSON 示例
      useFileDownload(
        { id: 1, name: '测试数据' },
        '封装下载示例.json',
        'application/json;charset=utf-8'
      );
    };
    </script>
    
  6. 场景 1:下载文本 / JSON 文件(最常用)

    <template>
      <button @click="downloadText">下载文本文件</button>
      <button @click="downloadJSON">下载JSON数据</button>
    </template>
    
    <script setup>
    import { saveAs } from 'file-saver';
    
    // 下载纯文本
    const downloadText = () => {
      // 1. 构建文本内容
      const content = 'Vue3 + file-saver 文本下载示例\n自定义换行\n中文正常显示';
      // 2. 转为 Blob(指定 UTF-8 编码避免中文乱码)
      const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
      // 3. 触发下载
      saveAs(blob, '自定义文本文件名.txt');
    };
    
    // 下载JSON文件
    const downloadJSON = () => {
      // 1. 构建JSON数据
      const jsonData = {
        list: [{ name: '张三', age: 25 }, { name: '李四', age: 30 }],
        time: new Date().toLocaleString()
      };
      // 2. JSON转字符串并生成Blob(缩进2格便于阅读)
      const blob = new Blob(
        [JSON.stringify(jsonData, null, 2)],
        { type: 'application/json;charset=utf-8' }
      );
      // 3. 下载
      saveAs(blob, '用户数据.json');
    };
    </script>
    
  7. 场景 2:下载图片 / 二进制文件

    <template>
      <button @click="downloadImage">下载在线图片</button>
    </template>
    
    <script setup>
    import { saveAs } from 'file-saver';
    
    // 下载图片(支持在线/本地图片)
    const downloadImage = async () => {
      try {
        // 1. 拉取图片为 Blob(fetch 支持跨域需服务端允许)
        const response = await fetch('https://picsum.photos/200/100');
        const blob = await response.blob();
        // 2. 下载(指定文件名和格式)
        saveAs(blob, '风景图.png', { autoBom: false }); // autoBom:禁用自动编码(图片无需)
      } catch (err) {
        console.error('图片下载失败:', err);
        alert('下载失败,请检查网络');
      }
    };
    </script>
    
  8. 场景 3:下载 CSV(模拟 Excel)

    <template>
      <button @click="downloadCSV">下载Excel格式数据</button>
    </template>
    
    <script setup>
    import { saveAs } from 'file-saver';
    
    // 下载 CSV(Excel 可直接打开)
    const downloadCSV = () => {
      // 1. 构建 CSV 内容(逗号分隔,换行用 \n)
      const csvContent = `姓名,年龄,职业
    张三,25,前端开发
    李四,30,后端开发
    王五,28,UI设计`;
      // 2. 生成 Blob(指定 CSV 类型)
      const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' });
      // 3. 下载
      saveAs(blob, '人员信息.csv');
    };
    </script>
    
  9. 带样式 + 新窗口下载

    <template>
      <div style="padding: 20px;">
        <button 
          style="padding: 8px 16px; cursor: pointer;"
          @click="downloadWithStyle"
        >
          带样式下载(新窗口)
        </button>
      </div>
    </template>
    
    <script setup>
    import { saveAs } from 'file-saver';
    
    // 新窗口下载 + 样式优化
    const downloadWithStyle = () => {
      const content = '新窗口下载示例,点击后保留原页面';
      const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
      
      // 新窗口下载(通过 a 标签模拟)
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = '新窗口下载.txt';
      a.target = '_blank'; // 新窗口打开
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url); // 释放 URL 资源
    };
    </script>
    

--- @tanstack/vue-virtual 虚拟化列表

  1. TanStack Virtual是什么插件:

    TanStack Virtual 是一款无头界面工具,用于虚拟化 JS/TS、React、Vue、Svelte、Solid、Lit 和 Angular 中的大量元素列表。anStack Virtual 的核心是虚拟化器。虚拟化器可以选择垂直(默认)或水平轴,这使得通过将两个轴配置结合实现垂直、水平甚至网格状虚拟化。

  2. 下载第三方插件“@tanstack/vue-virtual”:

    npm install @tanstack/vue-virtual
    或
    pnpm add @tanstack/vue-virtual
    或
    yarn add @tanstack/vue-virtual
    
  3. 基本配置项:

    /*************** 配置项 ***************/
    const virtualizer = useVirtualizer(computed(()=>({
      count: 1000,                    // 总项目数量
      getScrollElement: () => parentRef.value, // 滚动容器元素
      estimateSize: () => 50,         // 估算每项高度
      overscan: 5,                    // 缓冲区项目数量
      horizontal: false,              // 是否水平滚动
      paddingStart: 0,               // 起始填充
      paddingEnd: 0,                 // 结束填充
      scrollMargin: 0,               // 滚动边距【滚动边界预留空间(像素)】
      gap: 0,                        // 项目间距
      indexAttribute: 'data-index',   // 索引属性名
      initialOffset: 0,              // 初始偏移量
      getItemKey: (index) => index,   // 获取项目key的函数
      rangeExtractor: defaultRangeExtractor, // 范围提取器
      measureElement: undefined,      // 测量元素函数
      scrollToFn: elementScrollToFn,  // 滚动函数
      // ...其余的配置可以到官网进行查询
    })));
    
    /*************** 属性 ***************/
    
    // 获取当前虚拟项目列表(每一项的索引值,key值,起始位置或结束位置等等...)
    virtualizer.value.getVirtualItems(); // 用于循环渲染列表项【***实际要渲染的dom参数以及数量***】
    // 获取总的虚拟尺寸 return<number>
    virtualizer.value.getTotalSize();
    
    /*************** 方法 ***************/
    // 滚动到指定索引
    virtualizer.value.scrollToIndex(index, { index: number, options?: { align: 'start'\|'center'\|'end', behavior: 'auto'\|'smooth' })
    // 滚动到指定偏移量
    virtualizer.value.scrollToOffset(offset, { offset: number, options?: { align: 'start'\|'center'\|'end', behavior: 'auto'\|'smooth' });
    // 重新测量所有项目
    virtualizer.value.measure();
    // 重新测量某一项的高度
    virtualizer.value.measureElement(el);
    // 强制重新计算布局
    virtualizer.value.update();
    // 重置状态
    virtualizer.value.reset();
    
  4. 【固定高度】使用示例:

    <template>
      <!-- 外层滚动容器(必须添加)【滚动条层】 -->
       <div @scroll="handleScroll" ref="customAudioVirtualList" class="custom-audio-virtualList" :style="{ height: `${160}px` }">
        <!-- 虚拟滚动占位层【item项采用transform,必需需要外层去撑开盒子】 -->
          <ul :style="{ height: `${totalSize}px`, position: 'relative' }">
                <li class="item" 
                    v-for="row in virtualRows"
                    :key="row.index"
                    :style="{
                        position: 'absolute',
                        top: 0,
                        left: 0,
                        width: '100%',
                        transform: `translateY(${row.start}px)`,
                    }"
                    >
                  项目 {{ row.index }}:{{ row }}
                </li>
          </ul>
       </div>
    </template>
    
    <script setup>
    import { onMounted, ref, nextTick, computed } from 'vue'
    import { useVirtualizer } from '@tanstack/vue-virtual'
    
    
    const itemsList = ref([1,2,3,4,5,6,7,8]);
    
    setTimeout(() => {
      // 数据
      itemsList.value = Array.from({ length: 100000 }, (_, i) => i)
    }, 3000);
    
    // 滚动容器
    const customAudioVirtualList = ref(null);
    
    // 创建虚拟滚动器的函数 ---- 使用 computed - 修复响应式问题
    const virtualizer = useVirtualizer(
      computed(()=>({
        // 使用最新数据长度
        count: itemsList.value.length,
        // 绑定滚动容器
        getScrollElement: ()=> customAudioVirtualList.value,
        // 可视区域上下额外渲染5项,优化滚动过渡体验
        overscan: 2,
        // 固定高度场景:直接返回单个项高度(性能最优)
        estimateSize: () => 77,
        // containerHeight: 160, // 容器固定高度
        // 起始填充
        paddingStart: 10,
        // 项目间距
        gap: 10, // item项之间的距离
      }))
    )
    
    onMounted(async ()=>{
      // 确保 DOM 完全渲染
      await nextTick() 
      // 定位到某一个索引
      const index = 6;
      // 定位
      virtualizer.value.scrollToIndex(index ,{ align: 'center' });
    })
    
    // 总数据 【***实际要渲染的dom参数以及数量***】
    const virtualRows = computed(() => virtualizer.value.getVirtualItems() || [])
    // item项总体高度
    const totalSize = computed(() => virtualizer.value.getTotalSize() || 0)
    
    // 滚动同步div盒子
    const handleScroll = ({ target }) => virtualizer.value.scrollToOffset(target.scrollTop);
    </script>
    
    <style scoped>
    .custom-audio-virtualList{
      background: pink;
      overflow-y: auto;
    }
    .item {
      margin-top: 5px;
      padding: 12px;
      border-bottom: 1px solid #eee;
      background: white;
    
      box-sizing: border-box;
      background-color: aqua;
      height: 77px;
      overflow: hidden;
    }
    </style>
    
  5. 【动态高度】使用示例:

    关键点:
    <li
    	v-for="row in virtualRows"
    	:key="row.index"
    	:data-index='row.index'
    	:ref="measureElement">
    </li>
    /*** 必须要有data-index属性,并赋值。且并指定ref ***/
    
    // 动态计算
    const measureElement = (el: HTMLElement | null) => {
      // 测量实际高度(通知virtualizer重新测量当前dom元素)
      if (el) virtualizer.value.measureElement(el);
    }
    

    @tanstack/vue-virtual官网

自定义指令

--- 搜索匹配的文本高亮

1). 低阶版
// 注册全局
app.directive("highlightTxt", (el: any, binding: { value: { search: string, txt: string } }) => {
  // 搜索的文字
  const searchTerm = binding.value.search.trim();
  // 当前item项的文字
  const txt = binding.value.txt;
  // 清除高亮
  if (!searchTerm) return (el.innerHTML = txt);

  // 转义正则特殊字符
  const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const regex = new RegExp(`(${escapedTerm})`, "gi");

  // 替换匹配文本
  el.innerHTML = txt.replace(regex, '<span class="highlight">$1</span>');
});


/** style.css 编写样式 */
/* 代码高亮 */
.highlight {
  background: linear-gradient(120deg, #fff9c4 0%, #ffecb3 100%);
  padding: 0 4px;
  border-radius: 4px;
  font-weight: 700;
  color: #d35400;
}

使用方法

<p v-highlightTxt="{txt: '文本', search: '搜索的内容'}">文本</p>
2). 进阶版
/**
 * 自定义文字高亮指令(带节流+XSS防护)
 * 功能:根据搜索关键词实时高亮元素中的匹配文本,通过节流减少高频DOM操作
 * 适用场景:搜索输入实时高亮、列表筛选高亮等
 */
export default {
  /**
   * 指令绑定到元素时执行(初始化阶段)
   * @param el 绑定的DOM元素(需是HTMLElement类型,确保可操作innerHTML)
   * @param binding 指令绑定值,格式:{ search: 搜索关键词, txt: 待高亮的原始文本 }
   */
  mounted(el: HTMLElement, binding: { value: { search: string; txt: string } }) {
    /**
     * 节流后的高亮核心逻辑
     * @param searchTerm 搜索关键词(可能为空)
     * @param txt 待高亮的原始文本
     * 节流延迟:200ms(可根据需求调整,输入类场景建议100-200ms)
     */
    const highlightThrottle = throttle((searchTerm: string, txt: string) => {
      // 无搜索关键词时,直接渲染原始文本(清除高亮)
      if (!searchTerm.trim()) {
        el.innerHTML = escapeHtml(txt); // 始终转义,防止XSS
        return;
      }

      // 转义正则特殊字符(如 . * + ? 等),避免正则匹配异常
      const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      // 构建正则:g-全局匹配,i-忽略大小写
      const regex = new RegExp(`(${escapedTerm})`, 'gi');

      // 1. 先转义原始文本(防XSS)→ 2. 用正则匹配关键词 → 3. 包裹高亮样式
      el.innerHTML = escapeHtml(txt).replace(regex, '<span class="highlight">$1</span>');
    }, 200);

    // 将节流函数挂载到el上,供updated钩子复用(避免重复创建节流函数)
    (el as any)._highlightThrottle = highlightThrottle;

    // 初始化执行一次高亮(确保页面加载时就有正确的显示)
    const { search, txt } = binding.value;
    highlightThrottle(search, txt);
  },

  /**
   * 绑定值(search/txt)变化时执行(更新阶段)
   * @param el 绑定的DOM元素
   * @param binding 包含新值(value)和旧值(oldValue)的绑定对象
   */
  updated(
    el: HTMLElement,
    binding: {
      value: { search: string; txt: string };
      oldValue: { search: string; txt: string };
    }
  ) {
    const { search: newSearch, txt: newTxt } = binding.value;
    const { search: oldSearch, txt: oldTxt } = binding.oldValue;

    // 优化:只有搜索词或原始文本真的变化时,才触发高亮(避免无效执行)
    if (newSearch === oldSearch && newTxt === oldTxt) return;

    // 执行节流后的高亮逻辑(从el上获取之前挂载的节流函数)
    (el as any)._highlightThrottle(newSearch, newTxt);
  },

  /**
   * 指令解绑时执行(清理阶段)
   * 作用:避免内存泄漏(清除未执行的节流定时器、删除el上的挂载属性)
   * @param el 绑定的DOM元素
   */
  unmounted(el: HTMLElement) {
    // 获取el上挂载的节流函数
    const throttleFn = (el as any)._highlightThrottle;

    // 若节流函数存在取消方法,执行取消(清除未触发的定时器)
    if (throttleFn && typeof throttleFn.cancel === 'function') {
      throttleFn.cancel();
    }

    // 删除el上的自定义属性,避免内存泄漏
    delete (el as any)._highlightThrottle;
  },
};

/**
 * 节流函数实现(适配浏览器环境)
 * 功能:控制函数在指定时间内最多执行一次,避免高频调用
 * @param fn 待节流的目标函数
 * @param delay 节流延迟时间(毫秒)
 * @returns 节流后的函数(附带cancel方法,用于取消未执行的任务)
 */
function throttle<T extends (...args: any[]) => void>(
  fn: T,
  delay: number
): T & { cancel: () => void } {
  let timer: number | null = null; // 浏览器环境定时器类型为number(修复NodeJS.Timeout报错)
  let lastExecTime = 0; // 上次函数执行时间戳

  /**
   * 节流包装函数
   * @param this 显式声明为void,修复"this隐式具有类型any"报错(无需绑定this)
   * @param args 目标函数的参数列表
   */
  const throttled = function (this: void, ...args: Parameters<T>) {
    const now = Date.now(); // 当前时间戳
    const elapsed = now - lastExecTime; // 距离上次执行的时间差

    // 情况1:距离上次执行超过延迟时间 → 直接执行目标函数
    if (elapsed >= delay) {
      lastExecTime = now; // 更新上次执行时间
      fn(...args); // 直接调用目标函数(无需绑定this,因声明this为void)
    } else {
      // 情况2:未超过延迟时间 → 取消之前的定时器,重新设置
      if (timer) clearTimeout(timer); // 清除旧定时器,避免重复执行
      timer = setTimeout(() => {
        lastExecTime = Date.now(); // 更新上次执行时间
        fn(...args); // 延迟后执行目标函数
        timer = null; // 定时器执行后重置为null
      }, delay - elapsed); // 剩余延迟时间 = 总延迟 - 已过去时间
    }
  } as T & { cancel: () => void };

  /**
   * 取消节流任务的方法
   * 作用:在指令解绑时清除未执行的定时器,避免内存泄漏
   */
  throttled.cancel = () => {
    if (timer) clearTimeout(timer); // 清除定时器
    timer = null; // 重置定时器状态
    lastExecTime = 0; // 重置上次执行时间戳
  };

  return throttled;
}

/**
 * HTML转义函数(防XSS攻击核心)
 * 作用:将HTML特殊字符转义为实体,避免恶意脚本注入
 * 需转义的特殊字符:& < > " '
 */
function escapeHtml(str: string): string {
  return str
    .replace(/&/g, '&amp;') // & → &amp;
    .replace(/</g, '&lt;') // < → &lt;
    .replace(/>/g, '&gt;') // > → &gt;
    .replace(/"/g, '&quot;') // " → &quot;
    .replace(/'/g, '&#39;'); // ' → &#39;
}

/**
 * 使用说明:
 * 1. 在Vue中注册指令:Vue.directive('highlight', highlightDirective)
 * 2. 模板中使用:<div v-highlight="{ search: 搜索关键词, txt: 原始文本 }"></div>
 * 3. 需添加高亮样式(示例):
 *    .highlight {
 *      background-color: #fff2cc; // 浅黄色背景
 *      color: #d97706; // 橙色文字
 *      padding: 0 2px;
 *      border-radius: 2px;
 *    }
 * 4. 调整建议:
 *    - 节流延迟:输入类场景用100-200ms,筛选类用300ms
 *    - 若需"输入停止后立即高亮",可将throttle改为debounce(防抖)
 *    - 若txt是可信HTML片段(不建议),可移除escapeHtml调用
 */

--- 元素平滑上升

// y轴偏移的默认值
const DISTANCE = 50;

// 弱映射:存储元素与动画实例的关联(避免内存泄漏,元素销毁时自动释放)
const map = new WeakMap<HTMLElement, Animation>();

// ========================= IntersectionObserver配置 =========================
/**
 * 观察器配置项 - 优化元素交叉判定精度
 * root: 默认null(视口作为根元素)
 * rootMargin: 根元素边界扩展/收缩(下边界扩展DISTANCE,让元素快进入视口时提前触发动画)
 * threshold: 交叉阈值(元素可见比例≥30%时才判定为"交叉",避免1px误触发)
 */
const observerOptions: IntersectionObserverInit = {
  rootMargin: `0px 0px ${DISTANCE}px 0px`,
  threshold: 0.3,
};

// ========================= 创建IntersectionObserver实例 =========================
/**
 * 元素可见性监听器 - 监听元素是否进入视口
 * 核心逻辑:元素交叉(可见)时,播放预存的动画并清理资源
 */
const ob = new IntersectionObserver((entries) =>{
    for (const entry of entries) {
        // 仅当元素处于交叉状态(可见)时执行
        if(entry.isIntersecting){
            // 从映射中获取当前元素的动画实例
            const animation = map.get(entry.target as HTMLElement);
            // 播放动画(存在动画实例时才执行)
            animation?.play();
            // 动画播放后,从映射中删除(确保只播放一次)
            map.delete(entry.target as HTMLElement);
            // 取消对当前元素的监听(减少性能消耗)
            ob.unobserve(entry.target);
        }
    }
}, observerOptions);


export default{
    mounted(el: HTMLElement, binding: { value: { firstScreen: Boolean } }){  // firstScreen为true时表示第一屏也有动画,默认为false
        // 条件判断:非首屏动画 + 元素在视口外 → 跳过动画(直接显示元素)
        // 反之(首屏动画 或 元素在视口内)→ 初始化动画
        if(!(el.getBoundingClientRect().top > window.innerHeight) && !binding.value?.firstScreen) return;

        // 创建动画
        const animation = el.animate([
            // 动画起始状态:向下偏移DISTANCE像素 + 完全透明
            // { transform: `translateY(${DISTANCE}px)`, opacity: 1 },
            { transform: `translate3d(0, ${DISTANCE}px, 0)`, opacity: 0 },
            // 动画结束状态:偏移量为0 + 完全不透明
            // { transform: `translateY(0)`, opacity: 1 }
            { transform: `translate3d(0, 0, 0)`, opacity: 1 }
        ],{
            // 动画时长
            duration: 300,
            // 时间函数
            // easing: 'cubic-bezier(1, 0.6, 0.6, 1)',
            easing: 'ease-out',
            // 填充模式(动画前后元素状态)
            // fill: 'forwards',
        })
        
        // 初始暂停动画(等待元素进入视口后再播放)
        animation.pause();
        // 将动画实例存入映射(与元素关联)
        map.set(el, animation);
        // 开始监听当前元素的可见性
        ob.observe(el);
    },
    // 卸载时移除监听
    unmounted(el: HTMLElement){
        // 从映射中获取动画实例并取消(避免动画残留)
        const animation = map.get(el);
        // 全部清除
        animation?.cancel();
        // 从映射中删除元素关联
        map.delete(el);
        // 取消对元素的可见性监听
        ob.unobserve(el);
    }
}


// 直接注册使用即可
app.directive('slideIn', slideIn);

// 指令使用 firstScreen表示第一屏是否就开启动画,默认不开启
v-slide-in="{ firstScreen: true }"

--- 缩放动画

/********************* 缩放动画 *********************/

// 弱映射:存储元素与动画实例的关联(避免内存泄漏,元素销毁时自动释放)
const map = new WeakMap<HTMLElement, Animation>();

// ========================= IntersectionObserver配置 =========================
/**
 * 观察器配置项 - 优化元素交叉判定精度
 * root: 默认null(视口作为根元素)
 * rootMargin: 根元素边界扩展/收缩(下边界扩展DISTANCE,让元素快进入视口时提前触发动画)
 * threshold: 交叉阈值(元素可见比例≥30%时才判定为"交叉",避免1px误触发)
 */
const observerOptions: IntersectionObserverInit = {
  threshold: 0.3,
};

// ========================= 创建IntersectionObserver实例 =========================
/**
 * 元素可见性监听器 - 监听元素是否进入视口
 * 核心逻辑:元素交叉(可见)时,播放预存的动画并清理资源
 */
const ob = new IntersectionObserver((entries) =>{
    for (const entry of entries) {
        // 仅当元素处于交叉状态(可见)时执行
        if(entry.isIntersecting){
            // 从映射中获取当前元素的动画实例
            const animation = map.get(entry.target as HTMLElement);
            // 播放动画(存在动画实例时才执行)
            animation?.play();
            // 动画播放后,从映射中删除(确保只播放一次)
            map.delete(entry.target as HTMLElement);
            // 取消对当前元素的监听(减少性能消耗)
            ob.unobserve(entry.target);
        }
    }
}, observerOptions);


export default{  //  firstScreen: 第一屏是否立即执行动画.
    mounted(el: HTMLElement, binding: { value: { firstScreen: boolean } }){  // firstScreen为true时表示第一屏也有动画,默认为false
        // 条件判断:非首屏动画 + 元素在视口外 → 跳过动画(直接显示元素)
        // 反之(首屏动画 或 元素在视口内)→ 初始化动画
        if(!(el.getBoundingClientRect().top > window.innerHeight) && !binding.value?.firstScreen) return;
        
        // 创建动画
        const animation = el.animate([
            // 动画起始状态:向下偏移DISTANCE像素 + 完全透明
            { scale: 0.9, opacity: 0 },
            // 动画结束状态:偏移量为0 + 完全不透明
            { scale: 1, opacity: 1 }
        ],{
            // 动画时长
            duration: 300,
            easing: 'ease-out',
        })
        
        // 初始暂停动画(等待元素进入视口后再播放)
        animation.pause();
        // 将动画实例存入映射(与元素关联)
        map.set(el, animation);
        // 开始监听当前元素的可见性
        ob.observe(el);
    },
    // 卸载时移除监听
    unmounted(el: HTMLElement){
        // 从映射中获取动画实例并取消(避免动画残留)
        const animation = map.get(el);
        // 全部清除
        animation?.cancel();
        // 从映射中删除元素关联
        map.delete(el);
        // 取消对元素的可见性监听
        ob.unobserve(el);
    }
}

// 直接注册使用即可
app.directive('scalingIn', scalingIn);

// 指令使用 firstScreen表示第一屏是否就开启动画,默认不开启
v-scaling-in="{ firstScreen: true }"

Vue路由切换动画

--- 右进左出动画

<template>
  <!-- 优化处理 -->
  <div class="contain-container">
    <router-view v-slot="{ Component, route }">
      <transition name="animation" mode="out-in">
        <component :is="Component" :key="route.path"/>
      </transition>
    </router-view>
  </div>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped>
/* 过度动画配置代码 */
// 进入
@keyframes page-enter {
  from { transform: translate3d(25px, 0, 0); opacity: 0; }
  to   { transform: translate3d(0, 0, 0); opacity: 1; }
}

// 离开
@keyframes page-leave {
  from { transform: translate3d(0, 0, 0); opacity: 1; }
  to   { transform: translate3d(25px, 0, 0); opacity: 0; }
}

// 性能优化
.animation-enter-active,
.animation-leave-active {
  /* 提示浏览器该元素将变化,提前优化 */
  will-change: transform, opacity;

  // Firefox浏览器降级处理
  @supports (-moz-appearance: none) {
    will-change: initial;
  }
}

/* 进入效果 */
.animation-enter-active {
  // animation: page-enter 0.5s ease both;
  animation: page-enter 0.5s cubic-bezier(0.25, 0, 0.25, 1) both; // 正常  →   正常   →    慢
}

/* 离开效果 */
.animation-leave-active {
  // animation: page-leave 0.3s cubic-bezier(1, 0.6, 0.6, 1) both; // 老动画,起步很慢-中间突然冲出去-结尾又拖沓
  // animation: page-leave 0.3s cubic-bezier(0.25, 0, 0.75, 1); // 快  ───→   稳定   ───→  减速
  animation: page-leave 0.3s cubic-bezier(0.4 ,0 ,0.8 ,1); // 慢一点  →   加速   →     减速停下
}
</style>

注意事项:

// 左右移动会出现x轴的滚动条,hidden可能会影响其它元素,如:position: sticky;会失效;所以clip不会引发任何问题,完美解决;
body, html {
  // 隐藏x轴滚动条
  overflow-x: clip; //clip: 超出部分直接裁剪;不会创建滚动容器
}

vite配置

--- 默认配置

// vite
import { defineConfig } from 'vite';
// 导入vue
import vue from '@vitejs/plugin-vue';
// vue调试工具
import vueDevTools from 'vite-plugin-vue-devtools';
// element按需引入
import AutoImport from 'unplugin-auto-import/vite';
// 自动注册全局组件
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';

// 路径模块(1)
// import { resolve } from "path";

// 路径导入以及url(2)
import { fileURLToPath, URL } from 'node:url';

export default defineConfig({
  base: "./",
  resolve: {
    alias: {
      // "@": resolve(__dirname, "./src"),
      // comp: resolve(__dirname, "./src/components"),

      '@': fileURLToPath(new URL('./src', import.meta.url)),
      'comp': fileURLToPath(new URL('./src/components', import.meta.url)),
    },
  },
  plugins: [
    vue(),
    // vue调试工具
    vueDevTools(),
    // element按需引入
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
      // 方法1:明确指定只扫描哪些目录
      dirs: ["src/components/base"], // 只自动注册 `src/components/base`下的组件
      // 方法2:使用排除规则(正则表达式)
      // exclude: [
      //   /TopNavigation\.vue/, // 排除 TopNavigation 组件
      //   /loading\index\.vue/,       // 排除 Loading 组件
      //   /Progress\.vue/,       // 排除 progress 组件
      // ],
    }),
  ],
  
  // 生产构建设置
  build:{
    rollupOptions:{
      // 分包
      output:{
        manualChunks: (id)=> {
          // 把第三方依赖进行分包处理
          if (id.includes('node_modules') && (id.endsWith('.js') || id.endsWith('.ts'))) {
            return 'vendor';
          }
        }
      }
    }
  },
  
  server: {
    // 自动打开浏览器进行预览
    open: false,
    // 网络端口号
    port: 9999,
    host: "0.0.0.0",
    // 配置反向代理
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        // target就是你要访问的目标地址,可以是基础地址,这样方便在这个网站的其他api口调用数据
        ws: true, //是否代理 websockets
        secure: false, //是否https接口
        changeOrigin: true, //是否跨域
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
      "/apiOne": {
        target: "http://luke.svvs.top",
        // target就是你要访问的目标地址,可以是基础地址,这样方便在这个网站的其他api口调用数据
        ws: true, //是否代理 websockets
        secure: false, //是否https接口
        changeOrigin: true, //是否跨域
        rewrite: (path) => path.replace(/^\/apiOne/, ""),
      },
    },
  },
});

uniapp

Api Function

--- 下载apk方法
class nativeAPI {
    /**
    * 本地下载api封装
    * @param {*} parameter
    * parameter: {   // 下载地址(http/https)开头
    *     url:文件下载地址,网络连接
    *     fileName:保存本地的文件夹名称(下载后保存到该文件夹内)
    *     appName:文件名称(下载的文件名称)
    * }
    * @param {*} callback (文件下载的进度条进度)(下载成功后返回的对象)
    */
    downLoadApi(parameter, callback) {
        // 创建下载接口
        const dtask = plus.downloader.createDownload(
            // 下载地址
            parameter.url,
            // 本地文件地址(安卓手机路径)
            {
                filename: `file://storage/emulated/0/${parameter.fileName}/${parameter.appName}`,
            },
            function (r, status) {
                if (status == 200) {
                    // 下载成功
                } else {
                    //status为200表示下载成功
                    // 则表示下载失败
                    console.log("下载失败", r);
                    plus.downloader.clear(); //清除下载任务
                }
            }
        );

        // 保存上一次的进度条进度变化的值
        let copyProgress = "";

        // 监听下载进度(每次下载进度变化便会执行一次该回调)
        dtask.addEventListener("statechanged", (task) => {
            // 如果监听对象不存在,则直接退出
            if (!dtask) return;

            /**
             * task.state: 状态:1,2,3,4
             * 1. 表示开始下载
             * 2. 链接到服务器
             * 3. 下载中 (监听进度进度)
             * 4. 下载完成
             */
            switch (task.state) {
                case 3:
                    // 计算下载进度比列
                    const progress = Math.floor(
                        (task.downloadedSize / task.totalSize) * 100
                    );
                    /**
                     * 起因:一样的进度值会多次执行
                     * 解决方法:保留上一次的进度值和目前进度值进行对比,如果相同,表示重复执行立即退出,否则为最新进度,对比参数同步更新
                     */
                    if (copyProgress == progress) return;
                    copyProgress = progress;
                    // 如果进度值为100者退出执行 让其进度下一个判断
                    if (progress == 100) return;
                    callback && callback({ progress, path: "下载中,下载后可获取文件路径" });
                    break;

                case 4:
                    // 状态码为4表示当前下载完成,恢复对比参数
                    copyProgress = "";
                    // 执行完回调函数 (progress: 为进度条进度 )(path为下载后的文件路径)
                    callback && callback({ progress: 100, path: task.filename });
                    break;
            }
        });

        // 开始下载
        dtask.start();
    }

    /**
     * 查找文件是否存在
     * @param {*} callback 
     * callback: {   // 下载地址(http/https)开头
     *     path:文件下载地址,网络连接
     *     success:()=>{}    查询成功后调取的回调函数
     *     fail:()=>{}       查询失败后调取的回调函数
     * }
     */
    lookupFile(callback) {
        plus.io.getFileInfo({
            // 查询的文件路径
            filePath: callback.path,
            // 查询成功
            success: (res) => {
                callback && callback.success && callback.success({ status: 200, size: Number(res.size) / 1024 });
            },
            // 查询失败
            fail: (r) => {
                callback && callback.fail && callback.fail({ status: 201, msg: "文件不存在", ...r });
            },
        });
    }

    /**
     * 调起手机app安装
     * @param {*} path 安装的app路径
     */
    installApp(path) {
        // plus.runtime.openFile(`file://storage/emulated/0/${path}`);
        plus.runtime.openFile(path);
    }

     /**
     * 权限申请
     * @param {*} callback 回调函数(在存储条件允许的情况下执行该函数)
     */
    PermissionApplication(callback) {
        plus.android.requestPermissions(['android.permission.WRITE_EXTERNAL_STORAGE'], function (e) {
            if (e.deniedAlways.length > 0) { //权限被永久拒绝
                // 弹出提示框解释为何需要读写手机储存权限,引导用户打开设置页面开启
                uni.showModal({
                    title: '存储权限',
                    content: '您拒绝了存储权限,请前往设置-应用开启存储权限。',
                    success: function (res) {
                        if (res.confirm) {
                            // console.log('用户点击确定');
                        } else if (res.cancel) {
                            // console.log('用户点击取消');
                        }
                    }
                });
            }

            if (e.deniedPresent.length > 0) { //权限被临时拒绝
                // 弹出提示框解释为何需要读写手机储存权限,可再次调用plus.android.requestPermissions申请权限
                plus.android.requestPermissions(['android.permission.WRITE_EXTERNAL_STORAGE'])
            }

            if (e.granted.length > 0) { //权限被允许
                //调用依赖获取读写手机储存权限的代码
                callback && callback();
            }
        });
    }
}

*** 使用方法

  // 查看当前app是否存在
  nativeAPI.lookupFile({
    // 文件路径
    path: `file://storage/emulated/0/${'LUKES后台管理/LUKES后台管理.apk'}`,
    success(res) {
      // 查询到文件返回文件信息
      // console.log(res, "文件查找成功");
    },
    fail(r) {
      // 没有该文件
      // console.log(res, "文件不存在");
    }
  });


   // 安装app方法
   nativeAPI.installApp(`file://storage/emulated/0/${'LUKES后台管理/LUKES后台管理.apk'}`);

   // 下载app方法
   nativeAPI.downLoadApi(
       {
       // 下载app地址 (下载地址(http/https)开头)(网络连接)
         url: "https://vip.123pan.cn/1818386522/%E6%B········",
       // 创建本地文件并保存到(fileName属性值)文件夹中  (自定义文件夹,并把下载好的文件放到当前文件夹中)
         fileName: "LUKES后台管理",
       // 文件名称
         appName: "LUKES后台管理.apk",
       },
       (res) => {
           // res ==> res.progress (进度条进度)
           // res ==> res.path (当前文件地址)

           // 进度条为100时,表示下载成功
         if (res.progress == 100) {
           // 调起手机安装程序  安装app
           nativeAPI.installApp(res.path);
         }
       }
   );
--- 设置剪切板内容
/**
 * 复制文本内容
 * @param {*} text 复制的文本
 * @param {*} [callback] 复制完成后的回调函数 => { return text } 返回复制的文本内容
 */
setCopy(text, callback) {
    uni.setClipboardData({
        data: text,
        showToast: false,
        success() { callback && callback(text) }
    });
}
--- 获取剪切板内容
 /**
 * 获取复制的内容
 * @param {*} callback  [callback] 回调函数
 */
getCopy(callback) {
    uni.getClipboardData({
        success(res) { callback && callback(res) }
    });
}
--- 保存文件到本地
/**
 * 保存文件到本地
 * @param {*} path 文件链接 
 */
addFile(path) {
    uni.saveFile({
        tempFilePath: path,
        success(res) {
            console.log(res);
        }
    });
}
--- 保存图片到相册
/**
 * 保存图片到相册
 * @param {*} url 需要保存的图片地址
 * @param {*} [callback] 保存成功后的回调函数
 */
savePick(url, callback) {
    uni.saveImageToPhotosAlbum({
        filePath: url,
        success: (res) => {
            callback && callback(res);
            this.msgInfo("保存成功,请到相册中查看");
        }
    });
}
--- 预览图片
/**
 * 预览图片
 * @param {*} imgsArr 预览图片的数组 (图片集合)
 * @param {*} [itemList] 长安图片的菜单栏 例如:['发送给朋友', '保存图片', '收藏']
 * @param {*} [callback] 点击某一个菜单栏的回调函数 (返回点击的按钮以及图片索引)
 * @param {*} [rej] 返回错误信息
 */
pickyl(current, imgsArr, itemList = ["暂无功能"], callback, rej) {
    let _this = this;
    uni.previewImage({
        // 当前图片索引
        current,
        // 图片集合
        urls: imgsArr,
        indicator: "number",
        longPressActions: {
            itemList: ['保存图片'],
            // itemList: itemList,
            success: (data) => {
                // 用户点击的当前图片
                const downImg = imgsArr[data.index];
                _this.downLoad(downImg, () => { }, { img: true });
                // callback && callback({
                //     // '选中了第' + (data.tapIndex + 1) + '个按钮
                //     btnIndex: data.tapIndex + 1,
                //     // 第' + (data.index + 1) + '张图片'
                //     imgIndex: data.index + 1,
                // })
                // console.log('选中了第' + (data.tapIndex + 1) + '个按钮,第' + (data.index + 1) + '张图片');
            },
            fail(err) { rej && rej(err) }
        }
    });
}
--- 选择图片
/**
 * 选择图片 
 * @param {*} num 选择的张数
 * @param {*} optionType 从哪里选择 (1.表示从相册选图 2.相机拍照 3.两者都有)
 * @param {*} [oss] 是否转换为oss格式 (Boolean)
 * @param {*} [callback] 图片选取后的回调函数
 */
optionsImg(num = 9, optionType, callback, oss) {
    let _this = this;
    // 判断需要的类型
    let arr = optionType == 1 ? ["album"] : optionType == 2 ? ["camera"] : ["album", "camera"];
    uni.chooseImage({
        count: num, //默认可以选择9张图片
        sizeType: ["original", "compressed"], //可以指定是原图还是压缩图,默认二者都有
        sourceType: arr, //从相册选择
        success: async (res) => {
            if (oss) {
                let arr = await _this.picKImgs(res.tempFilePaths)
                callback && callback(arr);
                return
            }
            callback && callback(res.tempFilePaths)
        },
    });
}
--- 图片转换为oss
// 图片转换为oss [res为数组,包含多张图片路径]
async picKImgs(res) {
    // 文件名称
    let name = appInst.getStore("info").nickname || "新用户";
    // 转换后的路径
    let newImgs = []
    for (const item of res) {
        let img = await uploadImg({ path: item, name });
        // 判断是否转换成功
        if (img.success) {
            newImgs.push(img.fileID)
            console.log(img.fileID);
        }
        // 判断是否把所有的图片都成功的转为oss
        if (res.length == newImgs.length) {
            return newImgs;
        }
    }
}
--- 查看图片信息
/**
 * 查看图片信息
 * @param {*} url 图片地址
 * @param {*} callback 成功后的回调函数 return(图片信息)
 */
imgInfo(url, callback) {
    uni.getImageInfo({
        src: url,
        success: (res) => callback && callback(res)
    });
}
--- 图片下载
/**
 * 图片下载
 * @param {*} url 文件地址
 * @param {*} callback 下载后的回调函数
 */
downLoad(url, callback, setting) {
    let _this = this;
    uni.downloadFile({
        url,
        success: (res) => {
            callback && callback(res)
            if (setting.img) {
                // 保存到相册中
                _this.savePick(res.tempFilePath);
            }
        },
        fail(err) {
            console.log(err);
        }
    });
}
--- 判断手机号码是否正确
/**
 * 判断手机号码是否正确
 * @param {*} num 手机号码
 * @returns 
 */
isTel(num) {
    let tel = /^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[1589]))\d{8}$/;
    return tel.test(num)
}
--- 截取字符串个数
/**
 * 截取字符串个数
 * @param {*} str 要截取的字符串
 * @param {*} num 要截取字符串的个数
 * @returns 
 */
interceptStr(str, num) {
    return str.length > num ? `${str.slice(0, num)}...` : str;
}
--- 获取用户当前地址
/**
 * 获取用户当前地址
 * @param {*} callback 回调函数 返回结果 
 */
getAddress(callback) {
    uni.getLocation({
        type: "gcj02", //返回可以用于uni.openLocation的经纬度
        geocode: true,
        success: (res) => {
            callback && callback(res.address);
        }
    })
}
--- 获取缓存中的数据
/**
 * 获取缓存中的数据
 * @param {*} name 
 */
getStore(name) {
    return uni.getStorageSync(name);
}
--- 设置本地缓存
/**
 * 设置本地缓存
 * @param {*} name 键名
 * @param {*} val 键值
 */
setStore(name, val) {
    uni.setStorageSync(name, val);
}
--- 指纹识别
/**
 * 指纹识别
 */
checkIsSupportSoterAuthentication() {
    // #ifdef APP-PLUS || MP-WEIXIN
    uni.checkIsSupportSoterAuthentication({
        success(res) {
            console.log(res);
            // 如果当前设备支持生物识别方式,且支持指纹识别方式
            if (res.supportMode && res.supportMode.includes("fingerPrint")) {
                /**
                 * uni.checkIsSoterEnrolledInDevice : 获取设备内是否录入指纹信息
                 *  checkAuthMode: 'fingerPrint', // 检验指纹信息
                 * */
                uni.checkIsSoterEnrolledInDevice({
                    checkAuthMode: "fingerPrint", // 检验指纹信息
                    success(res) {
                        console.log(res.isEnrolled);
                        if (res.isEnrolled == true) {
                            /**
                             * 开始 SOTER 生物认证
                             * 执行成功,进行后续操作
                             * */
                            uni.startSoterAuthentication({
                                requestAuthModes: ["fingerPrint"],
                                challenge: "123456",
                                // authContent: "请用指纹解锁",
                                success(res) {
                                    console.log(res);
                                    uni.showToast({
                                        title: "识别成功",
                                        duration: 5000,
                                        icon: "none",
                                    });
                                    //指纹识别成功后,进行后续工作
                                },
                                fail(err) {
                                    console.log(err, "66666666666666666");
                                },
                                complete(res) {
                                    console.log(res);
                                },
                            });
                            uni.hideLoading();
                        } else {
                            this.result = "此设备未录入指纹,请到设置中开启";
                        }
                    },
                    fail(err) {
                        uni.showModal({
                            title: "温馨提示",
                            content: "此设备未录入指纹,请到设置中开启",
                            showCancel: false,
                            success: function (res) {
                                // 进行后续逻辑
                            },
                        });
                    },
                });
            } else {
                this.result = "此设备不支持指纹识别功能";
            }
        },
        fail(err) {
            uni.showModal({
                title: "温馨提示",
                content: "此设备不支持指纹识别功能",
                showCancel: false,
                success: function (res) {
                    // 进行后续逻辑
                },
            });
        },
    });
    // #endif
    // #ifndef APP-PLUS || MP-WEIXIN
    this.result = "此平台不支持指纹识别";
    // #endif
}
--- 获取上个页面传递的数据
/**
 * 获取上个页面传递的数据
 * @returns 跳转类型 参数 跳转路径
 */
router() {
    let { openType, options, path } = getCurrentPages().pop().$page;
    return { openType, options, path }
}
--- 本地静态资源链接 (仅仅浏览器可用)
/**
 * 本地静态资源链接 (仅仅浏览器可用)
 * @param {*} url 本地资源地址
 * @returns 
 */
getPick(url) {
    return new URL(`../static/${url}`,
        import.meta.url).href;
}
--- 跳转首页
/**
 * 跳转首页
 * @param {*} url 页面路径
 * @param {*} [callback] 跳转完成后的回调函数 
 */
tab(url, callback) {
    uni.switchTab({
        url,
        success: () => callback && callback(),
        fail: (err) => {
            console.log(err);
        }
    });
}
--- 没有记录的跳转 (关闭所有页面) 没有记录
/**
 * 没有记录的跳转 (关闭所有页面) 没有记录
 * @param {*} url 页面路径
 * @param {*} [callback] 跳转完成后的回调函数 
 */
noJL(url, callback) {
    uni.reLaunch({
        url,
        success: () => callback && callback(),
        fail: (err) => {
            console.log(err);
        }
    })
}
--- 没有记录的跳转 (关闭当前页面) 没有记录
/**
 * 没有记录的跳转 (关闭当前页面) 没有记录
 * @param {*} url 页面路径
 * @param {*} [callback] 跳转完成后的回调函数 
 */
toOne(url, callback) {
    uni.redirectTo({
        url,
        success: () => callback && callback(),
        fail: (err) => {
            console.log(err);
        }
    })
}
--- 跳转到某一个页面 (非tabbar页面)
/**
 * 跳转到某一个页面 (非tabbar页面)
 * @param {*} url 页面路径
 * @param {*} [callback] 跳转完成后的回调函数 
 */
to(url, callback) {
    uni.navigateTo({
        url,
        success: () => callback && callback()
    })
    // this.shock()
}
--- 返回上个页面,后退
/**
 * 返回上个页面,后退
 * @param {*} [num] <string/number> (后退的步数) 
 */
last(num = 1, callback) {
    uni.navigateBack({ delta: num });
    this.delay(() => callback && callback(), 100);
}
--- 提示信息
/**
 * 提示信息
 * @param {*} title 提示的内容
 * @param {*} icon  警示图标
 * @param {*} [callback] 跳转完成后的回调函数 
 */
msgInfo(title, icon = "none", duration = 2500) {
    uni.showToast({
        title,
        icon: "none",
        duration
    });
}
--- 隐藏提示信息
/** 隐藏提示信息 **/
hide() {
    uni.hideToast();
}
--- 对象去重
/**
 * 对象去重
 * @param {*} arr 为存储对象的遍历容器
 * @param {*} val 则为要去重的条件
 * @returns 
 */
result(arr, val) {
    let newMap = new Map();
    return arr.filter(item => !newMap.has(item[val]) && newMap.set(item[val], 1));
}
--- 手机震动
/**
 * 手机震动
 * @param {*} [callback] 震动完成后的回调函数
 */
shock(callback) {
    uni.vibrateShort({ success: () => callback && callback() });
}
--- 双击事件封装
/**
 * 双击事件封装
 * @param hander 回调函数 {
 * click:单机时执行
 * doubleClick:双击时执行
 * }
 * @param time 执行时间,默认300毫秒后执行
 * @returns null
 */
double(hander: { click: Function, doubleClick: Function}, time = 300) {
  // 清除定时器
  clearTimeout(doubleTapTime);
  // 判断该属性是否存在 ,如果_this.sum不存在,者指定默认值
  let doubleSumTap = uni.getStorageSync("doubleSumTap") as any;
  doubleSumTap = doubleSumTap == "undefined" ? 0 : doubleSumTap;
  // 当前索引增加
  doubleSumTap++;
  // 修改缓存的数据
  uni.setStorageSync("doubleSumTap", doubleSumTap);
  // 判断是否到达预设的时间
  doubleTapTime = setTimeout(() => {
    // 单击
    if(doubleSumTap == 1) hander.click();
    // 双击执行完成
    if (doubleSumTap == 2) hander.doubleClick();
    // 清除缓存
    uni.removeStorageSync("doubleSumTap");
    // 默认值
    doubleSumTap = 0;
  }, time);
}

音乐播放类方法

--- 数组对象格式-如:[{},{},···]
// 音频播放
class musicPlayback {
    // 属性
    constructor(options) {
        // 获取全局唯一背景音乐管理器
        this.$audio = uni.getBackgroundAudioManager();
        // 获取当前音频的当前时间
        this.dio = uni.createInnerAudioContext();

        // 判断是否重复执行 [防止onTimeUpdate(监听音频进度)事件重复执行]
        this.currentTimeFlag = 0;

        // 当前播放音频的路径
        this.$src = "";

        // 当前音频播放时间 (用于暂停该实例被销毁,无法继续播放的问题) (解决小程序暂停后无法继续播放问题)
        this.$time = 0;

        // 背景音乐集合 (用于音频播放结束 后 [循环,逆时针,顺时针]播放条件)
        this.audioArr = [];

        // 用于控制音乐自然重复执行多次的问题
        this.$isFinish = false;
    }

    /**
     * 初始化
     * @param {*} initParameter 初始化配置 <Object>
     *          --- current:当前播放音乐的路径
     *          --- [urls]:音乐集合 (播放结束后从中抽取 根据自然播放结束事件执行的回调进行选择) [可选 (如果只有一首音频的情况下可不传)]
     * 
     * @param {*} callback 回调函数,(包含震动播放) <Function>
     */
    init(initParameter, callback) {
        // 抛出错误提示
        if (!initParameter.current) return console.log(new Error("未指向当前播放的路径,应初始化时指向初始化路径"));

        // 保存当前视频的路径
        this.$src = initParameter.current;

        // 保存当前音乐集合
        this.audioArr = initParameter.urls ? initParameter.urls : [initParameter.current];

        // 恢复默播放时间
        this.$time = 0;


        // 当前播放音频的索引值
        let index = initParameter.urls.findIndex((item) => item.url == initParameter.current);

        // 测试
        this.dio.src = initParameter.current;

        this.dio.onCanplay(() => {
            // 回调函数
            callback && callback({
                // 暴露播放方法
                play: () => this.play(),
                // 音频总时间
                allTime: this.dio.duration,
                // 当前播放音频的数据
                par: initParameter.urls[index]
            });
        });
    };

    /**
     * 监听事件集合
     * @param {*} callback 为对象包含多个函数  <Object>(每个函数为实时监听)
     * 
     * callback.onPlay:音频播放事件 <Function>
     * 
     * callback.onPause: 音频暂停事件 <Function>
     * 
     * callback.onEnded:音频自然播放至结束的事件 <Function>
     *                  函数回调函数返回 4 个方法  示例:(res) => { res.loop() }
     *                      1. loop()  循环播放
     *                      2. last()  上一首(逆时针) 播放
     *                      3. next()  下一首(顺时针) 播放
     *                      4. random()  随机播放
     * 
     * callback.onTimeUpdate:监听当前音乐播放的进度 <Function>
     *                  函数回调函数返回 当前播放进度 
     *                      { 
     *                          allTime:<Number>         音频总时间
     *                          currentTime:<Number>     当前播放音频时间
     *                          $step:<Number>           音频步长 (即进度条的1% 为该音频的多少秒)
     *                          $progress:<Number>       进度条进度
     *                      }
     * 
     * callback.onSeeked:监听音频位置发生改变 <Function>
     * 
     * callback.onError:音频播放错误事件 <Function>
     * 
     * callback.onStop:音频停止触发 <Function>
     */
    change(callback) {
        // 音频播放事件(参数为回调函数)
        this.$audio.onPlay(() => {
            // 开启 (用于控制音乐自然重复执行多次的问题)节流阀
            this.$isFinish = false;

            // 当前播放音频的索引值
            let index = this.audioArr.findIndex((item) => item.url == this.$src);
            // 暴露当前播放音乐的索引值以及item项信息
            callback.onPlay && callback.onPlay({ index, par: this.audioArr[index] });
        });

        // 音频暂停事件
        this.$audio.onPause(() => {
            callback.onPause && callback.onPause();
        });

        // 音频自然播放至结束的事件(参数为回调函数)
        this.$audio.onEnded(() => {

            // 控制自然播放结束的重复执行的问题
            if (this.$isFinish) return;
            // 关闭阀门
            this.$isFinish = true;

            // 判断是否重复执行 [防止onTimeUpdate(监听音频进度)事件重复执行]
            this.currentTimeFlag = 0;
            // 播放结束当前播放时间为0
            this.$time = 0;
            // 清除当前音乐实例参数配置
            this.stop();

            // 播放进度清除为0
            this.$audio.seek(0);
            this.$audio.startTime = 0;
            // 执行回调函数
            callback.onEnded && callback.onEnded({
                // 循环播放
                loop: () => {
                    this.play();
                },
                //(逆时针播放)
                last: () => this.last(),
                //( 顺时针播放)
                next: () => this.next(),
                // 随机播放
                random: () => {
                    // 判断该音频集合是不是为一个链接
                    if (this.audioArr.length <= 1) return this.play();
                    // 赛选出除当前播放音频的链接路径集合 (在其中随机选取一个进行比方)
                    let audioItems = this.audioArr.filter(item => item.url != this.$src);
                    // 生成随机数
                    const rand = this.randomNumber(0, audioItems.length - 1);

                    // 保存当前音频播放的路径
                    this.$src = audioItems[rand].url;
                    // 背景播放器必须拥有title属性,否则无法播放
                    this.$audio.title = '无';
                    // 选取随机播放音频路径
                    this.$audio.src = audioItems[rand].url;
                }
            });
        });

        // 监听当前音乐播放的进度
        this.$audio.onTimeUpdate(() => {
            // 保存当前事件
            if (this.$audio.currentTime) this.$time = this.$audio.currentTime;

            // 总时间
            let duration = parseInt(this.$audio.duration);
            // 当前播放进度的时间
            let currentTime = parseInt(this.$audio.currentTime);
            // 判断是否重复触发
            if (this.currentTimeFlag == currentTime) return;
            // 记录当前播放的时间 (防止重复触发)
            this.currentTimeFlag = currentTime;

            // 回调函数,该播放器播放后返回总播放进度以及当前播放进度
            callback.onTimeUpdate && callback.onTimeUpdate({
                // 总时间
                allTime: duration,
                // 当前播放的时间
                currentTime: currentTime,
                // 百分之一为多少秒 (计算音频的百分比)
                $step: duration / 100,
                // 进度条的进度比例
                $progress: (currentTime / duration) * 100 ? (currentTime / duration) * 100 : 0,
            });
        });

        // 音频播放错误事件(参数为回调函数)
        this.$audio.onError((err) => {
            callback.onError && callback.onError(err);
        });

        // 音频停止触发(参数为回调函数)
        this.$audio.onStop(() => {
            callback.onStop && callback.onStop();
        })

        // 	背景音频暂停事件(参数为回调函数)
        this.$audio.onPause(() => {
            callback.onPause && callback.onPause();
        })
    }

    /** 播放 */
    play() {
        // 背景播放器必须拥有title属性,否则无法播放
        this.$audio.title = '无';
        // 继续播放 
        this.$audio.src = this.$src;
        // 继续播放位置继续播放
        this.$audio.startTime = this.$time;
    }

    /**
     * 切换
     * @param {*} url 切换的音频地址 <String>
     * @param {*} [urls] 音频集合 <Array>
     */
    switch(url, urls) {
        // 播放进度清除为0
        this.$audio.seek(0);
        this.$audio.startTime = 0;
        // 复原当前播放的时间
        this.$time = 0;
        // 背景播放器必须拥有title属性,否则无法播放
        this.$audio.title = '无';
        // 保存当前播放音乐的路径
        this.$src = url;
        // 继续播放 
        this.$audio.src = url;

        // 切换音频可替换需要播放的音频集合 (有可进行替换)
        if (urls) this.audioArr = urls;
    }

    /** 停止播放 */
    stop() {
        this.$audio.src = "";
        // 停止播放
        this.$audio.stop();
    }

    /**
     * 设置当前音频播放的进度
     * @param {*} num <Number> 单位秒
     */
    step(num) {
        this.$audio.seek(num);
    }

    /** 上一首 */
    last() {
        // 播放进度清除为0
        this.$audio.seek(0);
        this.$audio.startTime = 0;

        // 判断是否重复执行 [防止onTimeUpdate(监听音频进度)事件重复执行]
        this.currentTimeFlag = 0;
        // 复原当前播放时间
        this.$time = 0;
        // 判断该音频集合是不是为一个链接
        if (this.audioArr.length <= 1) return this.play();
        // 查找当前播放音频的索引
        let index = this.audioArr.findIndex((item) => item.url == this.$src);
        // 索引递减(为上一音频)
        index--;
        // 判断当前索引是否超过最小索引值
        if (index < 0) index = this.audioArr.length - 1;
        // 保存当前音频播放的路径
        this.$src = this.audioArr[index].url;
        // 背景播放器必须拥有title属性,否则无法播放
        this.$audio.title = '无';
        // 修改音频路径进行播放
        this.$audio.src = this.audioArr[index].url;
    }

    /** 下一首 */
    next() {
        // 播放进度清除为0
        this.$audio.seek(0);
        this.$audio.startTime = 0;

        // 判断是否重复执行 [防止onTimeUpdate(监听音频进度)事件重复执行]
        this.currentTimeFlag = 0;
        // 复原当前播放时间
        this.$time = 0;
        // 判断该音频集合是不是为一个链接
        if (this.audioArr.length <= 1) return this.play();
        // 查找当前播放音频的索引
        let index = this.audioArr.findIndex((item) => item.url == this.$src);
        // 索引递增 (为下一音频)
        index++;
        // 判断当前索引是否超过最大索引值
        if (this.audioArr.length - 1 < index) index = 0;
        // 保存当前音频播放的路径
        this.$src = this.audioArr[index].url;
        // 背景播放器必须拥有title属性,否则无法播放
        this.$audio.title = '无';
        // 修改音频路径进行播放
        this.$audio.src = this.audioArr[index].url;
    }

    /**
    * @randomNumber 动态生成随机数
    * @param {*} min 最小值 (包含) <Number>
    * @param {*} max 最大值(包含) <Number>
    * @returns retuen <数字 Number>
    */
    randomNumber(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }
};


// 实例化
export const audio = new musicPlayback();
初始化
    // 初始化
    audio.init({
        // 当前默认播放音频 <string>
        current: item.url, 
        // 当前音乐集合
        urls: this.musicList //<Array>
        //例如: 歌曲链接集合
        urls: [
            {
                url: "http://xiaolu.3vzhuji.cc/appAudio/大家一起喜羊羊.mp3"
            ...
            },
            {
                url: "http://xiaolu.3vzhuji.cc/appAudio/大家一起喜羊羊.mp3"
            ...
            },
        ]
    }, (res) => {
        // 是否立即播放
        res.play();

        // 保存当前视频总时间 (初始化音频总时间)
        this.schedule.allTime = appInst.formatTime(res.allTime);

        // 当前播放item项的数据
        // 保存当前音频数据
        this.$audio$ = res.par;
    });
--- 数组格式-如:["","",···]
// 音频播放
class musicPlayback {
    // 属性
    constructor(options) {
        // 获取全局唯一背景音乐管理器
        this.$audio = uni.getBackgroundAudioManager();

        this.dio = uni.createInnerAudioContext();

        // 判断是否重复执行 [防止onTimeUpdate(监听音频进度)事件重复执行]
        this.currentTimeFlag = 0;

        // 当前播放音频的路径
        this.$src = "";

        // 当前音频播放时间 (用于暂停该实例被销毁,无法继续播放的问题) (解决小程序暂停后无法继续播放问题)
        this.$time = 0;

        // 背景音乐集合 (用于音频播放结束 后 [循环,逆时针,顺时针]播放条件)
        this.audioArr = [];
    }

    /**
     * 初始化
     * @param {*} initParameter 初始化配置 <Object>
     *          --- current:当前播放音乐的路径
     *          --- [urls]:音乐集合 (播放结束后从中抽取 根据自然播放结束事件执行的回调进行选择) [可选 (如果只有一首音频的情况下可不传)]
     *
     * @param {*} callback 回调函数,(包含震动播放) <Function>
     */
    init(initParameter, callback) {
        // 抛出错误提示
        if (!initParameter.current) return console.log(new Error("未指向当前播放的路径,应初始化时指向初始化路径"));

        // 保存当前视频的路径
        this.$src = initParameter.current;


        // 测试
        this.dio.src = initParameter.current;

        this.dio.onCanplay(() => {
            // 回调函数
            callback && callback({
                // 暴露播放方法
                play: () => this.play(),
                allTime: this.dio.duration
            });
        });


        // 保存当前音乐集合
        this.audioArr = initParameter.urls ? initParameter.urls : [initParameter.current];

        // 恢复默播放时间
        this.$time = 0;
    };

    /**
     * 监听事件集合
     * @param {*} callback 为对象包含多个函数  <Object>(每个函数为实时监听)
     *
     * callback.onPlay:音频播放事件 <Function>
     *
     * callback.onPause: 音频暂停事件 <Function>
     *
     * callback.onEnded:音频自然播放至结束的事件 <Function>
     *                  函数回调函数返回 4 个方法  示例:(res) => { res.loop() }
     *                      1. loop()  循环播放
     *                      2. last()  上一首(逆时针) 播放
     *                      3. next()  下一首(顺时针) 播放
     *                      4. random()  随机播放
     *
     * callback.onTimeUpdate:监听当前音乐播放的进度 <Function>
     *                  函数回调函数返回 当前播放进度
     *                      {
     *                          allTime:<Number>         音频总时间
     *                          currentTime:<Number>     当前播放音频时间
     *                          $step:<Number>           音频步长 (即进度条的1% 为该音频的多少秒)
     *                          $progress:<Number>       进度条进度
     *                      }
     *
     * callback.onSeeked:监听音频位置发生改变 <Function>
     *
     * callback.onError:音频播放错误事件 <Function>
     *
     * callback.onStop:音频停止触发 <Function>
     */
    change(callback) {
        // 音频播放事件(参数为回调函数)
        this.$audio.onPlay(() => {
            let index = this.audioArr.indexOf(this.$src);
            callback.onPlay && callback.onPlay(index);
        });

        // 音频暂停事件
        this.$audio.onPause(() => {
            callback.onPause && callback.onPause();
        });

        // 音频自然播放至结束的事件(参数为回调函数)
        this.$audio.onEnded(() => {
            // 播放结束当前播放时间为0
            this.$time = 0;
            this.stop();
            // 执行回调函数
            callback.onEnded && callback.onEnded({
                // 循环播放
                loop: () => {
                    this.play();
                },
                //(逆时针播放)
                last: () => this.last(),
                //( 顺时针播放)
                next: () => this.next(),
                // 随机播放
                random: () => {
                    // 判断该音频集合是不是为一个链接
                    if (this.audioArr.length <= 1) return this.play();
                    // 赛选出除当前播放音频的链接路径集合 (在其中随机选取一个进行比方)
                    let audioItems = this.audioArr.filter(item => item != this.$src);
                    // 生成随机数
                    const rand = this.randomNumber(0, audioItems.length - 1);

                    // 保存当前音频播放的路径
                    this.$src = audioItems[rand];
                    // 背景播放器必须拥有title属性,否则无法播放
                    this.$audio.title = '无';
                    // 选取随机播放音频路径
                    this.$audio.src = audioItems[rand];
                }
            });
        });

        // 监听当前音乐播放的进度
        this.$audio.onTimeUpdate(() => {
            // 保存当前事件
            if (this.$audio.currentTime) this.$time = this.$audio.currentTime;

            // 总时间
            let duration = parseInt(this.$audio.duration);
            // 当前播放进度的时间
            let currentTime = parseInt(this.$audio.currentTime);
            // 判断是否重复触发
            if (this.currentTimeFlag == currentTime) return;
            // 记录当前播放的时间 (防止重复触发)
            this.currentTimeFlag = currentTime;

            // 回调函数,该播放器播放后返回总播放进度以及当前播放进度
            callback.onTimeUpdate && callback.onTimeUpdate({
                // 总时间
                allTime: duration,
                // 当前播放的时间
                currentTime: currentTime,
                // 百分之一为多少秒 (计算音频的百分比)
                $step: duration / 100,
                // 进度条的进度比例
                $progress: (currentTime / duration) * 100 ? (currentTime / duration) * 100 : 0,
            });
        });

        // 音频播放错误事件(参数为回调函数)
        this.$audio.onError((err) => {
            callback.onError && callback.onError(err);
        });

        // 音频停止触发(参数为回调函数)
        this.$audio.onStop(() => {
            callback.onStop && callback.onStop();
        })
    }

    /** 播放 */
    play() {
        // 背景播放器必须拥有title属性,否则无法播放
        this.$audio.title = '无';
        // 继续播放
        this.$audio.src = this.$src;
        // 继续播放位置继续播放
        this.$audio.startTime = this.$time;
    }

    /**
     * 切换
     * @param {*} url 切换的音频地址 <String>
     * @param {*} [urls] 音频集合 <Array>
     */
    switch(url, urls) {
        // 背景播放器必须拥有title属性,否则无法播放
        this.$audio.title = '无';
        // 保存当前播放音乐的路径
        this.$src = url;
        // 继续播放
        this.$audio.src = url;

        // 切换音频可替换需要播放的音频集合 (有可进行替换)
        if (urls) this.audioArr = urls;
    }

    /** 停止播放 */
    stop() {
        this.$audio.src = "";
        // 停止播放
        this.$audio.stop();
    }

    /**
     * 设置当前音频播放的进度
     * @param {*} num <Number> 单位秒
     */
    step(num) {
        this.$audio.seek(num);
    }

    /** 上一首 */
    last() {
        // 复原当前播放时间
        this.$time = 0;
        // 判断该音频集合是不是为一个链接
        if (this.audioArr.length <= 1) return this.play();
        // 查找当前播放音频的索引
        let index = this.audioArr.indexOf(this.$src);
        // 索引递减(为上一音频)
        index--;
        // 判断当前索引是否超过最小索引值
        if (index < 0) index = this.audioArr.length - 1;
        // 保存当前音频播放的路径
        this.$src = this.audioArr[index];
        // 背景播放器必须拥有title属性,否则无法播放
        this.$audio.title = '无';
        // 修改音频路径进行播放
        this.$audio.src = this.audioArr[index];
    }

    /** 下一首 */
    next() {
        // 复原当前播放时间
        this.$time = 0;
        // 判断该音频集合是不是为一个链接
        if (this.audioArr.length <= 1) return this.play();
        // 查找当前播放音频的索引
        let index = this.audioArr.indexOf(this.$src);
        // 索引递增 (为下一音频)
        index++;
        // 判断当前索引是否超过最大索引值
        if (this.audioArr.length - 1 < index) index = 0;
        // 保存当前音频播放的路径
        this.$src = this.audioArr[index];
        // 背景播放器必须拥有title属性,否则无法播放
        this.$audio.title = '无';
        // 修改音频路径进行播放
        this.$audio.src = this.audioArr[index];
    }

    /**
    * @randomNumber 动态生成随机数
    * @param {*} min 最小值 (包含) <Number>
    * @param {*} max 最大值(包含) <Number>
    * @returns retuen <数字 Number>
    */
    randomNumber(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }
};

// 实例化
export const audio = new musicPlayback();
初始化
    // 初始化
    audio.init({
        // 当前默认播放音频 <string>
        current: item.url, 
        // 当前音乐集合
        urls: this.musicList // <Array>
    }, (res) => {
        // 是否立即播放
        res.play();
    });

微信小程序

事件说明
catch:tap点击事件
微信小程序官方暂未提供 (所以只能自己动手,丰衣足食)双击事件
catch:longpress手指长按事件
catch:input键盘输入事件
catch:confirm回车事件
catch:focus输入框聚集事件
catch:blur输入框失去焦点事件
catch:changevalue改变时触发 (输入框)事件
catch:touchstart手指按下事件
catch:touchmove手指移动事件
catch:touchend手指抬起事件
catch:touchcancel手指触摸动作被打断事件
catch:submit提交表单事件
catch:reset重置表单事件
band 和 catch 的区别 ?
bind:事件绑定不会阻止冒泡事件往上冒泡,简单来说,bind所绑定的事件对应会向上传递,让自己的父组件响应对应的事件。
catch:事件会把对应的事件阻拦在自己这里,只有自己能够响应对应事件。


因微信小程序并未提供双击事件,所以应当自己动手,丰衣足食.

关键代码 getStorageSync,setStorageSync,removeStorageSync

  /**
   * 双击事件封装
   * @param {*} hander 条件满足时执行的回调函数
   * @param {*} howManyTimes 点击几次执行回调函数 (默认为2)
   * @param {*} time 条件开始到结束的事件 (默认为300) (单位毫秒)
   */
  double(hander, howManyTimes = 2, time = 300) {
    // 判断该属性是否存在 ,如果_this.sum不存在,者指定默认值
    let doubleSumTap = wx.getStorageSync("doubleSumTap");
    doubleSumTap = doubleSumTap == "undefined" ? 0 : doubleSumTap;
    // 当前索引增加
    doubleSumTap++;
    // 修改缓存的数据
    wx.setStorageSync("doubleSumTap", doubleSumTap);

    // 判断是否到达预设的时间
    setTimeout(() => ((doubleSumTap = 0), wx.removeStorageSync("doubleSumTap")), time);

    // 双击执行完成
    if (doubleSumTap == howManyTimes) hander();
  }

使用less

// --- > 设置
// 在settings.json添加一下配置
"less.compile": {
     "outExt": ".wxss"
},

Api Function

--- 选取图片
export const collectPictures = function (sum) {   // sum:表示选取图片的张数
    let arr = [];
    return new Promise((reslove, reject) => {
        wx.chooseMedia({
            count: sum,
            mediaType: ['image'],
            sourceType: ['album', 'camera'],
            maxDuration: 30,
            camera: 'back',
            success(res) {
                // _this.data.imgList.push(res.tempFiles[0].tempFilePath)
                if (res.tempFiles.length == 1) {
                    arr.push(res.tempFiles[0].tempFilePath)
                    reslove(arr)
                } else {
                    let i = 0
                    res.tempFiles.forEach(item => {
                        arr.push(item.tempFilePath)
                        i += 1;
                        if (res.tempFiles.length == i) {
                            reslove(arr)
                        }
                    })
                }
            },
            fail(error) {
                reject(error)
            }
        })
    })
}
--- 图片预览
// 图片预览
export const picturePreview = (target, all) => {   // target 表示当前的图片地址    all表示当前imgs数据的所有值(array)
    wx.previewImage({
        current: target, // 当前显示图片的http链接
        urls: imgList // 需要预览的图片http链接列表
    });
}

//列: picturePreview(this.data.imgPath,[ this.data.imgPath ])
picturePreview(this.data.imgPath,[ this.data.imgPath ])
--- 文件预览
// 预览文件
// tempFilePath要预览的文件路径,callback为回调函数,文件打开完成后要做什么事,callback(非必填)
    previewFile(tempFilePath, callback) {
        wx.downloadFile({
            url: tempFilePath,
            success: (res) => {
                console.log(res)
                wx.openDocument({
                    filePath: res.tempFilePath,
                    showMenu: true,
                    success(res) {
			// 如果callback为真,则执行
                        callback && callback()
                    },
                    fail(err) {
                        console.log(err)
                    }
                })
            },
            fail: (err) => {
                console.log(err)
                return this.showToast('下载失败', 'error')
            },
            complete: () => { }
        });
    },
--- 预览图片和视频
previewMedio(tempFilePath, current = 0, showmenu = false) {
        wx.downloadFile({
            url: tempFilePath,
            success(res) {
                wx.previewMedia({
                    current: current,
                    sources: [res.tempFilePath],
                    showmenu: showmenu,
                });
            },
            fail: (err) => {
                console.log(err)
                return this.showToast('下载失败', 'error')
            },
        })
    },
--- 上传文件
upfile(num, type = "all", extension) {
        return new Promise(function (resolve, reject) {
            wx.chooseMessageFile({
                count: num,
                type,
                extension,
                success(res) {
                    resolve(res)
                },
                fali(err) {
                    reject(err)
                }
            })
        })
    },
--- 上传图片或视频
// 上传图片或视频
    uploadMedia(num, type = "mix") {
        return new Promise(function (resolve, reject) {
            wx.chooseMedia({
                count: num,
                mediaType: type,
                success(res) {
                    resolve(res)
                },
                fali(err) {
                    reject(err)
                }
            })
        })
    },
--- 防抖
// 搜索防抖
export const SEARCH = (obj, callback) => {
    clearTimeout(obj.timer)
    obj.timer = setTimeout(() => callback && callback(), 1500);
}
--- 微信支付
// 微信支付api
export const paymentMoney = (data) => {
    return new Promise((reslove, reject) => {
        wx.requestPayment({
            /** 
                timeStamp: '',      时间戳
                nonceStr: '',       随机字符串,长度为32个字符以下
                package: '',        统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=
                paySign: '',        签名
                signType: 'MD5',    签名算法,应与后台下单时的值一致
            **/
            ...data,
            success(response) {
                reslove(response)
            },
            fail(error) {
                reject(error)
            }
        })
    })
}
--- 获取个人信息
export const getInfo = () => {
    return new Promise((reslove, reject) => {
        wx.getUserProfile({
            desc: '完善用户信息',
            success(res) {
                wx.login({
                    success(data) {
                        reslove({ iv: res.iv, code: data.code, encryptedData: res.encryptedData, info: res.userInfo })
                    },
                    fail(error) {
                        reject(error)
                    }
                })
            },
            fail(error) {
                reject(error)
            }
        })
    })
}

--- 元素偏移距离
deviation(id) {
    return new Promise(response => {
      wx.createSelectorQuery().select(id).boundingClientRect((rect) => {
        rect.id      // 节点的ID
        rect.dataset // 节点的dataset
        rect.left    // 节点的左边界坐标
        rect.right   // 节点的右边界坐标
        rect.top     // 节点的上边界坐标
        rect.bottom  // 节点的下边界坐标
        rect.width   // 节点的宽度
        rect.height  // 节点的高度
      }).exec(res => {
        response({
          minTop: parseInt(res[0].top),  // 最小顶部距离
          maxTop: parseInt(res[0].top) + parseInt(res[0].height),  //盒子底部距离
          minLeft: parseInt(res[0].left), // 最小左边距离
          maxLeft: parseInt(res[0].left) + parseInt(res[0].width)
        })
      })
    });
  },
  // 例如:
  this.deviation('.minBox').then(data => console.log(data))

--- 提示信息
  /** 提示信息 **/
  msgInfo(msg, icon = "none", timer = "2000") {
    wx.showToast({
      // 标题
      title: msg,
      // 图标
      icon: icon,
      // 显示时间
      duration: timer
    })
  },
  /** 警告框 **/
  config(title) {
    return new Promise((reslove, reject) => {
      wx.showModal({
        title: title,
        // content: '这是一个模态弹窗',
        success(res) {
          if (res.confirm) {
            reslove(true)
          } else if (res.cancel) {
            reject(false)
          }
        }
      })
    })
  },

  /**  loading效果 **/
  loading(title) {
    wx.showLoading({
      title,
    })
  },
  // 取消提示框
  hide() {
     wx.hideLoading();
  },
--- 保存图片到本地
  wx.saveImageToPhotosAlbum({
    // 图片地址 
    filePath: this.imagePath,
  });
--- canvas自定义海报
// 1. 导入painter组件
 "painter":"/components/painter/painter"


// 2. 页面写入
<painter palette="{{paintPallette}}" bind:imgOK="onImgOK" bind:touchEnd="touchEnd" widthPixels="1000" />

<painter palette="{{paintPallette}}" bind:imgOK="onImgOK" widthPixels="1000" />
<image src="{{image}}" style="width: 654rpx; height: 1000rpx; margin-left:40rpx;"/>

3. js
// 导入绘制图像参数
import Card from '../../palette/card';


属性
  data: {
    imagePath: '',
  },



方法
// 初始化,获取图片路径
  onImgOK(e) {
     this.setData({
      imagePath: e.detail.path
    });
  },

  // 保存
  saveImage(){
    let resolve = this.data;
    if (resolve.imagePath && typeof resolve.imagePath === 'string') {
      wx.saveImageToPhotosAlbum({
        filePath: resolve.imagePath,
      });
    }
  },

  // 显示画板
  open() {
    this.setData({
      paintPallette: new Card().palette(),
    });
  },





// 获取canvas图表路径的方法
/** 保存图片到临时的本地文件 **/
    const ecComponent = this.selectComponent('#mychart-dom-graph');//获取echarts组件
    ecComponent.canvasToTempFilePath({
      //安卓机型此处不会成功回调
      success: res => {
	console.log(res.tempFilePath);
	// paintPallette: new store.ShadowExample().palette(res.tempFilePath)
      },
    });



// 绘制海报参数
this.ShadowExample = class ShadowExample {
            palette(path) {
                return ({
                    width: '620rpx',
                    height: '1038rpx',
                    // background: '#eee',
                    views: [
                        // 背景一
                        {
                            type: 'image',
                            // url: 'https://res.facebodyfitness.com/onlinevideo/img/4a4a345e-e308-4d89-a829-739d5f06b50d.png',
                            url: 'https://res.facebodyfitness.com/onlinevideo/img/f91a1f7b-716e-4505-bb6b-9b00c635929d.',
                            css: {
                                width: '100%',
                                height: '100%',
                            },
                        },
                        // 背景二
                        {
                            type: 'image',
                            url: '/asset/gay.png',
                            css: {
                                width: '526rpx',
                                height: '796rpx',
                                "top": "64rpx",
                                "left": "47rpx"
                            },
                        },
                        //分数
                        {
                            type: 'image',
                            url: "/asset/icon/xingxing_green.png",
                            css: {
                                width: '184rpx',
                                height: '54rpx',
                                "top": "117rpx",
                                "left": "218rpx"
                            },
                        },
                        // 分数名
                        {
                            type: 'text',
                            text: `${_this.reportHeader.score}`,
                            css: {
                                left: '257rpx',
                                fontSize: '66rpx',
                                "color": "#FE8A36",
                                top: '104rpx',
                                "font- family": "DIN",
                                "font-weight": "bold",
                            },
                        },
                        // 分数名
                        {
                            type: 'text',
                            text: "分",
                            css: {
                                left: '340rpx',
                                fontSize: '30rpx',
                                "color": "#FE8A36",
                                top: '143rpx',
                                "font- family": "DIN",
                                "font-weight": "bold",
                            },
                        },
                        // 打败人数的比例
                        {
                            type: 'text',
                            text: "你已打败",
                            css: {
                                top: '195rpx',
                                left: '190rpx',
                                fontSize: '24rpx',
                                "color": "#EBEBEB",
                                "font- family": "PingFang SC",
                                // "font-weight": "bold",
                            },
                        },
                        // 打败人数的比例
                        {
                            type: 'text',
                            text: `${_this.exceed}%`,
                            css: {
                                width: "95rpx",
                                height: "30rpx",
                                textAlign: "center",
                                top: '195rpx',
                                left: '295rpx',
                                fontSize: '24rpx',
                                "color": "#7C30E0",
                                "font- family": "PingFang SC",

                                // "font-weight": "bold",
                            },
                        },
                        // 打败人数的比例
                        {
                            type: 'text',
                            text: "的人",
                            css: {
                                top: '195rpx',
                                left: '390rpx',
                                fontSize: '24rpx',
                                "color": "#EBEBEB",
                                "font- family": "PingFang SC",
                                // "font-weight": "bold",
                            },
                        },
                        // 体适能分数
                        {
                            type: 'text',
                            text: "体适能分数",
                            css: {
                                top: '243rpx',
                                left: '245rpx',
                                fontSize: '26rpx',
                                "color": "#EBEBEB",
                                "font- family": "PingFang SC",
                                // "font-weight": "bold",
                            },
                        },
                        // 雷达图
                        {
                            type: 'image',
                            url: path,
                            css: {
                                width: '433rpx',
                                height: '453rpx',
                                "top": "349rpx",
                                "left": "94rpx"
                            },
                        },

                        // 体适能分数
                        {
                            type: 'text',
                            text: `分享人:${wx.getStorageSync("info").name}`,
                            css: {
                                top: '901rpx',
                                left: '47rpx',
                                fontSize: '26rpx',
                                "color": "#EBEBEB",
                                "font- family": "PingFang SC",
                            },
                        },
                        // 体适能分数
                        {
                            type: 'text',
                            text: "我也要测,微信扫码",
                            css: {
                                top: '950rpx',
                                left: '47rpx',
                                fontSize: '26rpx',
                                "color": "#EBEBEB",
                                "font- family": "PingFang SC",
                            },

                        },
                        // 微信小程序
                        {
                            type: 'image',
                            url: '/asset/wx.jpg',
                            css: {
                                width: '118rpx',
                                height: '118rpx',
                                "top": "884rpx",
                                "left": "455rpx",
                                borderRadius: "20rpx",
                            },
                        },
                    ],
                });
            }
        }

--- 微信小程序Audio Class封装 (该方法退出小程序后无法使用)
/** 局部播放 **/

// 音乐播放器
class AudioPlay {
    constructor(option) {
        // 创建音频上下文对象
        this.audio = wx.createInnerAudioContext();
        // 判断是否重复执行
        this.currentTimeFlag = 0;
    }

    /**
     * 初始化
     * @param {*} url 音频路径
     * @param {*} callback 回调函数,(包含自动播放)
     */
    init(url, callback) {
        // 音频路径
        this.audio.src = url;

        // 回调函数
        callback && callback({
            // 是否立即播放方法
            play: () => this.play(),
        });
    }

    /**
     * 监听事件集合
     * @param {*} callback 为对象包含多个函数 (每个函数均为实时监听)
     * 
     * callback.onPlay:音频播放事件
     * 
     * callback.onPause: 音频暂停事件
     * 
     * callback.onEnded:音频自然播放至结束的事件
     * 
     * callback.onTimeUpdate:监听当前音乐播放的进度
     * 
     * callback.onSeeked:监听音频位置发生改变
     * 
     * callback.onError:音频播放错误事件
     */
    change(callback) {
        // 音频播放事件(参数为回调函数)
        this.audio.onPlay(() => {
            callback.onPlay && callback.onPlay();
        });

        // 音频暂停事件
        this.audio.onPause(() => {
            callback.onPause && callback.onPause();
        });

        // 音频自然播放至结束的事件(参数为回调函数)
        this.audio.onEnded(() => {
            callback.onEnded && callback.onEnded();
        });

        // 监听当前音乐播放的进度
        this.audio.onTimeUpdate(() => {
            // 总时间
            let duration = parseInt(this.audio.duration);
            // 当前播放进度的时间
            let currentTime = parseInt(this.audio.currentTime);
            // 判断是否重复触发
            if (this.currentTimeFlag == currentTime) return;
            // 记录当前播放的时间 (防止重复触发)
            this.currentTimeFlag = currentTime;

            // 回调函数,该播放器播放后返回总播放进度以及当前播放进度
            callback.onTimeUpdate && callback.onTimeUpdate({
                // 总时间
                allTime: duration,
                // 当前播放的时间
                currentTime: currentTime,
                // 百分之一为多少秒 (计算音频的百分比)
                $audio: duration / 100,
                // 进度条的进度比例
                $progress: (currentTime / duration) * 100,
            });
        });

        // 监听音频位置发生改变
        this.audio.onSeeked(() => {
            // 解决onTimeUpdate不生效问题
            this.audio.duration;
            // 执行onSeeked监听回调
            callback.onSeeked && callback.onSeeked();
        });

        // 音频播放错误事件(参数为回调函数)
        this.audio.onError(() => {
            callback.onError && callback.onError();
        });
    }

    // 播放
    play() {
        this.audio.duration
        // 解决onTimeUpdate不生效问题
        setTimeout(() => { this.audio.duration }, 800);
        // 音频播放
        this.audio.play();
    }

    // 设置当前音频播放的进度
    setSteep(num) {
        this.audio.seek(num);
    }

    // 暂停(暂停后的音频再播放会从暂停处开始播放)
    pause() {
        this.audio.pause();
    }

    // 停止(停止后的音频再播放会从头开始播放)
    stop() {
        this.audio.stop();
    }

};

--- 微信小程序musicPlayback Class封装 (后台仍可继续播放)
// 音频播放
class musicPlayback {
    // 属性
    constructor(options) {
        // 获取全局唯一背景音乐管理器
        this.$audio = wx.getBackgroundAudioManager();

        // 判断是否重复执行 [防止onTimeUpdate(监听音频进度)事件重复执行]
        this.currentTimeFlag = 0;

        // 当前播放音频的路径
        this.$src = "";

        // 当前音频播放时间 (用于暂停该实例被销毁,无法继续播放的问题) (解决小程序暂停后无法继续播放问题)
        this.$time = 0;

        // 背景音乐集合 (用于音频播放结束 后 [循环,逆时针,顺时针]播放条件)
        this.audioArr = [];
    }

    /**
     * 初始化
     * @param {*} initParameter 初始化配置 <Object>
     *          --- current:当前播放音乐的路径
     *          --- [urls]:音乐集合 (播放结束后从中抽取 根据自然播放结束事件执行的回调进行选择) [可选 (如果只有一首音频的情况下可不传)]
     * 
     * @param {*} callback 回调函数,(包含自动播放) <Function>
     */
    init(initParameter, callback) {
        // 抛出错误提示
        if (!initParameter.current) return console.log(new Error("未指向当前播放的路径,应初始化时指向初始化路径"));

        // 保存当前视频的路径
        this.$src = initParameter.current;

        // 保存当前音乐集合
        this.audioArr = initParameter.urls ? initParameter.urls : [initParameter.current];

        // 恢复默播放时间
        this.$time = 0;
        // 回调函数
        callback && callback({
            // 暴露播放方法
            play: () => this.play(),
        });
    };

    /**
     * 监听事件集合
     * @param {*} callback 为对象包含多个函数  <Object>(每个函数均为实时监听)
     * 
     * callback.onPlay:音频播放事件 <Function>
     * 
     * callback.onPause: 音频暂停事件 <Function>
     * 
     * callback.onEnded:音频自然播放至结束的事件 <Function>
     *                  函数回调函数返回 4 个方法  示例:(res) => { res.loop() }
     *                      1. loop()  循环播放
     *                      2. last()  上一首(逆时针) 播放
     *                      3. next()  下一首(顺时针) 播放
     *                      4. random()  随机播放
     * 
     * callback.onTimeUpdate:监听当前音乐播放的进度 <Function>
     *                  函数回调函数返回 当前播放进度 
     *                      { 
     *                          allTime:<Number>         音频总时间
     *                          currentTime:<Number>     当前播放音频时间
     *                          $step:<Number>           音频步长 (即进度条的1% 为该音频的多少秒)
     *                          $progress:<Number>       进度条进度
     *                      }
     * 
     * callback.onSeeked:监听音频位置发生改变 <Function>
     * 
     * callback.onError:音频播放错误事件 <Function>
     */
    change(callback) {
        // 音频播放事件(参数为回调函数)
        this.$audio.onPlay(() => {
            callback.onPlay && callback.onPlay();
        });

        // 音频暂停事件
        this.$audio.onPause(() => {
            callback.onPause && callback.onPause();
        });

        // 音频自然播放至结束的事件(参数为回调函数)
        this.$audio.onEnded(() => {
            // 播放结束当前播放时间为0
            this.$time = 0;
            // 执行回调函数
            callback.onEnded && callback.onEnded({
                // 循环播放
                loop: () => {
                    this.play();
                },
                //(逆时针播放)
                last: () => this.last(),
                //( 顺时针播放)
                next: () => this.next(),
                // 随机播放
                random: () => {
                    // 判断该音频集合是不是为一个链接
                    if (this.audioArr.length <= 1) return this.play();
                    // 赛选出除当前播放音频的链接路径集合 (在其中随机选取一个进行播放)
                    let audioItems = this.audioArr.filter(item => item != this.$src);
                    // 生成随机数
                    const rand = this.randomNumber(0, audioItems.length - 1);

                    // 保存当前音频播放的路径
                    this.$src = audioItems[rand];
                    // 背景播放器必须拥有title属性,否则无法播放
                    this.$audio.title = '无';
                    // 选取随机播放音频路径
                    this.$audio.src = audioItems[rand];
                }
            });
        });

        // 监听当前音乐播放的进度
        this.$audio.onTimeUpdate(() => {
            // 保存当前事件
            if (this.$audio.currentTime) this.$time = this.$audio.currentTime;

            // 总时间
            let duration = parseInt(this.$audio.duration);
            // 当前播放进度的时间
            let currentTime = parseInt(this.$audio.currentTime);
            // 判断是否重复触发
            if (this.currentTimeFlag == currentTime) return;
            // 记录当前播放的时间 (防止重复触发)
            this.currentTimeFlag = currentTime;

            // 回调函数,该播放器播放后返回总播放进度以及当前播放进度
            callback.onTimeUpdate && callback.onTimeUpdate({
                // 总时间
                allTime: duration,
                // 当前播放的时间
                currentTime: currentTime,
                // 百分之一为多少秒 (计算音频的百分比)
                $step: duration / 100,
                // 进度条的进度比例
                $progress: (currentTime / duration) * 100 ? (currentTime / duration) * 100 : 0,
            });
        });

        // 监听音频位置发生改变
        this.$audio.onSeeked(() => {
            // 解决onTimeUpdate不生效问题
            this.$audio.duration;
            // 执行onSeeked监听回调
            callback.onSeeked && callback.onSeeked();
        });

        // 音频播放错误事件(参数为回调函数)
        this.$audio.onError(() => {
            callback.onError && callback.onError();
        });
    }

    /** 播放 */
    play() {
        // 背景播放器必须拥有title属性,否则无法播放
        this.$audio.title = '无';
        // 继续播放 
        this.$audio.src = this.$src;
        // 继续播放位置继续播放
        this.setSteep(this.$time);
    }

    /**
     * 切换
     * @param {*} url 切换的音频地址 <String>
     * @param {*} [urls] 音频集合 <Array>
     */
    switch(url, urls) {
        // 背景播放器必须拥有title属性,否则无法播放
        this.$audio.title = '无';
        // 保存当前播放音乐的路径
        this.$src = url;
        // 继续播放 
        this.$audio.src = url;

        // 切换音频可替换需要播放的音频集合 (有可进行替换)
        if (urls) this.audioArr = urls;
    }

    /** 停止播放 */
    stop() {
        // 停止播放
        this.$audio.stop();
    }

    /**
     * 设置当前音频播放的进度
     * @param {*} num <Number> 单位秒
     */
    setSteep(num) {
        this.$audio.seek(num);
    }

    /** 上一首 */
    last() {
        // 复原当前播放时间
        this.$time = 0;
        // 判断该音频集合是不是为一个链接
        if (this.audioArr.length <= 1) return this.play();
        // 查找当前播放音频的索引
        let index = this.audioArr.indexOf(this.$src);
        // 索引递减(为上一音频)
        index--;
        // 判断当前索引是否超过最小索引值
        if (index < 0) index = this.audioArr.length - 1;
        // 保存当前音频播放的路径
        this.$src = this.audioArr[index];
        // 背景播放器必须拥有title属性,否则无法播放
        this.$audio.title = '无';
        // 修改音频路径进行播放
        this.$audio.src = this.audioArr[index];
    }

    /** 下一首 */
    next() {
        // 复原当前播放时间
        this.$time = 0;
        // 判断该音频集合是不是为一个链接
        if (this.audioArr.length <= 1) return this.play();
        // 查找当前播放音频的索引
        let index = this.audioArr.indexOf(this.$src);
        // 索引递增 (为下一音频)
        index++;
        // 判断当前索引是否超过最大索引值
        if (this.audioArr.length - 1 < index) index = 0;
        // 保存当前音频播放的路径
        this.$src = this.audioArr[index];
        // 背景播放器必须拥有title属性,否则无法播放
        this.$audio.title = '无';
        // 修改音频路径进行播放
        this.$audio.src = this.audioArr[index];
    }

    /**
    * @randomNumber 动态生成随机数
    * @param {*} min 最小值 (包含) <Number>
    * @param {*} max 最大值(包含) <Number>
    * @returns retuen <数字 Number>
    */
    randomNumber(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }
};


// 实例化
const audio = new musicPlayback();

audio.init({
    // 当前默认播放音频
    current: "https://wechat800.oss-cn-shanghai.aliyuncs.com/huilaijiuhao/material/made-in-xiaolu.mp3",
    // 后续播放音频集合
    urls: []
}, (res) => {
    /** 回调函数 */
    // 是否默认播放
    res.play();
});

// 监听事件
audio.change({
    onPlay() {
        console.log('开始播放');

    },
    onTimeUpdate(res) {
        // console.log(res);
    },
    onEnded(res) {
        res.next();
        console.log("播放结束");
    }
});

正则图片筛选(富文本图片样式修改)

--- 正则筛选图片编辑
formatRichText(html) {
    return new Promise(res => {
      let newContent = html.replace(/<img[^>]*>/gi, function (match, capture) {
        match = match.replace(/style="[^"]+"/gi, '').replace(/style='[^']+'/gi, '');
        match = match.replace(/width="[^"]+"/gi, '').replace(/width='[^']+'/gi, '');
        match = match.replace(/height="[^"]+"/gi, '').replace(/height='[^']+'/gi, '');
        return match;
      });
      newContent = newContent.replace(/style="[^"]+"/gi, function (match, capture) {
        match = match.replace(/width:[^;]+;/gi, 'max-width:100%;').replace(/width:[^;]+;/gi, 'max-width:100%;');
        return match;
      });
      newContent = newContent.replace(/<br[^>]*\/>/gi, '');
      newContent = newContent.replace(/\<img/gi, '<img style="max-width:100%;height:auto;display:block;margin-top:0;margin-bottom:0;border-radius: 10px;"');
      res({ state: 200, html: newContent })
    })
  },
--- 正则筛选图片为数组
// 筛选图片
   var src = result.qualifications.match(/<img\s*src=\"([^\"]*?)\"[^>]*>/gi);  // result.qualifications 为html的富文本
   let srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i // 匹配图片中的src
   var urls = []
   if (src) {
      for (var i = 0; i < src.length; i++) {
         urls = urls.concat(src[i].match(srcReg)[1])
      }
   }
   console.log(urls);  //urls: 存放图片路径的数据

Mobx (创库管理)

--- 创建store
// Mobx 仓库
import { observable, action } from 'mobx-miniprogram'

// 暴露数据
export const store = observable({
  // 数据
})

--- 页面中引用
// 导入mobx
import { createStoreBindings } from 'mobx-miniprogram-bindings'
import { store } from '../../store'


page({
	data:{

	},
	onLoad:{
		createStoreBindings(this,{
			// 数据源
			store,
			// 属性
			fields:[],
			// 方法
			actions:[]
		})
	}

})
--- 组件中引用
// 导入mobx
import { storeBindingsBehavior } from 'mobx-miniprogram-bindings'
import { store } from '../../store'

Component({
	behaviors: [storeBindingsBehavior],
  	storeBindings: {
    		//数据源
   		store,
		// 方法
    		fields: [""],
		// 属性
		actions:[""]
  },
})
--- @版本
/** 历史版本 **/
"mobx-miniprogram": "^4.13.2",
"mobx-miniprogram-bindings": "^1.2.1"

/** 默认最新版 **/
npm i --save mobx-miniprogram mobx-miniprogram-bindings

JavaScript/TsSctipt(方法)

--> 封装

--- 实用方法
1. 动态生成随机数
/**
 * @rand 动态生成随机数
 * @param {*} min 最小值 (包含)
 * @param {*} max 最大值(包含)
 * @returns retuen <数字>
 */
function rand(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}
--- 动画
1. 上下移动动画封装
/**
 * @Up 上下移动动画封装
 * @param {*} target 要移动的对象
 * @param {*} end 移动距离
 * @param {*} callback 该函数完成后的回调函数
 */
function Up(target, end, callback) {
    clearInterval(target.timer);
    target.timer = setInterval(() => {
        let step = (end - target.offsetTop) / 12;
        step = step > 0 ? Math.ceil(step) : Math.floor(step);
        if (end == target.offsetTop) {
            callback && callback();
            clearInterval(target.timer);
            return;
        }
        target.style.top = `${target.offsetTop + step}px`
    }, 10);
};

2. 左右移动动画封装
/**
 * @moveLeftAndRight 左右移动动画封装
 * @param {*} target 要移动的对象
 * @param {*} end 移动距离
 * @param {*} callback 该函数完成后的回调函数
 */
function moveLeftAndRight(target, end, callback) {
    clearInterval(target.timer);
    target.timer = setInterval(() => {
        let step = (end - target.offsetLeft) / 15;
        step = step > 0 ? Math.ceil(step) : Math.floor(step);
        if (end == target.offsetLeft) {
            callback && callback();
            clearInterval(target.timer);
            return;
        }
        target.style.left = `${target.offsetLeft + step}px`
    }, 10);
};

3. 透明度动画封装
/**
 * @transparency 透明度动画封装
 * @param {*} target 透明的对象
 * @param {*} end 透明度 (值问 0 ~ 100)
 * @param {*} callback 该函数完成后的回调函数
 */
function transparency(target, end, callback) {
    let step = 0, sum = 0;
    sum = sum < end ? sum = 0 : sum = 100;

    clearInterval(target.times);
    target.times = setInterval(() => {
        step = end > sum ? 10 : -10;
        if (sum === end) {
            callback && callback();
            clearInterval(target.times);
            return;
        }
        sum += step;
        target.style.opacity = sum / 100;
    }, 30)
};
--- 返回顶部
/**
 * 封装动动画函数
 * @param {*} target 对象
 * @param {*} end 移动的距离 
 * @param {*} callback 事件完成后的执行的回调函数
 */
function backToTop(target, end, callback) {
    clearInterval(target.timer);
    target.timer = setInterval(function () {
        let step = (end - window.pageYOffset) / 10;
        step = step > 0 ? Math.ceil(step) : Math.floor(step);
        if (window.pageYOffset == end) {
            callback && callback();
            clearInterval(target.timer);
            return;
        };
        target.scroll(0, target.pageYOffset + step);
    }, 10);
}
backToTop(window, 0, () => { })
--- 禁用滚动条
/**
 * 禁用滚动条
 * @param flag <Boolean> 布尔值 true表示禁用
 */
disableScrollbar(flag: boolean): void {
    // 判断模态是打开状态还是隐藏状态,来判断滚动条是否禁用
    document.documentElement.style.overflow = flag ? 'hidden' : ''; // 禁用html
    document.body.style.overflow = flag ? 'hidden' : ''; // 禁用body
}
--- 复制文本封装
  1. 创建textarea进行模拟复制:

    function copyUrl(str, callback) {
      const el = document.createElement("textarea");
      el.value = str;
      el.style.position = "absolute";
      el.style.left = "-100%";
      document.body.appendChild(el);
      el.select();
      document.execCommand("copy");
      document.body.removeChild(el);
      callback && callback(str);
    }
    
  2. 使用navigator.clipboard.writeText:

    /**
    * 复制文字方法
    * @param str <string> 文本
    * @param callback 回调函数
    */
    async copyUrl(str: string, callback: Function) {
       try {
           // 复制请求
           await navigator.clipboard.writeText(str);
           // 返回请求状态
           callback && callback(true);
        } catch (err) {
           // 返回请求状态
           callback && callback(false);
        }
    }
    
--- 文本中筛选出所有的链接
/**
 * 在文本中筛选出所有的链接
 * @param text 需要处理的文本
 * @returns Array 链接集合
 */
extractLinks(text: string) {
    if (!text) return [];
    
    // 1. 定义你提供的正则表达式(注意:在 JS 字符串中,反斜杠 \ 需要转义为 \\)
    const regexStr = '(http|ftp|https):\\/\\/[\\w\\-_]+(\\.[\\w\\-_]+)+([\\w\\-\\.,@?^=%&:/~\\+#]*[\\w\\-\\@?^=%&/~\\+#])?';
    
    // 2. 创建正则对象,务必加上 'g' 参数进行全局匹配
    const pattern = new RegExp(regexStr, 'g');
    // 3. 使用 match 方法提取所有链接
    const matchArr = text.match(pattern);
    
    // 4. 如果没找到,match 会返回 null,这里做个兜底返回空数组
    return matchArr || [];
}
--- 文本中间用*****代替
/**
 * 处理显示的文本值
 * @param str 字符串 
 * @param starSum  中间显示多少个 <*> 【默认5个】
 * @param start  第几个字符开始 【默认第一个】
 * @param starSum  结束的字符 【默认最后一个】
 * @returns <string>
 */
maskString({ str , starSum = 5, start = 1, end = 1 } : { str: string, starSum?: number, start?: number, end?: number }): string {
    if (str.length <= start + end) return str; // 长度≤start + end时无需处理
    return str.substring(0, start) + '*'.repeat(starSum) + str.slice(-end);
}
--- 双击事件封装
/**
 * 双击事件封装
 * @param hander 回调函数 {
 * click:单机时执行
 * doubleClick:双击时执行
 * }
 * @param time 执行时间,默认300毫秒后执行
 * @returns null
 */
double(hander: { click: Function, doubleClick: Function}, time = 300) {
  // 清除定时器
  clearTimeout(doubleTapTime);
  // 判断该属性是否存在 ,如果_this.sum不存在,者指定默认值
  let doubleSumTap = localStorage.getItem("doubleSumTap") as any;
  doubleSumTap = doubleSumTap == "undefined" ? 0 : doubleSumTap;
  // 当前索引增加
  doubleSumTap++;
  // 修改缓存的数据
  localStorage.setItem("doubleSumTap", doubleSumTap);
  // 判断是否到达预设的时间
  doubleTapTime = setTimeout(() => {
    // 单击
    if(doubleSumTap == 1) hander.click();
    // 双击执行完成
    if (doubleSumTap == 2) hander.doubleClick();
    // 清除缓存
    localStorage.removeItem("doubleSumTap");
    // 默认值
    doubleSumTap = 0;
  }, time);
}
--- 事件委派封装
/**
 * 事件委派封装
 * @param parameter { 参数:<event: 事件对象,targetHTML:目标标签名,eventName:触发事件标识,callback(返回参数:返回自定义事件):回调函数> }
 */
eventDelegation(parameter: { event: MouseEvent, targetHTML: string | Array<string>, eventName: string | Array<string>, callback: Function}): void {
    // 触发目标元素
    const el = parameter.event.target as HTMLElement;
    
    // 判断点击的元素是否符合要求
    if(Array.isArray(parameter.targetHTML) && parameter.targetHTML.includes(el.tagName)) parameter.targetHTML = el.tagName;

    // 判断是否是事件委派的事件
    if(Array.isArray(parameter.eventName) && parameter.eventName.includes(el.dataset.eventname as string)) parameter.eventName = el.dataset.eventname as string;

    // 判断目标元素
    if(el.dataset.eventname !== parameter.eventName) return;
    
    // 符合要求触发事件回调
    if(el.tagName === parameter.targetHTML && el.dataset.eventname) parameter.callback && parameter.callback({...el.dataset });
}

使用方法【示例】:

<div @click="goTools($event)">
	<div data-eventName="go" :data-target="item.target"></div>
</div>

<script>
// 单个点击事件使用【示例】
const goTools = (e: MouseEvent)=> appInst.eventDelegation({ event: e, targetHTML: "DIV", eventName: "go", callback: ({ target }:{ target: string })=>{
    // 满足条件时的回调函数
}});

// 多个点击事件使用【示例】
const goTools = (e: MouseEvent)=> appInst.eventDelegation({ event: e, targetHTML: ["DIV", "P"], eventName: ["to", "copy"], callback: (result: any)=>{
    // 跳转
    result.eventname === 'to' && result.target && window.open(result.target);

    // 复制网址
    result.eventname === 'copy' && result.copy && copyUrl(result.copy);
}})
</script>

/***** 建议点击对象加一个伪元素,覆盖点击的对象,背景为透明,防止点击不理想的效果 *****/
--- 防抖函数
/**
* 防抖函数
* @param {Function} fn - 需要防抖的回调函数
* @param {number} [delay = 150] - 防抖延迟时间(默认150毫秒)
* @param {boolean} [immediate = false] - 是否立即执行第一次触发(默认false)
* @returns {Function} 包装后的防抖函数
*/
debounce<T extends (...args: any[]) => any>(fn: T,  delay: number = 150, immediate: boolean = false): (...args: Parameters<T>) => void {
  // 保存定时器ID,用于清除定时器
  let timer: number | null = null;
  // 返回包装后的函数
  return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
    // 绑定this上下文(确保原函数的this指向正确)
    const context = this;
    // 标记是否是第一次触发且需要立即执行
    const isCallNow = immediate && !timer;
    // 每次触发时,清除之前的定时器(重新计时)
    if (timer) clearTimeout(timer);
    // 设置新的定时器 --- 非立即执行模式:延迟结束后执行函数  --- 执行完成后清空定时器(重置状态)
    timer = setTimeout(() => (!immediate && fn.apply(context, args), timer = null), delay);
    // 立即执行模式:第一次触发时直接执行
    isCallNow && fn.apply(context, args);
  };
}
--- 随机token生成器
/**
 * 随机生成token
 * @param {*} tokenLength 生成的长度  (Number)
 * @returns [String]
 */
getToken(tokenLength: number): String {
  // 生成随机字符串
  const letters = "abcdefghijklmnopqrstuvwxyz0123456789";
  let token = "";
  for (let i = 0; i < parameter.tokenLength; i++) {
    if (i == 7 || i == 12 || i == 17 || i == 22) {
      token = token + letters.charAt(Math.floor(Math.random() * letters.length));
    } else {
      token += letters.charAt(Math.floor(Math.random() * letters.length));
    }
  }
  // 返回token
  return token;
}
/**
 * 随机生成token
 * @param {*} dataList 一个数组,用户生成的token不能和数组中的重复 (Array)
 * @param {*} tokenKey  数组dataList中标识token的属性名 (string)
 * @param {*} tokenLength 生成的长度  (Number)
 * @returns [String]
 */
async getToken(parameter: any): String {
  // 生成随机字符串
  const letters = "abcdefghijklmnopqrstuvwxyz0123456789";
  let token = "";
  for (let i = 0; i < parameter.tokenLength; i++) {
    if (i == 7 || i == 12 || i == 17 || i == 22) {
      token = token + letters.charAt(Math.floor(Math.random() * letters.length));
    } else {
      token += letters.charAt(Math.floor(Math.random() * letters.length));
    }
  }
  // 判断token是否重复
  let flagToken = parameter.dataList.some((item: any) => item[parameter.tokenKey] === token);
  // flagToken为true表示重复  false表示不重复
  if (!flagToken) return token;
  // 重新生成
  this.getToken(parameter);
}
--- 格式化时间
/**
 * 获取时间
 * @param {*} [parameter] <string/number> 用户指定的时间 
 * @returns 
 */
time(parameter: string): Object {
  // 补零
  const makeUpZero = (val: number) => (val < 10 ? `0${val}` : val);
  // 汉字化日期
  const dayArr = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
  // 获取时间
  let timer = parameter ? new Date(parameter) : new Date(),
    FullYear = timer.getFullYear(),
    month = makeUpZero(timer.getMonth() + 1),
    date = timer.getDate(),
    day = timer.getDay(),
    h = makeUpZero(timer.getHours()),
    m = makeUpZero(timer.getMinutes()),
    s = makeUpZero(timer.getSeconds());
  return {
    // 完整日期
    allDate: `${FullYear}/${month}/${date} ${dayArr[day]} ${h}:${m}:${s}`,
    // 年月日,小时分钟,秒
    allTime: `${FullYear}/${month}/${date} ${h}:${m}:${s}`,
    // 年月
    years: `${FullYear}/${month}`,
    // 年月日
    specificDate: `${FullYear}/${month}/${date}`,
    // 月日
    monthDay: `${month}/${date} ${dayArr[day]}`,
    // 时分秒
    hourMinuteSecond: `${h}:${m}:${s}`,
    // 每一项
    every: {
      // 年
      FullYear: FullYear,
      // 月
      month: month,
      // 日
      date: date,
      // 星期
      day: dayArr[day],
      // 小时
      h: h,
      // 分钟
      m: m,
      // 秒
      s: s,
    },
  };
}
--- 手写base64字符串加密
class base64 {
    constructor() {
        // 加密代替字符
        this._keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    }

    // 加密方法
    encode(strEncode) {
        var output = "";
        var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
        var i = 0;
        strEncode = _utf8_encode(strEncode);
        while (i < strEncode.length) {
            chr1 = strEncode.charCodeAt(i++);
            chr2 = strEncode.charCodeAt(i++);
            chr3 = strEncode.charCodeAt(i++);
            enc1 = chr1 >> 2;
            enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
            enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
            enc4 = chr3 & 63;
            if (isNaN(chr2)) {
                enc3 = enc4 = 64;
            } else if (isNaN(chr3)) {
                enc4 = 64;
            }
            output = output +
                this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
                this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
        }
        return output;

        // 转码处理
        function _utf8_encode(string) {
            string = string.replace(/\r\n/g, "\n");
            var utftext = "";
            for (var n = 0; n < string.length; n++) {
                var c = string.charCodeAt(n);
                if (c < 128) {
                    utftext += String.fromCharCode(c);
                } else if ((c > 127) && (c < 2048)) {
                    utftext += String.fromCharCode((c >> 6) | 192);
                    utfText += String.fromCharCode((c & 63) | 128);
                } else {
                    utftext += String.fromCharCode((c >> 12) | 224);
                    utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                    utftext += String.fromCharCode((c & 63) | 128);
                }
            }
            return utftext;
        }
    }

    // 解密方法
    decode(strDecode) {
        var output = "";
        var chr1, chr2, chr3;
        var enc1, enc2, enc3, enc4;
        var i = 0;
        strDecode = strDecode.replace(/[^A-Za-z0-9\+\/\=]/g, "");
        while (i < strDecode.length) {
            enc1 = this._keyStr.indexOf(strDecode.charAt(i++));
            enc2 = this._keyStr.indexOf(strDecode.charAt(i++));
            enc3 = this._keyStr.indexOf(strDecode.charAt(i++));
            enc4 = this._keyStr.indexOf(strDecode.charAt(i++));
            chr1 = (enc1 << 2) | (enc2 >> 4);
            chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
            chr3 = ((enc3 & 3) << 6) | enc4;
            output = output + String.fromCharCode(chr1);
            if (enc3 != 64) {
                output = output + String.fromCharCode(chr2);
            }
            if (enc4 != 64) {
                output = output + String.fromCharCode(chr3);
            }
        }
        output = _utf8_decode(output);
        return output;


        // 解码处理
        function _utf8_decode(utftext) {
            var string = "";
            var i = 0;
            var c = 0;
            var c1 = 0;
            var c2 = 0;
            while (i < utftext.length) {
                c = utftext.charCodeAt(i);
                if (c < 128) {
                    string += String.fromCharCode(c);
                    i++;
                } else if ((c > 191) && (c < 224)) {
                    c1 = utftext.charCodeAt(i + 1);
                    string += String.fromCharCode(((c & 31) << 6) | (c1 & 63));
                    i += 2;
                } else {
                    c1 = utftext.charCodeAt(i + 1);
                    c2 = utftext.charCodeAt(i + 2);
                    string += String.fromCharCode(((c & 15) << 12) | ((c1 & 63) << 6) | (c2 & 63));
                    i += 3;
                }
            }
            return string;
        }
    }
}

// 创建实例
const handle = new base64();

// 方法
const 加密 = handle.encode(str);
const 解密 = handle.decode(str);
--- 图片转Base64
/**
 * 将文件数组转换为 Base64 编码的 Promise
 * @param files 文件数组
 * @return Promise<{ name: string; type: string; size: number; base64: string, url: string }[]> 返回一个 Promise,解析为包含文件信息和 Base64 编码的对象数组
 */
filesToBase64(files: File[]): Promise<{ name: string; type: string; size: number; base64: string, url: string }[]> {
  // 使用 Promise.all 并发处理所有文件,确保所有文件都转换完成后才返回结果
  return Promise.all(files.map(file => new Promise<{ name: string; type: string; size: number; base64: string, url: string }>((resolve, reject) => {
    // 创建 FileReader 实例用于读取文件内容
    const reader = new FileReader();

    // 当文件读取成功完成时触发
    reader.onload = () => {
      resolve({
        name: file.name, // 文件名
        type: file.type, // 文件的 MIME 类型(如 image/png)
        size: file.size, // 文件大小(字节)
        base64: reader.result as string, // 读取结果,强制转换为字符串类型
        url: URL.createObjectURL(file) // 直接生成本地预览链接
      });
    };

    // 当文件读取发生错误时触发,拒绝(reject)该 Promise 并抛出错误
    reader.onerror = reject;

    // 以 Data URL(Base64)格式读取文件内容
    reader.readAsDataURL(file);
  })));
};
--- 原生自定义封装触底事件

javascript

/**
 * 通用底部触发回调函数(兼容+防抖+滚动方向+多场景扩展)
 * @param {Function} callback - 到达底部时执行的回调函数
 * @param {Object} options - 配置项(可选)
 * @param {number} options.offset - 提前触发偏移量(默认100px,值越大越早触发)
 * @param {number} options.debounceDelay - 防抖延迟(默认150ms)
 * @param {HTMLElement|null} options.scrollContainer - 滚动容器(默认window,支持自定义DOM)
 * @param {number|null} options.maxTriggerTimes - 最大触发次数(默认null,无限触发)
 * @param {boolean} options.repeatTrigger - 是否重复触发(默认false:触发一次后锁定,true:每次到达底部都触发)
 * @param {boolean} options.onlyDownScroll - 是否仅向下滚动触发(默认true:仅向下滚动到底部触发,false:忽略方向)
 * @param {number} options.triggerInterval - 同一底部状态下的最小触发间隔(默认1000ms,避免高频触发)
 * @returns {Function} 取消监听的函数(用于组件卸载时销毁)
 */
function onReachBottom(callback, options = {}) {
  // 默认配置
  const {
    offset = 100,
    debounceDelay = 150,
    scrollContainer = window,
    maxTriggerTimes = null,
    repeatTrigger = false,
    onlyDownScroll = true,
    triggerInterval = 1000 // 新增:同一底部状态最小触发间隔
  } = options;

  let isTriggered = false;
  let triggerCount = 0;
  let debounceTimer = null;
  let lastTriggerTime = 0; // 记录上次触发时间(用于间隔控制)
  const isWindow = scrollContainer === window;
  let lastScrollTop = getScrollTop();
  let isInBottom = false; // 标记当前是否处于底部状态

  // 优先获取正确的滚动位置(修复Safari兼容)
  function getScrollTop() {
    if (isWindow) {
      return window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
    }
    return scrollContainer.scrollTop;
  }

  // 处理自定义容器padding问题(包含内边距高度)
  function getClientHeight(element) {
    if (isWindow) {
      return window.innerHeight || document.documentElement.clientHeight;
    }
    const style = window.getComputedStyle(element);
    const paddingTop = parseFloat(style.paddingTop) || 0;
    const paddingBottom = parseFloat(style.paddingBottom) || 0;
    return element.clientHeight + paddingTop + paddingBottom;
  }

  // 底部判断逻辑(精准计算,兼容所有容器)
  function isReachBottom() {
    let scrollTop, scrollHeight, clientHeight;

    if (isWindow) {
      scrollTop = getScrollTop();
      scrollHeight = Math.max(
        document.body.scrollHeight,
        document.documentElement.scrollHeight
      );
      clientHeight = getClientHeight();
    } else {
      scrollTop = scrollContainer.scrollTop;
      scrollHeight = scrollContainer.scrollHeight;
      clientHeight = getClientHeight(scrollContainer);
    }

    // 增加1px容差,避免浮点精度问题
    return scrollTop + clientHeight + offset >= scrollHeight - 1;
  }

  // 滚动方向判断(先更新位置,再判断方向)
  function getScrollDirection() {
    const currentScrollTop = getScrollTop();
    const direction = currentScrollTop > lastScrollTop ? 'down' : 'up';
    lastScrollTop = currentScrollTop;
    return direction;
  }

  // 核心事件处理函数(修复重复触发问题)
  function handleScroll() {
    // 触发次数达上限 → 直接返回
    if (maxTriggerTimes !== null && triggerCount >= maxTriggerTimes) return;

    const direction = getScrollDirection();
    const reachedBottom = isReachBottom();
    const wasInBottom = isInBottom; // 记录上一次底部状态
    isInBottom = reachedBottom;

    // 未到底部 → 解锁触发标记(重复模式)
    if (!reachedBottom) {
      if (repeatTrigger) isTriggered = false;
      return;
    }

    // 仅向下滚动触发 → 方向不匹配返回
    if (onlyDownScroll && direction !== 'down') return;

    // 非重复模式已触发 → 返回
    if (isTriggered && !repeatTrigger) return;

    // 同一底部状态下,间隔不足不触发
    const now = Date.now();
    if (wasInBottom && now - lastTriggerTime < triggerInterval) return;

    // 只有从非底部进入底部时才触发(避免同一底部连续触发)
    if (wasInBottom && onlyDownScroll) return;

    // 防抖执行回调
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
      lastTriggerTime = now;
      callback({
        // 触发次数
        triggerCount: ++triggerCount,
        // 是否满足用户设置的最大触发次数
        isLastTrigger: maxTriggerTimes !== null && triggerCount === maxTriggerTimes,
        // 滚动方向
        scrollDirection: direction
      });

      // 非重复模式 → 触发后锁定
      if (!repeatTrigger) isTriggered = true;
    }, debounceDelay);
  }

  // 事件绑定(兼容移动端+被动监听)
  const eventTypes = ['scroll'];
  if (isWindow && 'ontouchstart' in window) {
    eventTypes.push('touchmove');
  }

  eventTypes.forEach(type => scrollContainer.addEventListener(type, handleScroll, { passive: true }));
  
  isWindow && window.addEventListener('resize', handleScroll);

  // 取消监听函数(防止内存泄漏)
  return function cancelOnReachBottom() {
    eventTypes.forEach(type => scrollContainer.removeEventListener(type, handleScroll, { passive: true }));
    isWindow && window.removeEventListener('resize', handleScroll)
    clearTimeout(debounceTimer);
  };
}

// 导出(浏览器环境可忽略,模块化环境启用)
if (typeof module !== 'undefined' && module.exports) {
  module.exports = onReachBottom;
}

使用示例:

// 封装的话导入一下
import onReachBottom from '@/app/reachBottom';
import { ref, onMounted, onUnmounted } from 'vue';

// 接收onReachBottom返回的函数,用于销毁监听事件,防止内存泄漏
let cancelScroll = ref(null);

// 编写触底业务逻辑
const handle = (msg) =>{
    console.log("触底了", msg)
}

onMounted(()=>{
    cancelScroll.value = onReachBottom(handle,
        { scrollContainer: container.value, repeatTrigger: true, offset: 150, onlyDownScroll: false, maxTriggerTimes: 10 }
        // { scrollContainer: window, repeatTrigger: true, offset: 100, onlyDownScroll: false, maxTriggerTimes: 100}
    );
})

// 组件销毁完成后触发
onUnmounted(()=>{
    // 销毁触底事件监听
    cancelScroll.value()
})

TS

/**
 * 回调函数参数类型定义(明确回调接收的参数结构)
 */
export interface ReachBottomCallbackParams {
  triggerCount: number; // 已触发次数(从1开始累加)
  isLastTrigger: boolean; // 是否是最后一次触发(基于maxTriggerTimes配置)
  scrollDirection: 'up' | 'down'; // 触发时的滚动方向
}

/**
 * 函数配置项类型定义(严格限制入参类型,避免错误传参)
 */
export interface OnReachBottomOptions {
  offset?: number; // 提前触发偏移量(默认100px,值越大越早触发,支持负数延后触发)
  debounceDelay?: number; // 防抖延迟(默认150ms,防止滚动时高频触发)
  scrollContainer?: HTMLElement | Window; // 滚动容器(默认window,支持自定义滚动DOM)
  maxTriggerTimes?: number; // 最大触发次数(默认undefined,无限触发;传数字限制次数)
  repeatTrigger?: boolean; // 是否重复触发(默认false:触发一次锁定;true:离开底部后可再次触发)
  onlyDownScroll?: boolean; // 是否仅向下滚动触发(默认true:仅向下滚到底部触发;false:忽略方向)
  triggerInterval?: number; // 同一底部状态最小触发间隔(默认1000ms,避免高频重复触发)
}

class reachBottom {
  // 配置项(实例属性,合并默认配置后存储)
  // 提前触发偏移量(默认100px,值越大越早触发,支持负数延后触发)
  private offset: number = 100;
  // 防抖延迟(默认150ms,防止滚动时高频触发)
  private debounceDelay: number = 150;
  // 滚动容器(默认window,支持自定义滚动DOM)
  private scrollContainer: HTMLElement | Window = window;
  // 最大触发次数(默认undefined,无限触发;传数字限制次数)
  private maxTriggerTimes: number | undefined = undefined;
  // 是否重复触发(默认false:触发一次锁定;true:离开底部后可再次触发)
  private repeatTrigger: boolean = false;
  // 是否仅向下滚动触发(默认true:仅向下滚到底部触发;false:忽略方向)
  private onlyDownScroll: boolean = true;
  // 同一底部状态最小触发间隔(默认1000ms,避免高频重复触发)
  private triggerInterval: number = 1000;

  // 非重复模式下的触发锁定标记
  private isTriggered: boolean = false;
  // 已触发次数计数器
  private triggerCount: number = 0;
  // 防抖定时器
  private debounceTimer: number | null = null;
  // 上次触发时间(用于间隔控制)
  private lastTriggerTime: number = 0;
  // 是否是window全局滚动容器
  private isWindow: boolean = true;
  // 上一次滚动位置(用于判断方向)
  private lastScrollTop: number = 0;
   // 当前是否处于底部状态
  private isInBottom: boolean = false;

  // 初始化需要绑定的事件类型(默认绑定scroll事件)
  private eventTypes: ('scroll' | 'touchmove' | 'resize')[] = ['scroll'];

   // 保存用户回调函数 【用户传入的回调函数】
  private callback: ( res: ReachBottomCallbackParams )=> void = ()=> {}; 

  constructor(){ }

  /**
  * 监听器【通用底部触发回调函数(兼容+防抖+滚动方向+多场景扩展)】
  * @param {Function} callback - 到达底部时执行的回调函数
  * @param {Object} options - 配置项(可选)
  * @param {number} options.offset - 提前触发偏移量(默认100px,值越大越早触发)
  * @param {number} options.debounceDelay - 防抖延迟(默认150ms)
  * @param {HTMLElement|null} options.scrollContainer - 滚动容器(默认window,支持自定义DOM)
  * @param {number|null} options.maxTriggerTimes - 最大触发次数(默认null,无限触发)
  * @param {boolean} options.repeatTrigger - 是否重复触发(默认false:触发一次后锁定,true:每次到达底部都触发)
  * @param {boolean} options.onlyDownScroll - 是否仅向下滚动触发(默认true:仅向下滚动到底部触发,false:忽略方向)
  * @param {number} options.triggerInterval - 同一底部状态下的最小触发间隔(默认1000ms,避免高频触发)
  */
  Listener(callback: (params: ReachBottomCallbackParams) => void, options: OnReachBottomOptions = {}){
    /*************** 初始化基础状态 ***************/
    // 合并参数
     const optionsResult = {
      // 偏移量
      offset: this.offset,
      // 防抖延迟
      debounceDelay: this.debounceDelay,
      // 监听的对象
      scrollContainer: this.scrollContainer,
      // 最大触发次数
      maxTriggerTimes: this.maxTriggerTimes,
      // 否重复触发
      repeatTrigger: this.repeatTrigger,
      // 是否仅向下滚动触发
      onlyDownScroll: this.onlyDownScroll,
      // 同一底部状态最小触发间隔
      triggerInterval: this.triggerInterval,
      // 用户配置项
      ...options,
    };
    // 保存用户传递的回调函数
    this.callback = callback;
    // 是否是window全局滚动容器
    options.scrollContainer && (this.isWindow = options.scrollContainer === window);
    // 提前触发偏移量(默认100px,值越大越早触发,支持负数延后触发)
    this.offset = optionsResult.offset
    // 防抖延迟(默认150ms,防止滚动时高频触发)
    this.debounceDelay = optionsResult.debounceDelay;
    // 滚动容器(默认window,支持自定义滚动DOM)
    this.scrollContainer = optionsResult.scrollContainer;
    // 最大触发次数(默认undefined,无限触发;传数字限制次数)
    this.maxTriggerTimes = optionsResult.maxTriggerTimes;
    // 是否重复触发(默认false:触发一次锁定;true:离开底部后可再次触发)
    this.repeatTrigger = optionsResult.repeatTrigger;
    // 是否仅向下滚动触发(默认true:仅向下滚到底部触发;false:忽略方向)
    this.onlyDownScroll = optionsResult.onlyDownScroll;
    // 同一底部状态最小触发间隔(默认1000ms,避免高频重复触发)
    this.triggerInterval = optionsResult.triggerInterval;
    // 上一次滚动位置(用于判断滚动方向)
    this.lastScrollTop = this.getScrollTop();

    // 仅全局容器(window)需要额外绑定:touchmove(移动端触摸滚动)、resize(窗口大小变化)
    if (this.isWindow) {
      if ('ontouchstart' in window) this.eventTypes.push('touchmove'); // 检测到移动端环境,绑定touchmove
      this.eventTypes.push('resize'); // 窗口大小变化时,重新计算容器高度,避免判断失效
    }

    // 监听事件
    this.addListener();
  }

  // 添加监听事件 【为滚动容器绑定所有需要的事件(passive: true提升移动端滚动性能)】
  private addListener(){
    // 为滚动容器绑定所有需要的事件(passive: true提升移动端滚动性能)
    this.eventTypes.forEach(type => {
      this.scrollContainer.addEventListener(type, this.handleScroll, {
        passive: true, // 告诉浏览器该事件不会阻止默认行为,提升滚动流畅度
        capture: false // 不使用事件捕获,使用冒泡阶段触发(符合常规滚动事件逻辑)
      });
    });
  }

  /**
   * 滚动事件核心处理函数(整合方向判断、底部判断、防抖、触发控制)
   */
  private handleScroll = (): void => {
    // 1. 触发次数达上限:直接返回,不再执行后续逻辑
    if (this.maxTriggerTimes !== undefined && this.triggerCount >= this.maxTriggerTimes) return;

    // 2. 获取关键状态:滚动方向 + 是否到达底部 + 上一次底部状态
    const direction = this.getScrollDirection(); // 当前滚动方向
    const reachedBottom = this.isReachBottom(); // 当前是否到达底部
    const wasInBottom = this.isInBottom; // 上一次是否处于底部状态(用于判断状态变化)
    this.isInBottom = reachedBottom; // 更新当前底部状态

    // 3. 未到达底部:解锁非重复模式的触发标记(让用户下次滚到底部可再次触发)
    if (!reachedBottom) {
      if (this.repeatTrigger) this.isTriggered = false; // 仅重复模式需要解锁
      return;
    }

    // 4. 仅向下滚动触发:如果方向不是向下,直接返回(忽略向上滚动触发)
    if (this.onlyDownScroll && direction !== 'down') return;

    // 5. 非重复模式已触发:如果已触发过且未解锁,直接返回(避免重复触发)
    if (this.isTriggered && !this.repeatTrigger) return;

    // 6. 同一底部状态间隔控制:如果还在底部且未到间隔时间,直接返回(避免高频触发)
    const now = Date.now(); // 当前时间戳
    if (wasInBottom && now - this.lastTriggerTime < this.triggerInterval) return;

    // 7. 同一底部状态过滤:如果之前已经在底部,直接返回(仅从非底部→底部时触发)关联底部状态和方向过滤【this.onlyDownScroll】,仅开启onlyDownScroll时拦截底部状态内滚动
    if (wasInBottom && this.onlyDownScroll) return;

    // 8. 防抖执行回调:清理之前的定时器,延迟执行回调(防止滚动高频触发)
    if (this.debounceTimer) clearTimeout(this.debounceTimer); // 清除旧定时器,避免多次触发
    // 浏览器环境用window.setTimeout(返回number类型,修复TS飘红)
    this.debounceTimer = window.setTimeout(() => {
      this.lastTriggerTime = now; // 更新上次触发时间(用于间隔控制)
      this.triggerCount++; // 触发次数累加
      // 执行用户传入的回调函数,传递触发相关参数
      this.callback({
        triggerCount: this.triggerCount, // 当前触发次数
        isLastTrigger: this.maxTriggerTimes !== undefined && this.triggerCount === this.maxTriggerTimes, // 是否最后一次触发
        scrollDirection: direction // 触发时的滚动方向
      });

      // 非重复模式:触发后锁定(避免同一底部状态重复触发)
      if (!this.repeatTrigger) this.isTriggered = true;
    }, this.debounceDelay); // 防抖延迟时间(用户可配置)
  };

  /**
   * 获取当前滚动位置(兼容window和自定义容器,修复Safari浏览器兼容问题)
   * @returns 滚动距离顶部的像素值(number类型,确保非undefined)
   */
  private getScrollTop(): number {
    // 全局滚动容器(window):优先取pageYOffset,兼容所有浏览器
    if (this.isWindow) {
      return window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop || 0; // 兜底0,避免返回undefined
    }
    // 自定义容器:断言为HTMLElement(已限制入参类型,断言安全),获取scrollTop
    return (this.scrollContainer as HTMLElement).scrollTop;
  };

  /**
   * 判断滚动方向(先更新上一次滚动位置,再判断当前方向,避免方向误判)
   * @returns 滚动方向('up':向上滚;'down':向下滚)
   */
  private getScrollDirection (): 'up' | 'down' {
    const currentScrollTop = this.getScrollTop(); // 获取当前滚动位置
    const direction = currentScrollTop > this.lastScrollTop ? 'down' : 'up'; // 对比上一次位置判断方向
    this.lastScrollTop = currentScrollTop; // 更新上一次位置(为下一次判断做准备)
    return direction;
  };

  /**
   * 判断是否到达容器底部(核心判断逻辑,兼容所有容器和浏览器)
   * @returns 是否到达底部(true:已到达/超过;false:未到达)
   */
  private isReachBottom (): boolean {
    let scrollTop: number; // 滚动距离顶部的距离
    let scrollHeight: number; // 容器内容总高度(包含不可见部分)
    let clientHeight: number; // 容器可视高度(含padding)

    // 全局滚动容器:计算页面总高度(取最大高度值,避免浏览器差异)
    if (this.isWindow) {
      scrollTop = this.getScrollTop();
      scrollHeight = Math.max(
        document.body.scrollHeight, // body滚动高度
        document.documentElement.scrollHeight // html滚动高度
      );
      clientHeight = this.getClientHeight(); // 窗口可视高度
    } else {
      // 自定义容器:直接获取元素的滚动相关属性(已断言为HTMLElement)
      const element = this.scrollContainer as HTMLElement;
      scrollTop = element.scrollTop;
      scrollHeight = element.scrollHeight; // 元素内容总高度
      clientHeight = this.getClientHeight(); // 元素可视高度(含padding)
    }

    // 核心判断公式:滚动位置 + 可视高度 + 提前偏移 ≥ 内容总高度 - 1px容差
    // 减1px是为了避免浮点精度问题(比如滚动高度为小数时判断失效)
    return scrollTop + clientHeight + this.offset >= scrollHeight - 1;
  };

  /**
   * 计算容器有效高度(包含padding,解决自定义容器padding导致的尺寸偏差)
   * @returns 容器实际可滚动可视高度(包含内边距)
   */
  private getClientHeight (): number {
    // 全局容器:取窗口可视高度,兼容不同浏览器
    if (this.isWindow) {
      return window.innerHeight || document.documentElement.clientHeight || 0; // 兜底0
    }
    // 自定义容器:获取元素样式,计算paddingTop+paddingBottom(clientHeight不含padding)
    const element = this.scrollContainer as HTMLElement;
    const style = window.getComputedStyle(element); // 获取计算后样式(含单位转换)
    const paddingTop = parseFloat(style.paddingTop) || 0; // 解析paddingTop(默认0)
    const paddingBottom = parseFloat(style.paddingBottom) || 0; // 解析paddingBottom(默认0)
    // 有效高度 = 容器可视高度 + 上下内边距(确保滚动高度计算准确)
    return element.clientHeight + paddingTop + paddingBottom;
  };

  /**
   * 取消监听函数(外部调用,用于销毁事件绑定和定时器)
   * 必须在组件卸载/不需要时调用,防止内存泄漏(事件绑定未移除导致的性能问题)
   */
  destroy(): void {
    // 移除所有绑定的事件(与绑定的事件类型、处理函数、capture保持一致)
    this.eventTypes.forEach(type => {
      // removeEventListener不支持passive选项,仅需匹配capture(与add时一致为false)
      this.scrollContainer.removeEventListener(type, this.handleScroll, false);
    });
    // 清除防抖定时器(避免定时器延迟执行导致的回调误触发)
    if (this.debounceTimer) clearTimeout(this.debounceTimer);
  }
}

export default new reachBottom();

使用方法

// 封装的话导入一下
import onReachBottom from '@/app/reachBottom';
import { onMounted, onUnmounted } from 'vue';

// 编写触底业务逻辑
const handle = (msg) =>{
    console.log(`第${msg.triggerCount}次触发,加载更多数据`,msg);
}

onMounted(()=>{
    onReachBottom.Listener(handle,
        { scrollContainer: container.value, repeatTrigger: true, offset: 150, onlyDownScroll: false, maxTriggerTimes: 10 }
        // { scrollContainer: window, repeatTrigger: true, offset: 100, onlyDownScroll: false, maxTriggerTimes: 100}
    );
})

// 组件销毁完成后触发
onUnmounted(()=>{
    // 销毁触底事件监听
    onReachBottom.destroy();
})

--- dom元素拖动Fixed定位(支持PC+移动端)
type PARAMETER = (parameter: {
    // 触发事件的类型
    eventType: string;
    // 是否是移动端
    isMobile: string;
    // 当前移动的状态
    state: string;
    // 当前移动中X轴的坐标
    left?: number;
    // 当前移动中Y轴的坐标
    top?: number;
    // 边界判断
    boundary?: {
        // 是否到达最顶部
        isTop: boolean,
        // 是否到达最下部
        isBottom: boolean,
        // 是否到达最左边
        isLeft: boolean,
        // 是否到达最右边
        isRight: boolean,
    };
}) => void;

class coordinates {
    // 音频元素的宽度以及高度
    private domOffsetWidth: number = 0;
    private domOffsetHeight: number = 0;
    // 点击音频元素内的坐标
    private offsetX: number = 0;
    private offsetY: number = 0;
    // 用于映射事件
    private map: Map<string, any> = new Map();
    // 滑动的元素
    private htmlDom: any;

    constructor(){ }

    // 监听事件
    change(callback?: PARAMETER): void {
        // 鼠标松开时触发
        const mouseup = (e: any) =>{
            // 阻止浏览器默认行为
            e.preventDefault();
            // 阻止事件冒泡
            e.stopPropagation();

            // e.type.includes("touch") true表示【移动端】, false表示【电脑端】
            const events = e.type.includes("touch") ? ['touchmove', 'touchend'] : ['mousemove', 'mouseup'];
            // 移除document事件
            events.forEach((eventKey: string)=> document.removeEventListener(eventKey, this.map.get(eventKey)));

            /** ------ mouseup中的四个边界判断 ------ */
            const playerRect = this.htmlDom!.getBoundingClientRect(); // 获取最终位置

            // 1. 判断是否到达最左边(左边缘 ≤ 0)
            const isAtLeftEdge = Math.round(playerRect.left) <= 0;
            // 2. 判断是否到达最右边(右边缘 ≥ 可视区宽度)
            const isAtRightEdge = Math.round(playerRect.right) >= window.innerWidth;
            // 3. 判断是否到达最上边(上边缘 ≤ 0)
            const isAtTopEdge = Math.round(playerRect.top) <= 0;
            // 4. 判断是否到达最下边(下边缘 ≥ 可视区高度)
            const isAtBottomEdge = Math.round(playerRect.bottom) >= window.innerHeight;

            // 触发的事件类型
            const eventType = e.type.includes("touch") ? 'touchend' : 'mouseup';

            // 为true表示到达最边界的位置
            callback && callback({
                // 触发事件的类型
                eventType,
                // 是否是移动端
                isMobile: e.type.includes("touch"),
                // 当前移动的状态
                state: 'end',
                // 边界判断
                boundary:{
                    // 是否到达最顶部
                    isTop: isAtTopEdge,
                    // 是否到达最下部
                    isBottom: isAtBottomEdge,
                    // 是否到达最左边
                    isLeft: isAtLeftEdge,
                    // 是否到达最右边
                    isRight: isAtRightEdge,
                }
            });
        }

        // 鼠标在元素内移动时实时触发
        const mousemove = (e: any) =>{
            // 阻止浏览器默认行为
            e.preventDefault();
            // 阻止事件冒泡
            e.stopPropagation();
            
            // 兼容鼠标和触摸事件的坐标获取
            const clientX = e.type.includes("touch") ? e.touches[0].clientX : e.clientX;
            const clientY = e.type.includes("touch") ? e.touches[0].clientY : e.clientY;

            // 移动元素的实时坐标
            const x = clientX - this.offsetX;
            const y = clientY - this.offsetY;
            // 限制播放器在可视区域内(最大边界限制)
            const maxX = window.innerWidth - this.domOffsetWidth;
            const maxY = window.innerHeight - this.domOffsetHeight;

            //  相当于判断边界,不允许超过最大边界
            const domX = Math.max(0, Math.min(x, maxX));
            const domY = Math.max(0, Math.min(y, maxY));
            
            // 触发的事件类型
            const eventType = e.type.includes("touch") ? 'touchmove' : 'mousemove';
            
            // 实时返回坐标数据
            callback && callback({
                // 触发事件的类型
                eventType,
                // 是否是移动端
                isMobile: e.type.includes("touch"),
                // 当前移动的状态
                state: 'move',
                // 当前移动中X轴的坐标
                left: domX,
                // 当前移动中Y轴的坐标
                top: domY
            })
        }

        // 映射事件到map中
        // 电脑端
        this.map.set("mousemove", mousemove);
        this.map.set("mouseup", mouseup);
        // 手机端
        this.map.set("touchmove", mousemove);
        this.map.set("touchend", mouseup);
    }

    // 鼠标按下事件
    mousedown(e: any): void{
        // 阻止事件冒泡
        e.stopPropagation();
        
        // 第二中冒泡解决方法
        // if(e.target.dataset.自定义属性名 !== '自定义属性值') return;
        // 示例【2】
        // if(e.target.dataset.slide !== 'audioBox') return;

        // e.type.includes("touch") true表示【移动端】, false表示【电脑端】
        const events = e.type.includes("touch") ? ['touchmove', 'touchend'] : ['mousemove', 'mouseup'];
        // 添加事件
        events.forEach((eventKey: string)=> document.addEventListener(eventKey, this.map.get(eventKey), { passive: false }));

        // 保存当前点击的元素
        this.htmlDom = e.target;

        // 获取当前鼠标点击的坐标
        const clientX = e.type.includes("touch") ? e.touches[0].clientX : e.clientX;
        const clientY = e.type.includes("touch") ? e.touches[0].clientY : e.clientY;

        // 获取鼠标相对于播放器的偏移量
        this.offsetX = clientX - e.target.getBoundingClientRect().left;
        this.offsetY = clientY - e.target.getBoundingClientRect().top;

        // 当前移动元素的高度以及宽度
        this.domOffsetWidth = e.target.offsetWidth;
        this.domOffsetHeight = e.target.offsetHeight;
    }
}
// 暴露数据
export default new coordinates();

使用示例:

<template>
    <!-- 分别添加mousedown和touchstart事件【要是PC和移动都需要支持,两者必需都绑定】,mousedown必需传入事件对象, 如果【movingCoordinates.mousedown】这样写,可省略-->
    <div @mousedown.stop="movingCoordinates.mousedown" @touchstart.stop="movingCoordinates.mousedown"></div>
</template>

************ 关于事件冒泡的问题 *************
    由于使用mousedown和touchstart,阻止事件冒泡必须是相同事件类型才行,不同事件类型不能阻止冒泡行为。
	例如:需要移动的dom元素绑定的mousedown事件,但dom元素下的子元素需要click...等等事件,必需要阻止冒泡行为,不能写click.stop,因为事件类型不同,阻止不了。
	解决方法【1】:dom元素下需要绑定其它事件是,加上【@mousedown.stop @touchstart.stop】两个事件即可解决。
	解决方法【2】:在class对象的mousedown方法加入判断,比如判断自定义属性,假如属性名为'slide',属性值为:'audioBox'的才能执行后续代码,否则直接【return】退出。

<script setup lang='ts'>
// 导入class对象
import movingCoordinates from './movingCoordinates';

movingCoordinates.change((result)=>{
    // 元素拖动后的坐标,拖动时实时返回最新坐标信息
    console.log(result);
})
</script>
--- 音频功能封装
interface initOptions {
    // 音频元素或音频对象
    audio: HTMLAudioElement;
    // 音频地址链接的字段名
    srcKey?: string;
    // 当前播放的音频链接
    currentPlayAudio?:  { [key: string]: any };
    // 音频列表集合
    audioList?: Array<{[key: string]: any}>;
    // 回调函数中返回的监听事件
    listeners?: {
        onLoad?: (res: { totalTime: number; currentTime: number; [key: string]: any }) => void;
        onTimeupdate?: (res: { totalTime: number; currentTime: number; cacheProgress: number; cacheSecond: number, currentTimeProgress: number }) => void;
        onPlayStatus?: (res: { playStatus: boolean, msg: string }) => void;
        onBuffer?: (res: { loading: boolean; msg: string }) => void;
        onRatechange?: (res: { playbackRate: number }) => void;
        onVolumechange?: (res: { volume: number, isMute: boolean }) => void;
        onEnded?: (controls: {
            loop: () => void;
            last: () => void;
            next: () => void;
            random: () => void;
        }) => void;
        onError?: (error: { status: number, errorMsg: string })=> void;
    };
}

class Audio {
    // 音频的html元素
    private $audio: HTMLAudioElement = null as unknown as HTMLAudioElement;
    // 音频地址链接的字段名
    #srcKey: string = "";
    // 音频列表集合
    private $audioList: Array<any> = [];
    // 当前播放的音频链接
    private $currentPlayAudio: any = null;
    // 判断是否重复执行
    #currentTimeFlag: number = 0;
    // 用于保存当前的监听事件集
    #listenerMap: Map<string, any> = new Map();
    // 保存用户传递的回调函数
    #callback: initOptions['listeners'];
    // 自定义事件名对应的audio的事件类型【监听事件】
    #eventsKeyName: {[key: string]: string | string[]} = {
        // 初始化加载完成后触发该函数,返回当前音频的总时间,名称等参数  (每当音频路径发生改变时都会触发,并返回最新的音频数据等信息)
        onLoad: 'loadedmetadata',
        // 视频缓冲中/缓冲完成执行该函数(监听视频正在缓冲资源)
        onTimeupdate: "timeupdate",
        // 音频播放开始时触发(包括暂停后继续播放) 暂停时也会触发
        onPlayStatus: ['play', 'pause'],
        // 播放进度改变时触发
        onBuffer: ['waiting', 'canplay'],
        // 播放倍数改变时触发
        onRatechange: 'ratechange',
        // 音频音量发生改变时触发【静音和音量发生改变时都会触发,静音和音量是两个完全独立的状态】
        onVolumechange: 'volumechange',
        // 当前音频播放结束时触发该函数
        onEnded: 'ended',
        // 播放错误触发,返回常见错误:文件不存在(404)、格式不支持、跨域限制
        onError: 'error'
    }

    constructor() {
        // 初始化映射事件到map中
        this.#mappingEvents();
    }

    /** 初始化函数 */
    init(options: initOptions): void {
        // 音频字段名称未指向
        if (!options.hasOwnProperty("srcKey")) return console.log(new Error("未指向字段名称,请先指向音频地址值的字段名称"));
        // 音频字段名称类型错误
        if (typeof options.srcKey !== "string") return console.log(new Error("字段名称类型错误,请确保字段名称为字符串类型"));

        // 保存音频字段名
        if(options.srcKey) this.#srcKey = options.srcKey;

        // 保持当前的aduio html元素或audio对象 【如果this.$audio存在音乐对象,则没必要重新配置】
        if(options.audio && !this.$audio) this.$audio = options.audio;

        // 仅预加载元数据(时长、格式等);
        this.$audio.preload = 'metadata';
        this.$audio.setAttribute('preload', 'metadata'); // 设置预加载模式【双重保障】

        /** iOS 专用属性 */
        // 使视频(或音频)在页面内播放(而不是全屏播放)。对于音频来说,这个属性主要是为了确保音频在页面内播放而不被系统接管。
        this.$audio.setAttribute('playsinline', 'true');
        // webkit-playsinline是旧版WebKit内核的前缀版本,为了兼容旧版iOS
        this.$audio.setAttribute('webkit-playsinline', 'true');
        // 允许通过AirPlay将音频(或视频)投射到其他Apple设备上播放。设置为allow表示允许AirPlay
        this.$audio.setAttribute('x-webkit-airplay', 'allow');

        // 判断当前播放的音频对象是否存在
        if(options.currentPlayAudio) {
            // 当前播放的音频对象错误
            if(options.currentPlayAudio == null || typeof options.currentPlayAudio !== "object") return console.log(new Error("当前播放的音频对象错误,请确保传入值的类型为object"));

            // 音频字段名称指向错误
            if (!options.currentPlayAudio.hasOwnProperty(options.srcKey)) return console.log(new Error("该字段不存在,请重新指向音频地址值的字段名称"));

            // 初始化路径配置项未指向
            if (!options.currentPlayAudio[this.#srcKey]) return console.log(new Error("未指向当前播放的路径,应初始化时指向初始化路径"));

            // 当前播放的音频链接
            this.$currentPlayAudio = options.currentPlayAudio;

            // 设置没默认播放链接
            this.$audio.src = options.currentPlayAudio[this.#srcKey];
        }

        // 保存当前音频列表
        options.audioList && Array.isArray(options.audioList) && (this.$audioList = options.audioList);

        // 保存配置的监听对象,方便后续调用返回数据
        options.listeners && (this.#callback = options.listeners);

        // 开启监听事件
        this.listeners(this.#callback);
    }

    // 开启监听器
    listeners(callback: initOptions['listeners']){
        // 保存配置项
        this.#callback = callback;

        // 如果this.$audio值为空,表示audio还没有创建或没有获取到
        if(!this.$audio || this.#callback === undefined) return;

        // 移除旧监听器
        this.destroy();
        
        // 筛选出已配置的函数名
        const eventKey = Object.keys(callback!);
        // 转换audio的真实事件名
        const events = eventKey.reduce((event: any, item)=> Array.isArray(this.#eventsKeyName[item]) ? [...event, ...this.#eventsKeyName[item]] : [...event, this.#eventsKeyName[item]], [])

        // 循环添加已配置的监听事件 (在Map中获取)
        for (const item of events) {
            // 添加audio相关事件
            this.$audio.addEventListener(item, this.#listenerMap.get(item));
        }
    }

    /** 销毁监听器 */
    destroy(options?: Array<'onLoad' | 'onTimeupdate' | 'onPlayStatus' | 'onBuffer' | 'onRatechange' | 'onVolumechange' | 'onEnded' | 'onError'>): void {
        // 判断是否指定注销某一个或多个监听器
        if(options && Array.isArray(options) && options.length >= 1){
            // 转换audio的真实事件名
            const events = options.reduce((event: any, item)=> Array.isArray(this.#eventsKeyName[item]) ?  [...event, ...this.#eventsKeyName[item]] : [...event, this.#eventsKeyName[item]], [])
            // 遍历销毁chang监听器
            for (const key of events) {
                // 注销audio相关事件
                this.$audio.removeEventListener(key!, this.#listenerMap.get(key!));
                // 删除map中的映射事件
                // this.#listenerMap.delete(key!);
            }
            return;
        }

        // 遍历销毁chang监听器
        for (const [key, value] of this.#listenerMap) {
            // 注销audio相关事件
            this.$audio.removeEventListener(key, value);
            // 删除map中的映射事件
            // this.#listenerMap.delete(key!);
        }
    }

    /**
     * 清空【释放所有变量】
     */
    clear(): void {
        // 音频的html元素
        this.$audio = null as unknown as HTMLAudioElement;
        // 音频地址链接的字段名
        this.#srcKey = "";
        // 音频列表集合
        this.$audioList = [];
        // 当前播放的音频链接
        this.$currentPlayAudio = null;
        // 判断是否重复执行
        this.#currentTimeFlag = 0;
        // 用于保存当前的监听事件集
        this.#listenerMap.clear();
        // 保存用户传递的回调函数
        this.#callback = {};
    }

    /** 播放 */
    play(): void {
        // 当前播放的音频对象错误
        if(this.$currentPlayAudio == null || typeof this.$currentPlayAudio !== "object") return console.log(new Error("当前播放的音频对象错误,请确保传入值的类型为object"));
        // 播放
        this.$audio.play();

        // ios获取封面和作者的唯一途径【菜单栏和灵动岛】
        navigator.mediaSession.metadata = new MediaMetadata({
            // 音频标题
            title: this.$currentPlayAudio.title,
            // 作者
            artist: this.$currentPlayAudio.author,
            // 封面
            artwork:[{ src: this.$currentPlayAudio.cover }]
        });
    }

    /** 暂停播放 */
    pause(): void {
        // 判断是否存在播放对象
        if(this.$currentPlayAudio == null || typeof this.$currentPlayAudio !== "object") return;
        // 暂停播放
        this.$audio.pause();
    }

    /**
     * 设置播放速率
     * @param num <Number> 0.5~4.0 【支持 0.5~4.0 范围】
     */
    setPlaybackRate(num: number): void {
        // 判断audio音频元素或音频对象是否存在
        if(!this.$audio) return;
        // 设置播放速率
        this.$audio.playbackRate = num;
    }

    /**
     * 获取播放速率
     * @return num <Number> 0.5~4.0 【0.5~4.0 范围】
     */
    getPlaybackRate(): number {
        // 判断audio音频元素或音频对象是否存在
        if(!this.$audio) return 0;
        // 设置播放速率
        return this.$audio.playbackRate;
    }

    /**
    * 设置音频音量 
    * @param {*} num <Number> 0~100
    */
    volume(num: number): void {
        // 判断audio音频元素或音频对象是否存在
        if(!this.$audio) return;
        // 设置音频音量
        this.$audio.volume = Number(num / 100);
    }

    /**
    * 获取音频音量 
    * @return num <Number> 0~100
    */
    getVolume(): number {
        // 判断audio音频元素或音频对象是否存在
        if(!this.$audio) return 0;
        // 设置音频音量
        return Number(this.$audio.volume * 100);
    }

    /** 静音 */
    muted(): void {
        // 判断audio音频元素或音频对象是否存在
        if(!this.$audio) return;
        // 静音
        this.$audio.muted = true;
    }

    /** 解除静音 */
    relieveMuted(): void {
        // 判断audio音频元素或音频对象是否存在
        if(!this.$audio) return;
        // 解除静音
        this.$audio.muted = false;
    }

    /**
    * 设置当前音频播放的进度
    * @param {*} num <Number> 单位秒
    */
    step(num: number): void {
        // 判断是否存在播放对象
        if(this.$currentPlayAudio == null || typeof this.$currentPlayAudio !== "object") return;
        // 设置当前音频播放的进度
        this.$audio.currentTime = num;
    }

    /** 上一首 */
    last(): void {
        // 判断该音频集合是不是为一个链接
        if (this.$audioList.length <= 1) return;
        // 查找当前播放音频的索引
        let index = this.$audioList.findIndex((item) => item[this.#srcKey] == this.$currentPlayAudio[this.#srcKey]);
        // 索引递减(为上一音频)
        index--;
        // 判断当前索引是否超过最小索引值
        if (index < 0) index = this.$audioList.length - 1;
        // 保存当前音频对象信息
        this.$currentPlayAudio = this.$audioList[index];
        // 赋值给audio当前音频的路径
        this.$audio.src = this.$audioList[index][this.#srcKey];
        // 进行播放
        this.play();
    }

    /** 下一首 */
    next(): void {
        // 判断该音频集合是不是为一个链接
        if (this.$audioList.length <= 1) return
        // 查找当前播放音频的索引
        let index = this.$audioList.findIndex((item) => item[this.#srcKey] == this.$currentPlayAudio[this.#srcKey]);
        // 索引递增 (为下一音频)
        index++;
        // 判断当前索引是否超过最大索引值
        if (this.$audioList.length - 1 < index) index = 0;
        // 保存当前音频对象信息
        this.$currentPlayAudio = this.$audioList[index];
        // 赋值给audio当前音频的路径
        this.$audio.src = this.$audioList[index][this.#srcKey];
        // 进行播放
        this.play();
    }

    /** 随机播放 */
    random(): void {
        // 判断该音频集合是不是为一个链接
        if (this.$audioList.length <= 1) return;
        // 筛选出除当前播放音频的链接路径集合 (在其中随机选取一个进行播放)
        let audioItems = this.$audioList.filter(item => item[this.#srcKey] != this.$currentPlayAudio[this.#srcKey]);
        // 生成随机数
        const rand = this.#randomNumber(0, audioItems.length - 1);
        // 保存当前音频对象信息
        this.$currentPlayAudio = audioItems[rand];
        // 赋值给audio当前音频的路径
        this.$audio.src = audioItems[rand][this.#srcKey];
        // 进行播放
        this.play();
    }

    /**
     * 设置音频地址链接的字段名
     * @param key <string> 音频地址的字段名
     */
    setSrcKey(key: string): void {
        if(key) this.#srcKey = key;
    }

    /** 循环播放 */
    loop(): void {
        // 恢复默认节流值
        this.#currentTimeFlag = 0;
        // 播放
        this.play();
    }

    /**
     * 切换音乐播放
     * @param currentPlayAudio 切换的音频对象 <Object>
     * @param audioList 音频集合 <Array | funtion>
     * 数组对象的形式 如: [{name: "",url :""},{name: "",url :""},等等...]
     * 函数形式 (res) => {
     *  return [...res, 新的音频配置项信息-如:{name: "",url :""}, 等等]
     * 注:函数形式必须有返回值,类型为Array<{[key: string]: any}>,可以操作当前音频列表集合
     * }
     */
    switch(currentPlayAudio: initOptions["currentPlayAudio"], audioList?: ((data: Array<{[key: string] : any}>)=> initOptions["audioList"]) | initOptions["audioList"]): void {
        // 判断切换的音频对象是否错误
        if(currentPlayAudio == null || typeof currentPlayAudio !== "object") return console.log(new Error("当前播放的音频对象错误,请确保传入值的类型为object"));
        // 判断音频字段名称指向是否错误
        if (!currentPlayAudio.hasOwnProperty(this.#srcKey)) return console.log(new Error("该字段不存在,请重新指向音频地址值的字段名称"));
        
        // 优化处理,切换相同路径时,不进行任何操作
        if(this.$currentPlayAudio !== currentPlayAudio){
            // 保存当前音频对象信息
            this.$currentPlayAudio = currentPlayAudio;
            // 赋值给audio当前音频的路径
            this.$audio.src = currentPlayAudio[this.#srcKey];
            // 进行播放
            this.play();
        }

        /** 切换音频可替换需要播放的音频集合 (可有可无) */
        // 直接传递的数组
        audioList && Array.isArray(audioList) && (this.$audioList = audioList);
        // 传递的是一个函数
        audioList && typeof audioList === 'function' && (this.$audioList = audioList(this.$audioList)!);
    }

    /**
     * 修改音频列表
     * @param audioList 音频集合 <Array | funtion>
     * 数组对象的形式 如: [{name: "",url :""},{name: "",url :""},等等...]
     * 函数形式 (res) => {
     *  return [...res, 新的音频配置项信息-如:{name: "",url :""}, 等等]
     * 注:函数形式必须有返回值,类型为Array<{[key: string]: any}>,可以操作当前音频列表集合
     * } 
     */
    setAudioList(audioList: ((data: Array<{[key: string] : any}>)=> initOptions["audioList"]) | initOptions["audioList"]): void {
        // 直接传递的数组
        audioList && Array.isArray(audioList) && (this.$audioList = audioList);
        // 传递的是一个函数
        audioList && typeof audioList === 'function' && (this.$audioList = audioList(this.$audioList)!);
    }

    /**
     * 将秒数格式化为 mm:ss 格式的时间字符串
     * @param {number} seconds - 输入的秒数(可以是整数或小数)
     * @returns {string} 格式化后的时间,如 02:30
     */
    formatTime(seconds: number): string {
        // 处理负数或非数字的情况,默认返回 00:00
        if (isNaN(seconds) || seconds < 0) return '00:00';

        // 计算分钟和秒数,取整
        const mins = Math.floor(seconds / 60), secs = Math.floor(seconds % 60);

        // 补零处理,确保是两位数并返回处理结果
        return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
    }

    /**
     * 判断是否是ios设备
     * @returns Boolean true表示为ios设备, false表示不是
     */
    isSafari(): boolean {
        const userAgent = navigator.userAgent.toLowerCase();
        // 核心判断:包含safari且排除其他浏览器的伪装
        return /safari/.test(userAgent) && !/chrome|chromium|edge|firefox|brave|opera|vivaldi/.test(userAgent);
    }

    /** 
     * 获取音频缓存进度
     * @return { progress: number, cacheSecond: number } 【progress】:当前缓存音频的百分比,【cacheSecond】;当前缓存音频的秒数
     */
    #getBufferProgress(): { progress: number, cacheSecond: number }{
        // 确保音频总时长已加载
        if (this.$audio.duration === Infinity || isNaN(this.$audio.duration)) return { progress: 0, cacheSecond: 0 };

        // 默认缓存进度
        let bufferedEnd = 0;
        // 获取已缓存的时间范围(可能有多个分段)
        if (this.$audio.buffered.length > 0) {
            // 取最后一个缓存分段的结束时间(代表已缓存的最大位置)
            bufferedEnd = this.$audio.buffered.end(this.$audio.buffered.length - 1);
        }

        // 计算缓存进度 使用Math.min(val, 100)确保不超过100%
        const progress = Math.min((bufferedEnd / this.$audio.duration) * 100, 100);
        // 当前已缓存的音频秒数
        const cacheSecond = Math.trunc(bufferedEnd);
        // 返回缓存音频百分比进度progress,以及秒数cacheSecond
        return { progress, cacheSecond };
    }

    /**
    * @randomNumber 动态生成随机数
    * @param {*} min 最小值 (包含) <Number>
    * @param {*} max 最大值(包含) <Number>
    * @returns retuen <数字 Number>
    */
    #randomNumber(min: number, max: number): number {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    // 映射事件到map中
    #mappingEvents(): void{
        // 判断是否已经初始化
        if (this.#listenerMap.size > 0) return; // 已初始化则跳过

        /** 添加事件到Map中 */
        // 当指定的音频/视频的元数据已加载时 (触发) 或音频路径切换时,必定会重新加载新的音频数据(也会触发)
        this.#listenerMap.set('loadedmetadata', this.#loadedmetadata.bind(this));
        // 播放进度监听
        this.#listenerMap.set('timeupdate', this.#timeupdate.bind(this));
        // 音频播放开始时触发(包括暂停后继续播放)
        this.#listenerMap.set('play', this.#play.bind(this));
        // 音频暂停时执行该函数
        this.#listenerMap.set('pause', this.#pause.bind(this));
        // 资源加载中(用于loading加载)【资源正在缓冲中】
        this.#listenerMap.set('waiting', this.#waiting.bind(this));
        // 资源加载完成(用于loading加载)【资源缓冲完成】
        this.#listenerMap.set('canplay', this.#canplay.bind(this));
        // 播放速率发生改变时执行该函数
        this.#listenerMap.set('ratechange', this.#ratechange.bind(this));
        // 音量发生改变时执行该函数【静音和音量发生改变时都会触发,静音和音量是两个完全独立的状态】
        this.#listenerMap.set('volumechange', this.#volumechange.bind(this));
        // 该音频播放结束执行
        this.#listenerMap.set('ended', this.#ended.bind(this));
        // 播放错误触发,可通过 audio.error 获取具体错误信息,常见错误:文件不存在(404)、格式不支持、跨域限制
        this.#listenerMap.set('error', this.#error.bind(this));
    }

    // 当指定的音频/视频的元数据已加载时 (触发) 或音频路径切换时,必定会重新加载新的音频数据(也会触发)
    #loadedmetadata = (): void => {
        // 恢复默认节流值
        this.#currentTimeFlag = 0;
        // 当前音频的总时间【优化烦人的ios在播放失败的时候,获取不到音频的总时间,搞个无穷大来糊弄我】
        const totalTime = this.$audio.duration === Infinity ? 0 : Math.trunc(this.$audio.duration);
        // 当前音加载时触发该函数
        this.#callback && this.#callback.onLoad && this.#callback.onLoad({
            // 当前音频的总时间
            totalTime,
            // 当前播放时间
            currentTime: 0,
            // 音频源数据信息
            ...this.$currentPlayAudio,
        })
    }

    // 播放进度监听
    #timeupdate = (): void => {
        // 总时间
        let duration = Math.trunc(this.$audio.duration);
        // 当前播放进度的时间
        let currentTime = Math.trunc(this.$audio.currentTime);
        // 判断是否重复触发
        if (this.#currentTimeFlag == currentTime) return;
        // 记录当前播放的时间 (防止重复触发)
        this.#currentTimeFlag = currentTime;

        // 判断duration值是否为NaN
        if(Number.isNaN(duration)) return;

        // 返回当前时间以及总时间
        this.#callback && this.#callback.onTimeupdate && this.#callback.onTimeupdate({
            // 总时间
            totalTime: duration,
            // 当前播放的时间
            currentTime: currentTime,
            // 当前已播放的百分比,可用于进度的制作
            // currentTimeProgress: Number(Math.min((currentTime / duration) * 100, 100).toFixed(2)), // 只保留后两位小数
            currentTimeProgress: Math.min((currentTime / duration) * 100, 100),
            // 缓存进度(百分比)
            // cacheProgress: Number(this.#getBufferProgress().progress.toFixed(2)), // 只保留后两位小数
            cacheProgress: this.#getBufferProgress().progress,
            // 缓存进度(秒数)
            cacheSecond: this.#getBufferProgress().cacheSecond,
        });
    }

    // 音频播放开始时触发(包括暂停后继续播放)
    #play = (): void => this.#callback && this.#callback.onPlayStatus && this.#callback.onPlayStatus({ playStatus: true, msg: "开始播放" })

    // 音频暂停时执行该函数
    #pause = (): void => this.#callback && this.#callback.onPlayStatus && this.#callback.onPlayStatus({ playStatus: false, msg: "暂停播放" })

    // 资源加载中(用于loading加载)【资源正在缓冲中】
    #waiting = (): void => this.#callback && this.#callback.onBuffer && this.#callback.onBuffer({ loading: true, msg: "音频缓冲中" })

    // 资源加载完成(用于loading加载)【资源缓冲完成】
    #canplay = (): void => this.#callback && this.#callback.onBuffer && this.#callback.onBuffer({ loading: false, msg: "音频缓冲完成" })

    // 播放速率发生改变时执行该函数
    #ratechange = (): void => this.#callback && this.#callback.onRatechange && this.#callback.onRatechange({ playbackRate: this.$audio.playbackRate })

    // 音量发生改变时执行该函数【静音和音量发生改变时都会触发,静音和音量是两个完全独立的状态】
    #volumechange = (): void => this.#callback && this.#callback.onVolumechange && this.#callback.onVolumechange({ volume: Math.ceil(this.$audio.volume * 100), isMute: this.$audio.muted })

    // 该音频播放结束执行
    #ended = (): void => this.#callback && this.#callback.onEnded && this.#callback.onEnded({
        // 循环播放
        loop: () => this.loop(),
        // 上一首
        last: () => this.last(),
        // 下一首
        next: () => this.next(),
        // 随机播放
        random: () => this.random(),
    })

    // 播放错误触发,可通过 audio.error 获取具体错误信息,常见错误:文件不存在(404)、格式不支持、跨域限制
    #error = (): void => {
        // 枚举错误状态码
        enum errorMsg{
            '未知错误' = 0,
            '音频加载被中止' = 1,
            '音频格式不支持' = 2,
            '音频加载异常(文件损坏或网络错误)' = 3,
            '音频资源不存在404' = 4,
        }
        // 约束状态码范围
        const errorCode = (this.$audio.error!.code in errorMsg) ? (this.$audio.error!.code as errorMsg) : 0;
        // 返回错误状态码信息
        this.#callback && this.#callback.onError && this.#callback.onError({ status: errorCode, errorMsg: errorMsg[errorCode] as string })
    }
}

// 暴露
export const audioHandle = new Audio();

使用方法

第一步先导入
import audioHandle from '路径'

// 因为在vue3中,真实dom存在延迟问题,获取不到audio元素,因此使用onMounted等待页面加载完成后再次调用
onMounted(() => {
    // 初始化
    audioHandle.init({
        // html元素(在vue3中,使用ref获取dom即可,原生直接使用[document.querySelector('')]获取即可.)
        audio: audio.value,    // 也可以使用 new Audio()
        // 音频地址链接的字段名 (可有可无,可有通过setSrcKey进行设置)
        srcKey: "url",
        // 保存当前音频列表 (可有可无)
        audioList: list,    // list必须为数组并包含对象的形式 如: [{name: "",url :""},{name: "",url :""},等等...]
        // 当前播放的音频链接  (可有可无)
        currentPlayAudio: list[1],
        // 方法  (可有可无)
        listeners: {
            // 初始化加载完成后触发该函数,返回当前音频的总时间,名称等参数  (每当音频路径发生改变时都会触发,并返回最新的音频数据等信息)
            onLoad(res) {
                console.log("音频加载时触发,总时间为:" + res.totalTime, res);
            },
            // 视频缓冲中/缓冲完成执行该函数(监听视频正在缓冲资源)
            onBuffer(res) {
                console.log(res);
            },
            // 播放进度改变时触发
            onTimeupdate(res) {
                console.log(res);
            },
            // 音频播放开始时触发(包括暂停后继续播放) 暂停时也会触发
            onPlayStatus(res){
                console.log(res);
            },
            // 播放倍数改变时触发
            onRatechange(res) {
                console.log(res);
            },
            // 音频音量发生改变时触发【静音和音量发生改变时都会触发,静音和音量是两个完全独立的状态】
            onVolumechange(res) {
                console.log(res);
            },
            // 当前音频播放结束时触发该函数
            onEnded(handle) {
                console.log("播放结束", handle);
            },
            // 播放错误触发,返回常见错误:文件不存在(404)、格式不支持、跨域限制
            onError(error) {
                console.log(error);
            }
        }
    });
});


// 播放方法
audioHandle.play();
// 暂停方法
audioHandle.pause();
// 修改音频进度方法 (单位秒/s)<Number>
audioHandle.step(40);
// 上一首音乐方法
audioHandle.last();
// 下一首音乐方法
audioHandle.next();
// 随机播放音乐
audioHandle.random();
// 循环播放音乐
audioHandle.loop();
// 静音方法
audioHandle.muted();
// 解除静音方法
audioHandle.relieveMuted();
// 设置音乐音量方法(0~100)<Number>
audioHandle.volume(20);
// 获取音频音量 直接返回number值,<Number>【1~100】
const volume = audioHandle.getVolume();
// 设置播放速率<Number> 0.5~4.0
audioHandle.setPlaybackRate(2);
// 获取播放倍率 直接返回number值,<Number>【0.5~4.0 范围】
const PlaybackRate = audioHandle.getPlaybackRate();
// 音频地址的字段名,用于读取对应的属性值
audioHandle.setSrcKey("src")

// 注销/销毁 指定的事件监听 【用于组件卸载或页面不存在时销毁监听,防止内存泄漏】
audioHandle.destroy(['onLoad', 'onTimeupdate', 'onPlayStatus', 'onBuffer', 'onRatechange', 'onVolumechange', 'onEnded']); // (每一个item皆为可选项)
//或全部销毁,可简写为
audioHandle.destroy();
// 清空【释放所有变量】
audioHandle.clear();


// 切换音乐方法
/**
* 切换音乐播放
* @param {*} currentPlayAudio 切换的音频地址 <Object>  如{name: "",url :""}
* @param {*} audioList 【可选】 音频集合 <Array | function>
 * 数组对象的形式 如: [{name: "",url :""},{name: "",url :""},等等...]
 * 函数形式 (res) => {
 *  return [...res, 新的音频配置项信息-如:{name: "",url :""}, 等等]
 * 注:函数形式必须有返回值,类型为Array<{[key: string]: any}>,可以操作当前音频列表集合
*/
// 数组形式
audioHandle.switch(currentPlayAudio【切换音频对象-必传】, [{name: "",url :""},{name: "",url :""},等等...])
// 函数形式
audioHandle.switch(currentPlayAudio【切换音频对象-必传】, (result) => console.log(result));

// 修改音频列表方法 (跟switch的第二个参数一模一样,且是必选项)
// 数组形式
audioHandle.setAudioList([{name: "",url :""},{name: "",url :""},等等...])
// 函数形式
audioHandle.setAudioList((result) => console.log(result));

// 监听audio事件
audioHandle.listeners({
  // 初始化加载完成后触发该函数,返回当前音频的总时间,名称等参数  (每当音频路径发生改变时都会触发,并返回最新的音频数据等信息)
  onLoad(res) {
    console.log("音频加载时触发,总时间为:" + res.totalTime, res);
  },
  // 视频缓冲中/缓冲完成执行该函数(监听视频正在缓冲资源)
  onBuffer(res) {
    console.log(res);
  },
  // 播放进度改变时触发
  onTimeupdate(res) {
    console.log(res);
  },
  // 音频播放开始时触发(包括暂停后继续播放) 暂停时也会触发
  onPlayStatus(res){
      console.log(res);
  },
  // 播放倍数改变时触发
  onRatechange(res) {
    console.log(res);
  },
  // 音频音量发生改变时触发【静音和音量发生改变时都会触发,静音和音量是两个完全独立的状态】
  onVolumechange(res) {
    console.log(res);
  },
  // 当前音频播放结束时触发该函数
  onEnded(handle) {
    console.log("播放结束", handle);
  },
  // 播放错误触发,返回常见错误:文件不存在(404)、格式不支持、跨域限制
  onEnded(error) {
    console.log(error);
  }
});

// 组件卸载之后执行
onUnmounted(() => {
  console.log("组件卸载");
  // 注销chang所有监听事件
  audioHandle.destroy();
  // 清除所有配置项以及属性
  audioHandle.clear();
});

注意事项:

/**
 * 原因?:
 * 哎,为啥要写这个呢,就是应为该死的【ios】,后台结束后不能继续播放,导致样式还是播放状态,audio对象的一些方法不能执行就寄了
 * 解决方法:监听visibilitychange,然后判断document.hidden的布尔值,可知晓用户是在前台还是后台【document.hidden是浏览器的专属属性,不是定义的】,当重新进入前台时恢复样式。
 * 当然,这取决于一个音频完整的播放结束,中途不需要判断调整。
 */
document.addEventListener('visibilitychange',()=> !document.hidden && 当前音频是否播放结束【ios在后台可以把当前音频播放完,但是不能继续播放下一首,必须要到在浏览器才可以】 && audioHandle.pause());

查看更多audio属性以及方法

--- 视频功能封装
class video {
    // 初始化
    constructor() {
        // 视频的html元素标签
        this.$video = null;
        // 视频地址链接的字段名
        this.$linkName = null;
        // 当前播放视频参数对象
        this.$currentVideo = null;
        // 播放视频集合(用于上集播放或下集播放)
        this.$videoList = [];
        // 视频的片头 (用于跳过当前视频的片头)<单位秒/s><number>
        this.$sliceHead = 0;
        // 恢复默认节流值
        this.currentTimeFlag = 0;
    }

    /** 初始化 */
    init(options) {
        // 音频字段名称未指向
        if (!options.hasOwnProperty("linkName")) return console.log(new Error("未指向字段名称,请先指向视频地址值的字段名称"));


        if (!options.current.hasOwnProperty(options.linkName)) return console.log(new Error("该字段不存在,请重新指向视频地址值的字段名称"));

        // 保存音频字段名
        this.$linkName = options.linkName;

        // 初始化路径配置项未指向
        if (!options.current[this.$linkName]) return console.log(new Error("未指向当前播放的路径,应初始化时指向初始化路径"));


        // 视频的html元素标签
        this.$video = options.videoHtml;
        // 视频地址链接的字段名
        this.$linkName = options.linkName;
        // 当前博凡视频参数对象
        this.$currentVideo = options.current;
        // 播放视频集合(用于上集播放或下集播放)
        this.$videoList = options.videoList;
        // 视频的片头 (用于跳过当前视频的片头)<单位秒/s><number>
        if (options.sliceHead) this.$sliceHead = options.sliceHead;
        // 回调函数
        options.change && this.change(options.change);

        // 把当前视频链接赋值给video标签
        this.$video.src = options.current[this.$linkName];

        this.$video.load()
    }

    // 回调函数
    change(callback) {

        // 当指定的音频/视频的元数据已加载时 (触发) 或音频路径切换时,必定会重新加载新的音频数据(也会触发)
        this.$video.addEventListener('loadedmetadata', () => {
            // 恢复默认节流值
            this.currentTimeFlag = 0;
            // 当前音加载时触发该函数
            callback && callback.onLoad && callback.onLoad({
                // 当前音频的总时间
                totalTime: this.$video.duration,
                // 当前播放时间
                currentTime: 0,
                // 视频源数据信息
                ...this.$currentVideo,
            })
        });

        // 监听视频播放进度
        this.$video.addEventListener("timeupdate", ({ target }) => {
            //  // 总时间
            let duration = parseInt(target.duration);
            //  // 当前播放进度的时间
            let currentTime = parseInt(target.currentTime);
            // 判断是否重复触发
            if (this.currentTimeFlag == currentTime) return;
            // 记录当前播放的时间 (防止重复触发)
            this.currentTimeFlag = currentTime;
            // 返回当前时间以及总时间
            callback && callback.onTimeupdate && callback.onTimeupdate({
                // 总时间
                totalTime: duration,
                // 当前播放的时间
                currentTime: currentTime,
            });
        });

        // 监听视频是否播放(play()和autoplay开始播放时触发)
        this.$video.addEventListener("play", () => {
            // console.log('视频已开始播放');
            callback && callback.onPlay && callback.onPlay()
        })

        // 监听视频是否暂停播放(视频暂停播放触发该函数)
        this.$video.addEventListener("pause", () => {
            callback && callback.onPause && callback.onPause()
        })

        // 资源加载中(用于loading加载)【资源正在缓冲中】
        this.$video.addEventListener("seeking", () => {
            // console.log('寻找中');
            callback && callback.onBuffer && callback.onBuffer({
                loading: true,
                msg: "视频缓冲中"
            })

        });
        // 资源加载完成(用于loading加载)【资源缓冲完成】
        this.$video.addEventListener("seeked", () => {
            // console.log('寻找完毕');
            callback && callback.onBuffer && callback.onBuffer({
                loading: false,
                msg: "视频缓冲完成"
            })
        });

        // 该音频播放结束执行
        this.$video.addEventListener('ended', () => {
            callback && callback.onEnded && callback.onEnded({
                // 循环播放
                loop: () => this.loop(),
                // 上一集
                last: () => this.last(),
                // 下一集
                next: () => this.next()
            });
        });
    }

    // 播放视频
    play() {
        this.$video.play()
    }

    // 暂停播放
    pause() {
        this.$video.pause()
    }

    /**
    * 设置音频音量
    * @param {*} num <Number> 0~100
    */
    volume(num) {
        // 设置音频音量
        this.$video.volume = Number(num / 100);
    }

    // 静音
    muted() {
        this.$video.muted = true;
    }

    // 解除静音
    relieveMuted() {
        this.$video.muted = false;
    }

    /**
    * 设置当前音频播放的进度
    * @param {*} num <Number> 单位秒
    */
    step(num) {
        this.$video.currentTime = Number(num);
    }

    // 小窗口播放
    smallWindow() {
        this.$video.requestPictureInPicture();
    }

    /** 全屏播放 */
    fullScreen() {
        let document = Document;
        //此方法不可以在異步任務中執行,否則火狐無法全屏
        if (this.$video.requestFullscreen) {
            this.$video.requestFullscreen();
        } else if (this.$video.mozRequestFullScreen) {
            this.$video.mozRequestFullScreen();
        } else if (this.$video.msRequestFullscreen) {
            this.$video.msRequestFullscreen();
        } else if (this.$video.oRequestFullscreen) {
            this.$video.oRequestFullscreen();
        } else if (this.$video.webkitRequestFullscreen) {
            this.$video.webkitRequestFullScreen();
        } else {
            var docHtml = document.documentElement;
            var docBody = document.body;
            var videobox = this.$video;
            var cssText = "width:100%;height:100%;overflow:hidden;";
            docHtml.style.cssText = cssText;
            docBody.style.cssText = cssText;
            videobox.style.cssText = cssText + ";" + "margin:0px;padding:0px;";
            document.IsFullScreen = true;
        }
    }

    /** 上一集 */
    last() {
        // 判断该音频集合是不是为一个链接
        if (this.$videoList.length <= 1) return;
        // 查找当前播放音频的索引
        let index = this.$videoList.findIndex((item) => item[this.$linkName] == this.$currentVideo[this.$linkName]);
        // 索引递减(为上一音频)
        index--;
        // 判断当前索引是否超过最小索引值
        if (index < 0) return (index == 0, console.log("本链接已是第一集"));
        // 保存当前音频对象信息
        this.$currentVideo = this.$videoList[index];
        // 赋值给video当前音频的路径
        this.$video.src = this.$videoList[index][this.$linkName];
        // 进行播放
        this.play();
    }

    /** 下一集 */
    next() {
        // 判断该音频集合是不是为一个链接
        if (this.$videoList.length <= 1) return
        // 查找当前播放音频的索引
        let index = this.$videoList.findIndex((item) => item[this.$linkName] == this.$currentVideo[this.$linkName]);
        // 索引递增 (为下一音频)
        index++;
        // 判断当前索引是否超过最大索引值
        if (this.$videoList.length - 1 < index) return (index = this.$videoList.length - 1, console.log("已完结"));
        // 保存当前音频对象信息
        this.$currentVideo = this.$videoList[index];
        // 赋值给video当前音频的路径
        this.$video.src = this.$videoList[index][this.$linkName];
        // 进行播放
        this.play();
    }

    // 循环播放
    loop() {
        // 恢复默认节流值
        this.currentTimeFlag = 0;

        // 播放
        this.play();
    }

    /**
    * 切换视频播放
    * @param {*} audioObj 切换的音频地址配置项 <Object>
    * @param {*} [urls] 音频集合 <Array>
    */
    switch(currentVideo, urls) {
        // 如果要切换的视频链接是当前播放的链接时,则直接退出,无须切换
        if (this.$currentVideo[this.$linkName] == currentVideo[this.$linkName]) return;

        // 保存当前音频对象信息
        this.$currentVideo = currentVideo;
        // 赋值给audio当前音频的路径
        this.$video.src = currentVideo[this.$linkName];
        // 进行播放
        this.play();

        // 切换音频可替换需要播放的音频集合 (有可进行替换)
        if (urls) this.$videoList = urls;
    }
}


// 暴漏vide Class类
export const videoHandle = new video();

使用方法

// 数据
let arr = [
  {
    name: "猫和老鼠第一集",
    url: ".......mp4",
  },
  {
    name: "猫和老鼠第二集",
    url: ".......mp4",
  },
  {
    name: "猫和老鼠第三集",
    url: ".......mp4",
  },
  {
    name: "猫和老鼠第四集",
    url: ".......mp4",
  },
];

// 获取video dom元素
const videoBox = ref(null);

onMounted(() => {
  // 初始化
  videoHandle.init({
    // 视频html元素
    videoHtml: videoBox.value,
    // 视频地址链接的字段名
    linkName: "url",
    // 视频列表
    videoList: arr,
    // 当前播放元素
    current: arr[0],
    // 视频的片头 (用于跳过当前视频的片头)<单位秒/s><number>
    sliceHead: 0,

    // 监听事件
    change: {
      // 当指定的音频/视频的元数据已加载时 (触发) 或音频路径切换时,必定会重新加载新的音频数据(也会触发)
      onLoad(res) {
        console.log(res);
      },
      // 视频播放中执行该函数(进度进度)
      onTimeupdate(res) {
        console.log(res);
      },
      // 视频缓冲中/缓冲完成执行该函数(监听视频正在缓冲资源)
      onBuffer(res) {
        console.log(res);
      },
      // 视频播放触发该函数
      onPlay() {
        console.log("视频已开始播放");
      },
      // 视频暂停播放触发该函数
      onPause() {
        console.log("视频暂停中");
      },
      // 播放结束
      onEnded(handle) {
        // 下一集
        handle.next();
      }
    }
  })
})

// 播放
const play = () => videoHandle.play();
// 暂停
const pause = () => videoHandle.pause();
// 修改进度套进度
const step = () => videoHandle.step(40);
// 上一集
const last = () => videoHandle.last();
// 下一集
const next = () => videoHandle.next();
// 小窗口播放
const smallWindow = () => videoHandle.smallWindow();
// 设置音量
const volume = () => videoHandle.volume(30);
// 静音
const muted = () => videoHandle.muted();
// 解除静音
const relieveMuted = () => videoHandle.relieveMuted();
// 全屏
const fullScreen = () => videoHandle.fullScreen();

// 视频切换
const switchTap = () => videoHandle.switch(arr[0]);
--- 控制台打印样式自定义
/**后台打印 */
    consoleLog(options: any) {
        // 标题
        let title = options.title.reduce((val, item) => val += '%c' + item, "");

        let style = options.style.reduce((val, item, i) => {
            let str = ''
            for (let item in options.style[i]) {
                str += `${item}: ${options.style[i][item]};`
            }
            return [...val, str]
        }, []);

        console.log(title, style[0] && style[0], style[1] && style[1]);
    }

使用方法

// 控制台打印
appInst.consoleLog({
  title: ["version", appInst.$version[0].version.slice(1)],
  style: [
    {
      padding: "3px",
      background: "#000",
      color: "white",
      "margin-bottom": "5px",
    },
    {
      padding: "3px",
      background: "#219ebc",
      color: "white",
    },
  ],
});

// 控制台打印
appInst.consoleLog({
  // title: ["LUKES版权所有", "窃取源码必承担法律责任,扒站/盗用者请自便"],
  title: ["LUKES版权所有", "小破站经不起大佬乱折腾,没经费乱搞"],
  style: [
    {
      "margin-top": "5px",
      padding: "3px",
      background: "#000",
      color: "rgb(248, 223, 154)",
    },
    {
      padding: "3px",
      background: "rgb(248, 223, 154)",
      color: "#666",
    },
  ],
});
--- 下载文件
/**
* 触发浏览器的下载
* @param {*} url 下载链接
* @param {*} name 文件名称
*/
downloadFiles(url: string, name = '') {
    const a = document.createElement("a");
    document.body.appendChild(a);
    a.style.display = "none";
    a.href = url;
    a.target = "_blank"; // 创建新窗口进行下载
    a.download = name;
    a.click();
    document.body.removeChild(a);
}
--- 判断是否为移动设备
/**
 * 精准检测移动设备(含平板/智能屏/折叠屏,适配iPad Pro/Nest Hub Max等特殊设备)
 * 核心逻辑:先识别特殊设备 → 排除明确桌面设备 → 匹配通用移动设备 → 触摸设备兜底
 * @returns {boolean} true=移动/平板/智能屏设备,false=桌面设备
 */
isMobileDevice(): boolean {
    const ua = navigator.userAgent.toLowerCase();
    const screenWidth = window.innerWidth;
    const screenHeight = window.innerHeight;
    // 1. 识别Nest Hub Max等智能屏设备
    const isSmartDisplay = /nest hub/.test(ua) || ('ontouchstart' in window && screenWidth <= 1280 && screenHeight <= 800 && /linux/.test(ua)); // 智能屏的尺寸+系统特征
    // 2. 识别iPad(覆盖所有型号)
    const isIPad = /ipad/.test(ua) || (/(macintosh|mac os)/.test(ua) && Math.min(screenWidth, screenHeight) >= 768 && Math.max(screenWidth, screenHeight) <= 1366);
    // 3. 排除桌面设备
    const isDesktop = (/windows|linux/.test(ua) && !isSmartDisplay && screenWidth > 1366) || (/mac os/.test(ua) && !isIPad && screenWidth > 1366);
    if (isDesktop) return false;
    // 4. 其他移动设备识别
    const isMobile = /iphone|ipod|android|mobile|touch|phone|blackberry|webos|iemobile|opera mini|symbian|kindle|firefox os/.test(ua);
    const isTouchDevice = 'ontouchstart' in window && Math.min(screenWidth, screenHeight) <= 1280;
    return isSmartDisplay || isIPad || isMobile || isTouchDevice;
}
--- 对象的模糊搜索
/**
 * 对象的模糊搜索 (最佳方法,性能最优)
 * @param options {
 * list: array<object>  查询的列表
 * search: string       查询的关键字
 * key: string          查询的字段名
 * }
 * @returns Array<object>  查询成功的数据列表
 */
function optimizedSearch(options: {list: Array<any>, search: string, key: string}): object {
  // 判断搜索的文件是否为空
  if (!options.search.trim()) return options.list;
  // toLowerCase() 方法用于把字符串转换为小写
  const searchLower = options.search.toLowerCase();
  // 定义一个新的数组,用户存放配置的数据
  const results = [];
  // 查找相匹配的数据
  for (let i = 0; i < options.list.length; i++) {
    options.list[i][options.key].toLowerCase().indexOf(searchLower) !== -1 ? results.push(options.list[i]) : null;
  }
  // 返回结果
  return results;
}
--- 数组转换为二维数组
/**
 * 转换为二维数组,方便直接转为map使用【最优,性能最好】
 * @param {Array} arr - 对象数组
 * @param {string} keyField - 作为键的字段名 把item列表的哪一属性值作为id,用于快速查找某一个对象
 */
twoDimensionalArray(arr: Array<object>, keyField: string){
    // 缓存 length
    const len = arr.length;
    // 提前分配数组
    const result = new Array(len);
    // 转换
    for (let i = 0; i < len; i++) {
      const item = arr[i] as any;
      result[i] = [item[keyField], item];
    }
    // 返回二维数组
    return result;
}
--- 查找并获取数组内某一个对象
/**
 * 数组转map 查找并获取数组内某一个对象 (性能最优)
 * @param {Array} arr - 对象数组
 * @param {string} keyField - 作为键的字段名 把item列表的哪一属性值作为id,用于快速查找某一个对象
 * @return Map对象
 */
arrayToNativeMap(arr: Array<object>, keyField: string) {
  const map = new Map();
  const len = arr.length;
  // 支持自定义键(比如组合键)
  const getKey = typeof keyField === "function" ? keyField : (item: any) => item[keyField];
  // 循环添加到map实例中
  for (let i = 0; i < len; i++) {
    const item = arr[i];
    const key = getKey(item);
    map.set(key, item);
  }
  return map;
}

使用示例:

// 示例使用
const data = [
  { id: 1, name: "张三" },
  { id: 2, name: "李四" },
  { id: 3, name: "王五" },
];

// 示例1:方法
const map1 = arrayToNativeMap(data, "id");
console.log(map1.get(2)); // { id: 2, name: "李四" }

// 示例2:自定义组合键
const map2 = arrayToNativeMap(data, (item) => `${item.id}_${item.name}`);
console.log(map2.get("2_李四")); // { id: 2, name: "李四" }
--- 筛选出b数组中存在的,且id值在a数组中的对象
/**
 * 筛选出当前用户的文件列表 (最优方案,性能最好)
 * @param {Object} parameter 参数对象
 * @param {Array} parameter.typeId 用户拥有的文件ID数组
 * @param {Array} parameter.dataSource 文件数据源数组
 * @param {string} parameter.dataSourceKey 文件数据源中对应ID的键名
 * @return {Array} 用户文件列表
 */
function filterType(parameter: { typeId: Array<string | number | undefined>, dataSource: Array<{ [key: string]: any }>,  dataSourceKey: string }) {
  const { typeId, dataSource, dataSourceKey } = parameter;
  // 使用Set去重
  const newSetUserObj = new Set(typeId);
  // 筛选出当前用户的文件列表
  return dataSource.filter((item) => newSetUserObj.has(item[dataSourceKey]));
}

使用示例:


// 用户拥有的文件ID数组
const idArr= [3, 7];

// 文件数据源数组
const dataArr = [
  { id: 1, name: '文件1' },
  { id: 2, name: '文件2' },
  { id: 3, name: '文件3' },
  { id: 4, name: '文件4' },   
  { id: 5, name: '文件5' },
  { id: 6, name: '文件6' },
  { id: 7, name: '文件7' },
];

const result = filterType({ typeId: idArr, dataSource: dataArr, dataSourceKey: 'id' });
console.log(result);  // [{ id: 3, name: '文件3' }, { id: 7, name: '文件7' }]
--- 圆形扩散主题
// 是否正在执行主题切换动画(防止连续点击)
let isTransitioning = false;

/**
 * 切换主题(带圆形扩散动画)
 * @param e 按钮点击事件
 */
export async function toggleTheme(e: MouseEvent) {
  // 动画未结束时,不允许再次点击
  if (isTransitioning) return;

  // html 元素
  const root = document.documentElement;

  // 当前主题
  const currentTheme = root.dataset.theme === "dark" ? "dark" : "light";

  // 切换后的主题
  const nextTheme = currentTheme === "dark" ? "light" : "dark";

  // 浏览器不支持 View Transition
  // 或用户开启了「减少动画」
  // 则直接切换主题
  if (!document.startViewTransition || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
    // 切换主题
    root.dataset.theme = nextTheme;
    // 根据主题切换favicon图标
    document.querySelector<HTMLLinkElement>("link[rel='icon']")!.href = `/favicon-${nextTheme}.ico`;
    // 保存当前主题
    localStorage.setItem("themeType", nextTheme);
    return;
  }

  isTransitioning = true;

  // 获取按钮位置(动画从按钮中心开始)
  const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();

  const x = rect.left + rect.width / 2;
  const y = rect.top + rect.height / 2;

  // 计算圆需要扩散到多大才能覆盖整个屏幕
  // Math.hypot = √(x²+y²)
  const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));

  // 开启 View Transition
  const transition = document.startViewTransition(() => {
    // 切换主题
    root.dataset.theme = nextTheme;
    // 根据主题切换favicon图标
    document.querySelector<HTMLLinkElement>("link[rel='icon']")!.href = `/favicon-${nextTheme}.ico`;
    // 保存当前主题
    localStorage.setItem("themeType", nextTheme);

    // 等待 Vue 更新 DOM
    // await nextTick();
  });

  // DOM 更新完成后开始动画
  transition.ready.then(() => {
    // 根据切换方向决定动画
    const clipPath =
      nextTheme === "dark"
        ? [
            // 亮 -> 暗:圆从小变大
            `circle(0px at ${x}px ${y}px)`,
            `circle(${endRadius}px at ${x}px ${y}px)`
          ]
        : [
            // 暗 -> 亮:圆从大变小
            `circle(${endRadius}px at ${x}px ${y}px)`,
            `circle(0px at ${x}px ${y}px)`
          ];

    const animation = document.documentElement.animate({ clipPath }, {
        // 动画时长(毫秒)
        duration: 450,

        // 缓动函数(Material Design 推荐)
        // easing: "cubic-bezier(0.4, 0, 0.2, 1)",
        easing: 'ease-in',

        // 立即应用第一帧的样式 不应用最后一帧的样式
        fill: "backwards", // 【这里切记不要修改】动画结束前确保不会闪

        // 指定动画作用到 View Transition 的哪个图层
        // 切到暗色:动画作用在新页面
        // 切到亮色:动画作用在旧页面
        pseudoElement: nextTheme === "dark" ? "::view-transition-new(root)" : "::view-transition-old(root)"
      });

     // ✅ 修复:动画结束前确保不会闪
    // 监听动画结束,在伪元素被移除前手动将 clip-path 设为最终状态
    // 作用:画播放完毕的那一刻,立即强制结束整个 View Transition 过渡,onfinish 负责"掐准时机",skipTransition() 负责"立刻清场",两者配合确保动画结束和伪元素销毁之间零间隔,消除闪烁
    animation.onfinish = () => transition.skipTransition(); //【解决闪烁以及帧数重叠问题】
  });

  // 动画结束,允许再次点击
  transition.finished.finally(() => {
    isTransitioning = false;
  });
};

css:

/* 去掉浏览器默认的淡入淡出动画 */
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

/* 亮 -> 暗 */
::view-transition-old(root) {
  z-index: 999;
}

::view-transition-new(root) {
  z-index: 1;
}

/* 暗 -> 亮 */
:root[data-theme="dark"]::view-transition-old(root) {
  z-index: 1;
}

:root[data-theme="dark"]::view-transition-new(root) {
  z-index: 999;
}

使用示例:

// 导入主题切换函数
import { toggleTheme } from '@/utils/circleThemeAnimation';

// 修改标题事件
const themeTap = (event: MouseEvent) => toggleTheme(event);// event为:事件对象
--- 阻止打开控制台并跳转空白页面
// 禁用右键菜单
document.addEventListener('contextmenu', function(e) {
    e.preventDefault(); // 阻止默认右键行为
});

// 禁用快捷键
document.addEventListener('keydown', function(e) {
    // F12键
    if (e.code === 'F12') {
        e.preventDefault();
    }
    // Ctrl+Shift+I
    if (e.ctrlKey && e.shiftKey && e.code === 'KeyI') {
        e.preventDefault();
    }
    // Ctrl+Shift+J (Chrome)
    if (e.ctrlKey && e.shiftKey && e.code === 'KeyJ') {
        e.preventDefault();
    }
    // Ctrl+U (查看网页源代码)
    if (e.ctrlKey && e.code === 'KeyU') {
        e.preventDefault();
    }
});

// 检测控制台打开并拦截
const checkConsole = () => {
    // 获取当前时间戳
    const start = Date.now();
    // 检测是否为移动端设备,如果是则不需要执行
    if(isMobileDevice()) return;
    // 控制台最小宽度/高度阈值
    const threshold = 160;
    // 判断控制台是否打开,如果打开则直接跳转空白页面
    (window.outerWidth - window.innerWidth > threshold || window.outerHeight - window.innerHeight > threshold) && window.location.replace('about:blank'); // 打开控制台立即跳转空白页面
    debugger;
    // 根据debugger前的时间戳判断是否是打开工作台【跳空白页和清除标签】
    Date.now() - start > 100 && (document.body.innerHTML = '', location.replace('about:blank'));
};
// 立即执行一次
checkConsole();
// 每500ms检测一次
setInterval(checkConsole, 500);

/**
 * 精准检测移动设备(含平板/智能屏/折叠屏,适配iPad Pro/Nest Hub Max等特殊设备)
 * 核心逻辑:先识别特殊设备 → 排除明确桌面设备 → 匹配通用移动设备 → 触摸设备兜底
 * @returns {boolean} true=移动/平板/智能屏设备,false=桌面设备
 */
function isMobileDevice() {
  const ua = navigator.userAgent.toLowerCase();
  const screenWidth = window.innerWidth;
  const screenHeight = window.innerHeight;

  // 1. 识别Nest Hub Max等智能屏设备
  const isSmartDisplay = /nest hub/.test(ua) || ('ontouchstart' in window && screenWidth <= 1280 && screenHeight <= 800 && /linux/.test(ua)); // 智能屏的尺寸+系统特征

  // 2. 识别iPad(覆盖所有型号)
  const isIPad = /ipad/.test(ua) || (/(macintosh|mac os)/.test(ua) && Math.min(screenWidth, screenHeight) >= 768 && Math.max(screenWidth, screenHeight) <= 1366);

  // 3. 排除桌面设备
  const isDesktop = (/windows|linux/.test(ua) && !isSmartDisplay && screenWidth > 1366) || (/mac os/.test(ua) && !isIPad && screenWidth > 1366);
  if (isDesktop) return false;

  // 4. 其他移动设备识别
  const isMobile = /iphone|ipod|android|mobile|touch|phone|blackberry|webos|iemobile|opera mini|symbian|kindle|firefox os/.test(ua);
  const isTouchDevice = 'ontouchstart' in window && Math.min(screenWidth, screenHeight) <= 1280;

  return isSmartDisplay || isIPad || isMobile || isTouchDevice;
}

--> 属性(JS)

--- 元素
/* 设置自定义属性 */
1. 元素.setAttribute((属性名), (属性值));

/* insertBefore ---> 把li插入到ul的ul.children[0]之前 */
2. ul.insertBefore(li, ul.children[0]);

3. document.documentElement.clientWidth  ---> 获取可视窗口的宽度
   document.documentElement.clientHeight ---> 获取可视窗口的高度
--- window
1. 获取滚动条滚动的距离
 1). document.documentElement.scrollTop 或 document.querySelector("div").scrollTop
 2). window.pageYoffset  ---> Y轴滚动条
 3). window.pageXoffset  ---> X轴滚动条
2. window.close() --- 关闭当前页面

--> 事件(鼠标键盘)

--- 鼠标
--- 核心鼠标事件类型
事件类型触发时机常用场景
点击类  
dblclick鼠标左键双击触发双击编辑、文件打开
mousedown鼠标任意按键按下时触发(左 / 中 / 右)拖拽开始、按住操作
mouseup鼠标任意按键松开时触发(左 / 中 / 右)拖拽结束、释放操作
contextmenu鼠标右键单击(触发上下文菜单)自定义右键菜单
移动类  
mousemove鼠标在元素内移动时持续触发拖拽跟踪、鼠标位置监听
mouseover鼠标移入元素(含子元素)时触发悬浮提示(会冒泡)
mouseenter鼠标移入元素(不含子元素)时触发悬浮样式(不冒泡,推荐)
mouseout鼠标移出元素(含子元素)时触发悬浮恢复(会冒泡)
mouseleave鼠标移出元素(不含子元素)时触发悬浮恢复(不冒泡,推荐)
滚动类  
wheel鼠标滚轮滚动时触发(上下 / 左右)自定义滚动逻辑、防滚动穿透
其他  
drag元素被拖拽时持续触发拖拽过程跟踪
dragstart元素开始被拖拽时触发拖拽初始化
dragend元素拖拽结束时触发拖拽收尾
drop拖拽元素释放到目标元素时触发拖拽放置逻辑

🔑 关键区别:mouseenter/mouseleave 不冒泡,mouseover/mouseout 会冒泡(子元素触发会传递到父元素),因此做悬浮交互时优先用 mouseenter/mouseleave。

--- 鼠标事件对象核心属性
属性作用
e.clientX/e.clientY鼠标相对于可视窗口的坐标(不含滚动条偏移)
e.pageX/e.pageY鼠标相对于文档的坐标(含滚动条偏移,常用)
e.screenX/e.screenY鼠标相对于电脑屏幕的坐标
e.offsetX/e.offsetY鼠标相对于触发元素的内部坐标(元素左上角为 0)
e.button按下的鼠标按键:0 = 左键,1 = 中键,2 = 右键(mousedown/mouseup 有效)
e.ctrlKey/e.shiftKey/e.altKey按下按键时是否按住 Ctrl/Shift/Alt 键(组合操作)
e.target触发事件的原始元素(事件冒泡时不变)
e.currentTarget绑定事件的当前元素(事件冒泡时为当前处理元素)
e.preventDefault()阻止默认行为(如右键菜单、链接跳转)
e.stopPropagation()阻止事件冒泡(如子元素点击不触发父元素事件)
--- 示例
  1. 获取鼠标位置
    <div id="box" style="width: 300px; height: 200px; border: 1px solid #000;"></div>
    <p id="pos"></p>
    <script>
      const box = document.getElementById('box');
      const pos = document.getElementById('pos');
      
      box.addEventListener('mousemove', (e) => {
        pos.innerHTML = `
          相对于文档:${e.pageX}, ${e.pageY}<br>
          相对于元素:${e.offsetX}, ${e.offsetY}<br>
          相对于窗口:${e.clientX}, ${e.clientY}
        `;
      });
    </script>
    
  2. 自定义右键菜单
    <div id="customMenu" style="display: none; position: absolute; width: 120px; background: #fff; border: 1px solid #ccc; padding: 5px;">
      <div>复制</div>
      <div>粘贴</div>
      <div>删除</div>
    </div>
    <script>
      const menu = document.getElementById('customMenu');
      
      // 禁用默认右键菜单
      document.addEventListener('contextmenu', (e) => {
        e.preventDefault();
        // 定位自定义菜单到鼠标位置
        menu.style.left = `${e.pageX}px`;
        menu.style.top = `${e.pageY}px`;
        menu.style.display = 'block';
      });
      
      // 点击空白处关闭菜单
      document.addEventListener('click', () => {
        menu.style.display = 'none';
      });
    </script>
    
  3. 鼠标拖拽元素

    核心:mousedown 记录初始位置 → mousemove 计算偏移 → mouseup 停止监听:

    <div id="dragBox" style="width: 100px; height: 100px; background: red; position: absolute; cursor: move;"></div>
    <script>
      const dragBox = document.getElementById('dragBox');
      let isDragging = false;
      let startX, startY;
    
      // 按下鼠标:开始拖拽
      dragBox.addEventListener('mousedown', (e) => {
        isDragging = true;
        // 记录鼠标相对于元素的初始位置
        startX = e.offsetX;
        startY = e.offsetY;
        dragBox.style.opacity = '0.8'; // 视觉反馈
      });
    
      // 移动鼠标:更新位置(绑定到document,避免鼠标移出元素中断)
      document.addEventListener('mousemove', (e) => {
        if (!isDragging) return;
        // 计算元素新位置:鼠标文档坐标 - 初始偏移
        const x = e.pageX - startX;
        const y = e.pageY - startY;
        dragBox.style.left = `${x}px`;
        dragBox.style.top = `${y}px`;
      });
    
      // 松开鼠标:结束拖拽
      document.addEventListener('mouseup', () => {
        if (isDragging) {
          isDragging = false;
          dragBox.style.opacity = '1';
        }
      });
    </script>
    
  4. 监听鼠标滚轮方向
    /* 鼠标滚轮事件 */
    window.onwheel = (e) => {
        if(e.wheelDeltaY > 0){
            console.log("往上滚动");
        } else {
            console.log("往下滚动");
        }
    }
    
    --------- 或 ---------
    
    document.addEventListener('wheel', (e) => {
      // e.deltaY > 0 向下滚动,< 0 向上滚动
      if (e.deltaY > 0) {
        console.log('滚轮向下');
      } else {
        console.log('滚轮向上');
      }
      // 阻止默认滚动(如页面不跟随滚轮滚动)
      // e.preventDefault();
    });
    
  5. 区分左键 / 右键 / 中键点击
    document.addEventListener('mousedown', (e) => {
      switch(e.button) {
        case 0:
          console.log('左键按下');
          break;
        case 1:
          console.log('中键按下(滚轮点击)');
          break;
        case 2:
          console.log('右键按下');
          break;
      }
    });
    
--- 注意事项
  1. 事件冒泡:click、mousedown 等事件会冒泡到父元素,如需阻止可调用 e.stopPropagation()。

    示例:子元素点击不触发父元素事件

    const parent = document.getElementById('parent');
    const child = document.getElementById('child');
    parent.addEventListener('click', () => console.log('父元素点击'));
    child.addEventListener('click', (e) => {
      e.stopPropagation(); // 阻止冒泡
      console.log('子元素点击');
    });
    
  2. 拖拽事件的兼容性:原生拖拽(drag/drop)需给元素设置 draggable="true",且 drop 事件触发前需阻止 dragover 的默认行为:
    const dropTarget = document.getElementById('dropTarget');
    dropTarget.addEventListener('dragover', (e) => {
      e.preventDefault(); // 必须阻止默认行为,否则drop不触发
    });
    dropTarget.addEventListener('drop', (e) => {
      console.log('元素拖入目标');
    });
    
  3. 鼠标位置的兼容性:pageX/pageY 在 IE 低版本中需兼容(可通过 clientX + document.documentElement.scrollLeft 计算)。
    let timer = null;
    document.addEventListener('mousemove', (e) => {
      if (timer) clearTimeout(timer);
      timer = setTimeout(() => {
        // 执行复杂逻辑(如更新DOM)
        console.log('鼠标位置:', e.pageX, e.pageY);
      }, 16); // 约60帧/秒
    });
    
  4. 性能优化:mousemove 触发频率极高,如需复杂逻辑,可通过「节流」减少触发次数:
    let timer = null;
    document.addEventListener('mousemove', (e) => {
      if (timer) clearTimeout(timer);
      timer = setTimeout(() => {
        // 执行复杂逻辑(如更新DOM)
        console.log('鼠标位置:', e.pageX, e.pageY);
      }, 16); // 约60帧/秒
    });
    
--- 键盘
// 方式1:addEventListener(推荐,支持多事件绑定)
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);

// 方式2:直接赋值(仅能绑定一个事件)
// document.onkeydown = handleKeyDown;

// 事件处理函数
function handleKeyDown(e) {
  console.log('按键按下:', {
    key: e.key,        // 按键名称(如 'a'、'Enter'、'ArrowUp')
    code: e.code,      // 按键编码(如 'KeyA'、'Enter'、'ArrowUp')
    keyCode: e.keyCode,// 按键ASCII码(已废弃,如 65 对应 A,13 对应 Enter)
    altKey: e.altKey,  // 是否按住 Alt
    ctrlKey: e.ctrlKey,// 是否按住 Ctrl
    shiftKey: e.shiftKey,// 是否按住 Shift
    metaKey: e.metaKey // 是否按住 Windows/Command 键
  });

  // 阻止默认行为(如禁用回车提交表单、禁用方向键滚动)
  // e.preventDefault();
}

function handleKeyUp(e) {
  console.log('按键松开:', e.key);
}

// 移除事件监听(需保留函数引用)
// document.removeEventListener('keydown', handleKeyDown);
--- 表单
监听滚动条移动事件 ---> scroll
获取焦点 ---> focus
失去焦点 ---> blur
监听表单值发生变化 ---> change

--> 获取文件/文件结构(封装集合)

--- 获取文件示例
<template>
<input type="file" ref="InputDom" accept="image/jpeg, image/png, image/gif, image/jpg, image/gif, image/webp" @change="monitorFile($event)" style="display: none;">
</template>

<script>
import { ref } from 'vue';

// 获取dom元素
const InputDom = ref<HTMLInputElement>();

// 选择文件后接收文件
const monitorFile = (e: Event) => {
  const el = e.target as HTMLInputElement;
  console.log(el.files);
}
</script>

注意事项:

/**
** 如果是拖动某一些文件到特定区域,如:文件上传,拖动文件到div上;
** 一定要阻止【**默认行为**】,要不然浏览器会跳转新窗口打开文件;
** 拖动到div的文件,使用事件对象获取,从 dataTransfer 中获取文件列表【e.dataTransfer.files】
**/

@dragover.prevent 和 @drop.prevent="monitorFile($event)" // 重要
--- input选择文件属性
1.选择多个文件【不写就是默认选择一个】:
multiple="true" 属性 【或】 multiple="multiple" 属性  ==> 开启选择多个文件

2.选择文件夹: webkitdirectory  mozddirectory oddirectory 兼容属性(个必填)只能选择文件夹

3. 限制选择文件属性accept
	一. 【使用 MIME 类型(推荐)】:
	1). 所有图片:accept="image/*";
		特定图片格式:accept="image/jpeg, image/png, image/gif"
	2). 所有视频:accept="video/*"
		特定视频格式:accept="video/mp4, video/webm"
	3). 所有音频:accept="audio/*";
	4). PDF 文档:accept="application/pdf";
	二. 【使用文件扩展名】:
	1). 限制特定后缀:accept=".jpg, .png, .pdf"
	2). 混合使用:accept=".doc, .docx, application/pdf"
	三. 【混合使用】
	1). accept="image/*, .pdf, application/msword" (接受所有图片、PDF文件以及旧版Word文档)
--- 获取移动到元素上的的方法
<html元素>.ondragstart => (e.preventDefault()) 开始拖动元素时触发
<html元素>.ondrag => (e.preventDefault()) 拖动过程中持续触发
<html元素>.ondragend => (e.preventDefault()) 拖动结束时触发(无论成功失败)
<html元素>.ondragenter => (e.preventDefault()) 文件移进后触发
<html元素>.ondragover => (e.preventDefault()) 文件移动触发
<html元素>.ondragleave => (e.preventDefault()) 离开目标元素时触发【离开移动区域】
<html元素>.ondrop => (e.preventDefault()) 文件放下后触发
--- 获取数据结构
// 获取数据结构 (移动到div上)
export const getFiles = (res) => {
    return new Promise(async (response) => {
        response(await getFile(res));
        async function getFile(entry: any) {
            // console.log(entry.name);
            if (entry.name === "node_modules" || entry.name === "miniprogram_npm")
                return {
                    // 文件
                    type: "files",
                    // 开关节流阀
                    isFLag: false,
                    // 后缀名
                    typeName: 'files',
                    // 文件名
                    name: entry.name,
                    // 种类, 性质, 方式
                    fullPath: entry.fullPath,
                    // file文件
                    file: [],
                };

            if (entry.isFile) {
                // 表示是一个文件
                return {
                    // 文件
                    type: "file",
                    // 开关节流阀
                    isFLag: false,
                    // 后缀名
                    typeName: entry.name.split(".").pop().toLocaleLowerCase(),
                    // 文件名
                    name: entry.name,
                    // 种类, 性质, 方式
                    fullPath: entry.fullPath,
                    // file文件
                    file: await callhander(entry),
                };
            } else {
                // 表示是一个文件夹列表
                let files: any = await hander(entry);
                return {
                    // file文件
                    files,
                    // 文件
                    type: "files",
                    // 开关节流阀
                    isFLag: false,
                    // 后缀名
                    typeName: 'files',
                    // 文件名
                    name: entry.name,
                    // 种类, 性质, 方式
                    fullPath: entry.fullPath,
                    // 提示信息
                    msg: files.length ? "many file" : `“${entry.name}”为空`,
                };
            }
        }

        // 创建异步迭代器
        function hander(entry) {
            return new Promise((res) => {
                let arr = [];
                // 表示是一个文件夹列表
                const reader = entry.createReader();
                reader.readEntries(async (en) => {
                    if (!en.length) res(arr);
                    for (const el of en) {
                        arr.push(await getFile(el));
                        if (arr.length == en.length) {
                            res(arr);
                        }
                    }
                });
            });
        }
    });

    // item为遍历每一项的FileEntry对象,调用 原型上的 file 方法使其转换为file对象 通过回调函数的方法返给调用者
    function callhander(item: any) {
        return new Promise((res) => {
            // 把转换的结构暴露出
            item.file((r: any): void => res(r));
        });
    }
}
--- 点击上传按钮(提取所有文件)
// 点击上传按钮(提取所有文件)
export const getHander = (target: any) => {
    return new Promise(response => {
        response({
            // 文件名称
            name: target[0].webkitRelativePath.split("/")[0],
            // 默认是否打开
            isFLag: false,
            // 提示信息
            msg: "many file",
            // 文件类型
            type: "files",
            // 图标类型
            typeName: "files",
            // 文件集合
            files: [...target].reduce((val, item) => {
                return [
                    ...val,
                    {
                        // 文件名称
                        name: item.name,
                        // 默认是否打开
                        isFLag: false,
                        // 提示信息
                        msg: "ok",
                        // 文件类型
                        type: "file",
                        // 图标类型
                        typeName: item.name.split(".").pop(),
                        // file文件
                        file: item,
                    },
                ];
            }, []),
        })
    });
}
--- 读取器读取的文件目录
 // 打开文件内容
let res: any = await window.showDirectoryPicker();
// 获取文件内容
const root: any = await getFolder(res);
console.log(root);




// 获取文件结构
function getFolder(res) {
  return new Promise((response) => {
    response(fn(res));

    async function fn(handle: any) {
      if (handle.kind === "file") {
        return {
          // 文件
          type: "file",
          // 开关节流阀
          isFLag: false,
          // 后缀名
          typeName: handle.name.split(".").pop().toLocaleLowerCase(),
          // 文件名
          name: handle.name,
          // 句柄
          handle,
          // 种类, 性质, 方式
          kind: handle.kind,
        };
      }

      // 添加判断node_modules模块,直接返回该模块文件夹
      if (handle.name === "node_modules" || handle.name === "miniprogram_npm") {
        return {
          // 文件
          type: "files",
          // 开关节流阀
          isFLag: false,
          // 后缀名
          typeName: "files",
          // 文件名
          name: handle.name,
          // 种类, 性质, 方式
          kind: handle.kind,
          // 文件列表
          files: handle.files,
          // 句柄
          handle,
        };
      }

      handle.files = [];
      // 得到异步迭代器
      const inter = handle.entries();
      for await (const item of inter) {
        handle.files.push(await getFolder(item[1]));
      }
      return {
        // 文件
        type: "files",
        // 开关节流阀
        isFLag: false,
        // 后缀名
        typeName: "files",
        // 文件名
        name: handle.name,
        // 种类, 性质, 方式
        kind: handle.kind,
        // 文件列表
        files: handle.files,
        // 句柄
        handle,
      };
    }
  });
}
--- 创建读取器
// 创建读取对象
const reader = new FileReader();

// 音频读取
reader.readAsDataURL('音频file文件');
reader.onload = (e) => {
  console.log({
    status: 200,
    msg: `读取成功`,
    type: "img",
    data: {
      src: e.target.result,
    },
  })
};

// 读取文本
// 文本自动读取
reader.readAsText('文件发file文件', "UTF-8");
reader.onload = (e) => {
  // 默认配置
  console.log({
    status: 200,
    msg: `读取成功`,
    type "text",
    data: {
      e.target.result
    },
  })
};

--> video

--- 获取video链接/可以本地预览(方法封装)
export const file = (videoFile: object | any) => {
    return new Promise((reponse) => {
        // 获取选择的file文件
        // 兼容设置
        window.URL = window.URL || window.webkitURL;
        // 创建本地路径的映射
        const url = window.URL.createObjectURL(videoFile);

        // 创建Audio对象
        const audioElement = new Audio(url);

        // 当视频准备完毕后触发该事件
        audioElement.addEventListener("loadedmetadata", () => {
            // 视频大小
            const size = `${(videoFile.size / 1024 / 1024).toFixed(1)}MB`;
            // 视频时长
            const duration = audioElement.duration;
            // 视频名称
            const name = videoFile.name;
            reponse({
                status: 200,
                msg: "读取成功",
                type: "video",
                data: {
                    // 视频大小
                    size,
                    // 视频时长
                    duration,
                    // 视频地址
                    url,
                    // 视频名称
                    name,
                },
            });
        });
    });
};

// 使用 (视频实例 对象)
 await file(e.target.files[0]);
--- 进入全屏 (全屏播放方法)
/**
 * 进入全屏 (全屏播放方法)
 * @param element html元素 <HTMLElement>
 */
export const QP = (element: HTMLElement | any): void => {
    let document = <HTMLElement | any>Document
    //此方法不可以在異步任務中執行,否則火狐無法全屏
    if (element.requestFullscreen) {
        element.requestFullscreen();
    } else if (element.mozRequestFullScreen) {
        element.mozRequestFullScreen();
    } else if (element.msRequestFullscreen) {
        element.msRequestFullscreen();
    } else if (element.oRequestFullscreen) {
        element.oRequestFullscreen();
    } else if (element.webkitRequestFullscreen) {
        element.webkitRequestFullScreen();
    } else {
        var docHtml = document.documentElement;
        var docBody = document.body;
        var videobox = element;
        var cssText = "width:100%;height:100%;overflow:hidden;";
        docHtml.style.cssText = cssText;
        docBody.style.cssText = cssText;
        videobox.style.cssText = cssText + ";" + "margin:0px;padding:0px;";
        document.IsFullScreen = true;
    }
};
--- 退出全屏 (退出全屏播放方法)
/**
 * 退出全屏 (退出全屏播放方法)
 * @param element html元素 <HTMLElement>
 */
export const EX = (element: HTMLElement | any): void => {
    let document = <HTMLElement | any>Document
    if (document.exitFullscreen) {
        document.exitFullscreen();
    } else if (document?.msExitFullscreen) {
        document.msExitFullscreen();
    } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen();
    } else if (document.oRequestFullscreen) {
        document.oCancelFullScreen();
    } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen();
    } else {
        var docHtml = document.documentElement;
        var docBody = document.body;
        var videobox = element;
        docHtml.style.cssText = "";
        docBody.style.cssText = "";
        videobox.style.cssText = "";
        document.IsFullScreen = false;
    }
};
--- 视频下载
/**
 * 视频下载
 * @param vidoUrl 目标文件地址
 */
export const downLoadVideo = (url: string): void => {
    fetch(url)
        .then(async (res) => {
            return {
                name: res.url,
                blob: await res.blob()
            }
        })
        .then(({ blob, name }) => {
            let arr = name.split("/");
            const a = document.createElement("a");
            document.body.appendChild(a);
            a.style.display = "none";
            const url = window.URL.createObjectURL(blob);
            a.href = url;
            // a.download = "视频.mp4";
            a.download = arr[arr.length - 1];
            a.click();
            document.body.removeChild(a);
            window.URL.revokeObjectURL(url);
        });
}

CSS通用样式

--- 第三方字体图标优化方案

/* Chrome 检测到字体加载过慢,为避免文字长时间不显示,使用了备用字体 */
/* 使用 font-display: swap; 表示:先使用备用字体渲染,等自定义字体加载好了再替换。这样可以避免文字长时间不显示*/

/* 例如: */
@font-face {
  font-family: 'IconFont';
  src: url('http://192.168.31.254:9999/src/fonts/iconfont.woff2?t=1764658108988') format('woff2');
   ......
  font-display: swap; /* 推荐 */
}

-->文字操作系列

--- 文字换行
// 文职换行,中英文通用.
white-space: normal;	// 文字能自动换行
word-break: break-all;	// 防止文字被挤压
overflow-wrap: break-word; /* 现代标准 */
word-wrap: break-word;	// 允许英文单词内换行(区别于 break-all):优先在单词间隙换行,仅单词超长时才在单词内换行,兼顾中英文排版
--- 文字不允许换行/超出'...'代替
overflow: hidden;
text-overflow: ellipsis; // 溢出部分显示省略号(仅在 -webkit-line-clamp 生效时显示)
-webkit-line-clamp: 2;  // 文字超出两行...
line-clamp: 2;
display: -webkit-box;	// 将元素转为弹性盒模型,配合 -webkit-line-clamp 生效
-webkit-box-orient: vertical;	// 设置弹性盒的排列方向为垂直,让文字按行排布
word-break: break-word;  // 兼容英文长词
overflow-wrap: break-word; // 标准语法

-->滚动条

--- 全局自定义滚动条样式
:root{
    --lukes-scrollbar-width: 8px;
}
/* 1. WebKit 浏览器(Chrome/Edge/Safari) */
/* 1. 滚动条整体样式(宽/高) */

::-webkit-scrollbar {
    width: var(--lukes-scrollbar-width);    /* 垂直滚动条宽度 */
    height: var(--lukes-scrollbar-width);   /* 水平滚动条高度 */
}

/* 2. 滚动条轨道(背景、圆角) */
::-webkit-scrollbar-track {
    /* 轨道背景色 */
    background: rgba(0,0,0,0);  
}

/* 3. 滚动条滑块(核心样式) */
::-webkit-scrollbar-thumb {
    /* 滑块默认颜色 */
    background-color: rgba(144, 147, 153, 0.3);  
    cursor: pointer;
    /* 上下左边框保留,营造悬浮感 */
    border-right: 2px solid #1c0e36;
    /* 滑块圆角(避免尖锐) */
    border-radius: calc(var(--lukes-scrollbar-width) / 2);  
}

/* 4. 滑块 hover 状态 */
::-webkit-scrollbar-thumb:hover {
    background-color: rgba(144, 147, 153, 0.5);  /* hover 时加深颜色 */
}

/* 5. 滑块激活(点击拖动)状态 */
::-webkit-scrollbar-thumb:active {
    /* background: #888;     激活时更深颜色 */
}

/* 可选:滚动条交汇处样式 */
::-webkit-scrollbar-corner {
    /* background: #f1f1f1;
    border-radius: 6px; */
}

/* 2. Firefox */
.container {
    /* scrollbar-width: 6px; */
    /* 滑块颜色 + 轨道颜色 */
    /* scrollbar-color: rgba(144, 147, 153, 0.3) rgba(0,0,0,0); */
}
--- 局部容器自定义滚动条样式
/* 1. container-scroll为容器的类名 */
.container-scroll{
    /* 1. WebKit 浏览器(Chrome/Edge/Safari) */
    /* 1. 滚动条整体样式(宽/高) */

    --lukes-container-scroll-width: 6px;

    &::-webkit-scrollbar {
        width: var(--lukes-container-scroll-width);    /* 垂直滚动条宽度 */
        height: var(--lukes-container-scroll-width);   /* 水平滚动条高度 */
    }

    /* 2. 滚动条轨道(背景、圆角) */
    &::-webkit-scrollbar-track {
        /* 轨道背景色 */
        background: transparent;  
    }

    /* 3. 滚动条滑块(核心样式) */
    &::-webkit-scrollbar-thumb {
        /* 滑块默认颜色 */
        background-color: rgba(144, 147, 153, 0.3);  
        cursor: pointer;
        /* 上下左边框保留,营造悬浮感 */
        border-right: 2px solid transparent;
        /* 滑块圆角(避免尖锐) */
        border-radius: calc(var(--lukes-container-scroll-width) / 2);  
    }

    /* 4. 滑块 hover 状态 */
    &::-webkit-scrollbar-thumb:hover {
        background-color: rgba(144, 147, 153, 0.5);  /* hover 时加深颜色 */
    }

    /* 5. 滑块激活(点击拖动)状态 */
    &::-webkit-scrollbar-thumb:active {
        /* background: #888;     激活时更深颜色 */
    }

    /* 可选:滚动条交汇处样式 */
    &::-webkit-scrollbar-corner {
        /* background: #f1f1f1;
        border-radius: 6px; */
    }
}

/* 在屏幕宽度小于等于 768px 时应用的样式 */
@media screen and (max-width: 768px) {
    .container-scroll{
        --lukes-container-scroll-width: 3px;
    }
}
--- 局部容器保存滚动功能隐藏滚动条
/** --------------------- 自定义容器隐藏滚动条 --------------------- **/
.container-scroll-none{
    // 只y轴滚动
    overflow-y: scroll;
    /* 容器级别的隐藏(兼容多浏览器) */
    -ms-overflow-style: none;  /* IE/Edge */
    scrollbar-width: none;     /* Firefox */

    &::-webkit-scrollbar {
        display: none; /* Chrome/Edge/Safari 容器滚动条隐藏 */
    }
}

--- 内部变化不影响外部

// 优化容器
.contain-container{
  // 性能优化:内部变化不影响外部 (主要处理【x轴滚】动条超出,顶部导航采用【粘性定位】不能使用【overflow-x: hidden】来隐藏x轴滚动条,所以用contain解决)
   /* 等同于告诉浏览器:
     1. 这个元素内部怎么布局都不会影响外面
     2. 这个元素只在自己的框里画东西,不会"污染"外部
  */
   //layout: 隔离元素的内部布局,使其与页面其余部分互不影响【使用 contain: layout 后,浏览器只需检查该元素内部。这意味着你可以放心地在这个元素内部增删节点或改变布局,而不会引发页面其他区域的重新计算。】
   //paint: 将元素的绘制严格限制在其边界内,防止内容溢出。【如果该元素位于屏幕可视区域之外,浏览器会直接跳过对其子元素的绘制,从而节省渲染资源,加快其他可见内容的渲染速度】
  contain: layout paint;
  min-height: 100vh;
}

/* dvh兼容则使用下面的样式 */
@supports (height: 100dvh) {
  // 优化容器
  .contain-container{
    min-height: 100dvh;
  }
}

--- 解决ios点击【按钮、输入框...】高亮、抖动问题

/** ============== 解决ios点击时,抖动问题 ================ */
/* 允许浏览器优化触摸事件,避免滑动被阻塞 */
touch-action: manipulation;
/* 禁用 Safari 原生的触摸高亮 */
-webkit-tap-highlight-color: transparent;
/** ============== 解决ios点击时,抖动问题 ================ */

--- ios系列问题

/* 禁止缩放/滚动,但部分场景下地址栏仍可被边缘手势触发
* none: 禁止所有默认触摸行为,所有触摸事件交给 JS
* pan-y: 允许垂直滚动
* pan-x: 允许水平滚动
*/
touch-action: none;


/* 防止iOS “滚动链”穿透防止用户在弹窗、侧边栏或局部滚动区域内滚动到底部时,意外带动整个页面滚动;
* contain:防止滚动穿透到body,但不禁止回弹效果。
* none:完全禁止
*/
overscroll-behavior: contain;

/* 彻底禁用垂直方向的过滚动反馈和滚动链 */
overscroll-behavior-y: none; 

/* 启用 iOS 惯性滚动 */
-webkit-overflow-scrolling: touch; 

--- 使用媒体查询@media

/** ==============  基础用法  ================ */
/* 当屏幕宽度大于等于 768px 时生效(大屏设备) */
@media (min-width: 768px) {
  .container { display: flex; }
}

/* 当屏幕宽度小于等于 600px 时生效(小屏设备) */
@media (max-width: 600px) {
  body { background-color: lightblue; }
}


/** ==============  针对不同的媒体类型  ================ */
/* 专门针对打印机的样式 */
@media print {
  body { color: black; background: white; }
}

/* 专门针对屏幕阅读器的样式 */
@media speech {
  /* 语音朗读相关样式 */
}


/** ==============  组合多个条件(逻辑运算符) ================ */
/* 【and】屏幕宽度在 400px 到 700px 之间,且处于横屏状态 */
@media screen and (min-width: 400px) and (max-width: 700px) and (orientation: landscape) {
  .box { background-color: red; }
}

/* 【逗号 / or】屏幕处于竖屏,或者宽度小于 3rem */
@media (orientation: portrait), (max-width: 3rem) {
  .box { background-color: blue; }
}

/* 【not】取反:排除不支持悬停操作的设备(如触摸屏) */
@media not screen and (hover: hover) {
  /* 针对触摸设备的特殊样式 */
}


/** ==============  针对设备特定属性  ================ */
/* 针对视网膜(高分辨率)屏幕 */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
  .icon { background-image: url('icon@2x.png'); }
}


/** ==============  进阶写法:Media Queries Level 4 新语法  ================ */
/* 传统写法 */
@media (min-width: 400px) and (max-width: 700px) { ... }

/* Level 4 更清晰的写法 */
@media (400px <= width <= 700px) { ... }


/** ==============  HTML 标签中直接使用  ================ */
<!-- 只有当屏幕宽度大于 900px 时,才会应用这个外部样式表 -->
<link rel="stylesheet" media="screen and (min-width: 900px)" href="widescreen.css">

--- 移动/电脑端【是否支持触摸】元素样式兼容性处理

@media (any-hover: hover)
// 这是一个媒体查询,用来检测设备是否支持原生的悬停能力(如桌面端的鼠标)。
any-hover: hover:表示设备有支持悬停的输入方式(如鼠标)。
any-hover: none:表示设备没有支持悬停的输入方式(如手机触摸屏)。
input:hover { cursor: pointer; }
只有在满足上面的媒体查询条件时,这段样式才会生效。
当鼠标悬停在 <input> 元素上时,鼠标指针会变成手型(pointer),提示用户这是一个可交互的元素。

***** 例如:*****
// 备有支持悬停的输入方式(不是触摸屏)
@media (any-hover: hover) { }
// 设备不支持悬停的输入方式(是触摸屏)
@media (any-hover: none){ }

--- 检测css属性是否支持【降级处理方案】

/** ============== 浏览器是否支持某个特定的 CSS 属性或属性值 ================ */

@supports (属性名: 属性值) {
  /* 只有当浏览器支持该属性值时,这里的 CSS 规则才会生效 */
}

/* 同时支持两个特性 【支持逻辑运算符(and / or / not)】*/
@supports (display: grid) and (gap: 10px) {
  /* 只有当浏览器支持该属性值时,这里的 CSS 规则才会生效 */
}

其它

--- 正则表达式

字符描述
\将下一个字符标记为一个特殊字符、或一个原义字符、或一个向后引用、或一个八进制转义符。例如,“n”匹配字符“n”。“\n”匹配一个换行符。串行“\\”匹配“\”而“\(”则匹配“(”。
^匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性,^也匹配“\n”或“\r”之后的位置。
$匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置。
*匹配前面的子表达式零次或多次。例如,zo能匹配“z”以及“zoo”。等价于{0,}。
+匹配前面的子表达式一次或多次。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{1,}。
?匹配前面的子表达式零次或一次。例如,“do(es)?”可以匹配“does”或“does”中的“do”。?等价于{0,1}。
{n}n是一个非负整数。匹配确定的n次。例如,“o{2}”不能匹配“Bob”中的“o”,但是能匹配“food”中的两个o。
{n,}n是一个非负整数。至少匹配n次。例如,“o{2,}”不能匹配“Bob”中的“o”,但能匹配“foooood”中的所有o。“o{1,}”等价于“o+”。“o{0,}”则等价于“o*”。
{n,m}m和n均为非负整数,其中n<=m。最少匹配n次且最多匹配m次。例如,“o{1,3}”将匹配“fooooood”中的前三个o。“o{0,1}”等价于“o?”。请注意在逗号和两个数之间不能有空格。
?当该字符紧跟在任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串“oooo”,“o+?”将匹配单个“o”,而“o+”将匹配所有“o”。
.匹配除“\``n”之外的任何单个字符。要匹配包括“\``n”在内的任何字符,请使用像“(.\|\n)”的模式。
(pattern)匹配pattern并获取这一匹配。所获取的匹配可以从产生的Matches集合得到,在VBScript中使用SubMatches集合,在JScript中则使用$0…$9属性。要匹配圆括号字符,请使用“\(”或“\)”。
(?:pattern)匹配pattern但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用或字符“(\|)”来组合一个模式的各个部分是很有用。例如“industr(?:y\|ies)”就是一个比“industry\|industries”更简略的表达式。
(?=pattern)正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,“Windows(?=95\|98\|NT\|2000)”能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?!pattern)正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如“Windows(?!95\|98\|NT\|2000)”能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始
(?<=pattern)反向肯定预查,与正向肯定预查类拟,只是方向相反。例如,“(?<=95\|98\|NT\|2000)Windows”能匹配“2000Windows”中的“Windows”,但不能匹配“3.1Windows”中的“Windows”。
(?<!pattern)反向否定预查,与正向否定预查类拟,只是方向相反。例如“(?<!95\|98\|NT\|2000)Windows”能匹配“3.1Windows”中的“Windows”,但不能匹配“2000Windows”中的“Windows”。
xy
[xyz]字符集合。匹配所包含的任意一个字符。例如,“[abc]”可以匹配“plain”中的“a”。
[^xyz]负值字符集合。匹配未包含的任意字符。例如,“[^abc]”可以匹配“plain”中的“p”。
[a-z]字符范围。匹配指定范围内的任意字符。例如,“[a-z]”可以匹配“a”到“z”范围内的任意小写字母字符。
[^a-z]负值字符范围。匹配任何不在指定范围内的任意字符。例如,“[^a-z]”可以匹配任何不在“a”到“z”范围内的任意字符。
\b匹配一个单词边界,也就是指单词和空格间的位置。例如,“er\b”可以匹配“never”中的“er”,但不能匹配“verb”中的“er”。
\B匹配非单词边界。“er\B”能匹配“verb”中的“er”,但不能匹配“never”中的“er”。
\cx匹配由x指明的控制字符。例如,\cM匹配一个Control-M或回车符。x的值必须为A-Z或a-z之一。否则,将c视为一个原义的“c”字符。
\d匹配一个数字字符。等价于[0-9]。
\D匹配一个非数字字符。等价于0-9。
\f匹配一个换页符。等价于\x0c和\cL。
\n匹配一个换行符。等价于\x0a和\cJ。
\r匹配一个回车符。等价于\x0d和\cM。
\s匹配任何空白字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。
\S匹配任何非空白字符。等价于 \f\n\r\t\v。
\t匹配一个制表符。等价于\x09和\cI。
\v匹配一个垂直制表符。等价于\x0b和\cK。
\w匹配包括下划线的任何单词字符。等价于“[A-Za-z0-9_]”。
\W匹配任何非单词字符。等价于“[^A-Za-z0-9_]”。
\xn匹配n,其中n为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,“\x41”匹配“A”。“\x041”则等价于“\x04&1”。正则表达式中可以使用ASCII编码。.
\num匹配num,其中num是一个正整数。对所获取的匹配的引用。例如,“(.)\1”匹配两个连续的相同字符。
\n标识一个八进制转义值或一个向后引用。如果\n之前至少n个获取的子表达式,则n为向后引用。否则,如果n为八进制数字(0-7),则n为一个八进制转义值。
\nm标识一个八进制转义值或一个向后引用。如果\nm之前至少有nm个获得子表达式,则nm为向后引用。如果\nm之前至少有n个获取,则n为一个后跟文字m的向后引用。如果前面的条件都不满足,若n和m均为八进制数字(0-7),则\nm将匹配八进制转义值nm。
\nml如果n为八进制数字(0-3),且m和l均为八进制数字(0-7),则匹配八进制转义值nml。
\un匹配n,其中n是一个用四个十六进制数字表示的Unicode字符。例如,\u00A9匹配版权符号(©)。

--- 常用正则表达式

用户名/^[a-z0-9_-]{3,16}$/
密码/^[a-z0-9_-]{6,18}$/
十六进制值/^#?([a-f0-9]{6}
电子邮箱/^([a-z0-9_.-]+)@([\da-z.-]+).([a-z.]{2,6})$/
/^[a-z\d]+(.[a-z\d]+)*@(\da-z?)+(.{1,2}[a-z]+)+$/
URL/^(https?:\/\/)?([\da-z.-]+).([a-z.]{2,6})([\/\w .-])\/?$/
IP 地址/((2[0-4]\d
HTML 标签/^[([a-z]+)(&lt;+)(?:]((%5Ba-z%5D+)(%5B%5E%3C%5D+)(?:)(.*)<\/\1>
删除代码\注释(?<!http:
Unicode编码中的汉字范围/^[\u2E80-\u9FFF]+$/

Very常用的正则

//手机号正则表达式
let tel = /^1[34578][0-9]{9}$/;

//身份证正则表达式
^\d{15}|\d{18}$

//电子邮箱表达式
/^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/<br/>/^[a-z\d]+(\.[a-z\d]+)*@([\da-z](-[\da-z])?)+(\.{1,2}[a-z]+)+$/

//密码表达式
let pas = /^[a-z0-9_-]{6,18}$/

//只能输入汉子
let 汉子 = /^[\u4e00-\u9fa5]{0,}$/;

//验证是否含有^%&',;=?$\等字符
let 变量名 = /[^%&',;=?$\x22]+/

//验证一年的12个月
^(0?[1-9]|1[0-2])$

//验证一个月的31天
^((0?[1-9])|((1|2)[0-9])|30|31)$

//匹配中国邮政编码
[1-9]\d{5}(?!\d)

//匹配腾讯QQ号
[1-9][0-9]{4,}

//匹配网址URL的正则表达式
[a-zA-z]+://[^\s]*

点击这里查看其它相关的正则:CSDN

--- @其它相关的技术栈

--- **在线工具

1. bootstrap
2. element-ui组件库
3. vueAPI文档
4. MDN文档
5. 微信小程序API