初始化仓库:
git init
配置提交者身份信息:
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 等平台的绑定邮箱一致)"
暂存文件变更,把当前项目里所有改动过的、新增的、删掉的文件,都标记为 “准备好提交” 的状态:
git add .
提交暂存区变更并添加提交说明,用于描述这次提交做了什么修改:
git commit -m "提交/修改描述"
把本地仓库和远程仓库进行绑定:
git remote add origin "创库地址"
把本地 master 分支的代码推送到 origin 仓库:
git push -u origin master
【可选】查看当前 Git 仓库配置了哪些远程仓库地址
git remote -v
【可选】修改 fetch 地址(同时默认 push 地址)【拉取地址】
git remote set-url origin "创库地址"
【可选】清空所有 推送地址,只保留这个新的
git remote set-url --push origin "创库地址"
【可选】添加多个仓库地址进行推送,可添加多个【双推】,命令如下:
git remote set-url --add --push origin "创库地址"
【可选】删除某个仓库推送地址
git remote set-url --delete --push origin "创库地址"
git-https-跳过验证的方法
git config --global http.ssLVerify "false"
Git 的配置命令,用于读取 / 设置 Git 的全局 / 本地配置项
git config
查看全局用户名
git config --global user.name
查看全局邮箱
git config --global user.email
查看当前仓库的用户名(若没设置,会显示全局配置)
git config user.name
查看所有配置(包含 name/email 及其他)
git config --list
查看提交记录
git log
查看本地所有分支(* 标记当前分支)
git branch
创建分支
git branch 分支名
查看本地+远程所有分支
git branch -a
查看分支详细信息(最后提交、关联远程等)
git branch -vv
切换已存在的分支
旧版写法
git checkout 分支名
新版 Git(2.23+)推荐写法,更直观
git switch 分支名
删除本地已合并的分支(安全)
git branch -d 分支名
强制删除本地未合并的分支(谨慎,会丢失未合并的修改)
git branch -D 分支名
将当前分支重命名
git branch -m 新分支名
将指定分支重命名
git branch -m 旧分支名 新分支名
取消本地 master 分支与远程 origin/master 的关联
git branch --unset-upstream master
方式1:HTTPS 地址(无需配置密钥,每次推送需输账号密码)
git remote add origin https://github.com/你的用户名/仓库名.git
方式2:SSH 地址(需配置 SSH 密钥,免密推送,推荐)
git remote add origin git@github.com:你的用户名/仓库名.git
// 新建vue3项目
npm create vue@latest
或
pnpm create vue@latest
或
yarn dlx create-vue@latest
或
bun create vue@latest
** 接收由父组件传递的数据
const props = defineProps({
// 属性配置
属性名: {
// 该属性值的类型
type: [String, Number, ...],或则type: Boolean,
// 该属性的默认值
default: true,
},
············
})
** 自定义事件
const emit = defineEmits(["自定义事件名","自定义事件名",·······]);
// 示例事件
const click = ()=>{
console.log("该事件由页面元素点击触发");
// 添加自定义事件
emit("自定义事件名", '携带参数(可以是数组,字符串,对象,函数等等)')
}
** 子组件对父组件暴露的数据以及方法
// 属性
const state = reactive({
name: "defineExpose",
message: "使用成功"
})
// 示例事件
const click = ()=>{
console.log("事件触发");
}
defineExpose({
// 暴露的属性
name: state.name,
message: state.message,
// 暴露的方法
click,
})
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'
}
下载路由:
npm install vue-router@4
或
yarn add vue-router@4
或
pnpm install vue-router@4
在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;
动态路由表'./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-",
},
}
},
......
];
** 在页面或组件修改仓库属性的方法
下载pinia:
npm i pinia
或
yarn add pinia
或
pnpm i pinia
store.js中的配置pinia:
// 导入pinia
import { createPinia } from 'pinia';
// 创建pinia实例
const pinia = createPinia();
// 暴露store
export const store = (app: { use: Function }) => (app.use(pinia));
创库配置以及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-plugin-persistedstate”:
npm i pinia-plugin-persistedstate
或
yarn add pinia-plugin-persistedstate
或
pnpm i pinia-plugin-persistedstate
在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));
在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);
}
}
},
});
*** 持久化存储官网
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以及less-loader:
npm i less less-loader --save // 由于vite内置less-loader则不需要再次安装
// 只需安装less即可
npm i less -D
或
yarn add less -D
或
pnpm i less -D
下载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
为什么用的scss却要安装sass?
答:我们通常使用scss作为预处理器,但安装的包是sass。这是因为Sass有两种语法:
SCSS(Sassy CSS)使用.scss扩展名,语法和CSS类似,使用大括号和分号。
缩进语法(Sass)使用.sass扩展名,使用缩进和换行而不是大括号和分号。
在Vue项目中,我们通常使用SCSS语法,因为更接近CSS,更容易上手。而安装的包sass是两种语法的实现。
安装mitt:
npm install mitt
或
yarn add mitt
核心 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() | 清空所有事件监听(慎用) |
封装全局事件总线
// 导入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;
触发事件(发送方:页面 / 组件 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')
}
监听 / 取消监听事件(接收方:页面 / 组件 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) // 需传入同一个处理函数
})
监听所有事件(调试 / 全局日志):
// 监听所有事件的触发,type 是事件名,data 是传递的数据
emitter.on('*', (type, data) => {
console.log(`触发事件:${type},数据:`, data)
})
一次性事件(监听一次后自动移除):
// 手动实现一次性监听(mitt 本身无 once 方法,可通过 off 实现)
const handleOnce = (data) => {
console.log('仅执行一次:', data)
emitter.off('once-event', handleOnce) // 执行后立即取消监听
}
emitter.on('once-event', handleOnce)
清空所有监听(慎用,会移除所有事件的监听):
emitter.all.clear()
下载axios:
npm i axios
或
pnpm i axios
或
yarn add axios
或
bun add axios
在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 });
导出封装的方法:
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”:
npm i vue3-lazyload
或
yarn add vue3-lazyload
或
pnpm i vue3-lazyload
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。
在页面/组件中使用懒加载:
<img v-lazy="图片地址" />
下载第三方插件“nprogress”:
npm i nprogress
或
pnpm i nprogress
main.js入口文件导入nprogress以及nprogress样式:
// 进度条进度
import nprogress from 'nprogress';
// 导入进度条进度
import 'nprogress/nprogress.css';
在js或ts中使用nprogress:
// 进度条进度开启
nprogress.start();
// 进度条进度关闭
nprogress.done();
下载第三方插件“animate.css”:
npm i animate.css
或
pnpm i animate.css
main.js入口文件导入animate:
// 导入animate动画库
import 'animate.css';
在页面/组件中使用懒加载:
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>
下载第三方插件“echarts”:
npm i echarts
或
pnpm i echarts
在全局或组件中导入echarts(或在main.js/main.ts中):
// 导入echarts
import * as echarts from 'echarts';
// 或全局注册 --- 可设置echarts全局属性9(通过vue3组合式api"getCurrentInstance"中获取)
app.config.globalProperties.echarts = echarts;
在页面/组件中使用:
/* 在组件或页面中 */
// 第一步,初始化
const myChart = appInst.echarts.init(html元素/html标签(div标签即可));
myChart.setOption({
// echarts官网查询配置项即可
})
示例(柱状图):
*** 注:测试环境 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],
},
],
});
}
示例(饼状图):
*** 注:测试环境 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>
第一步依赖安装
#bootstrap 依赖popperjs 必须安装
npm i --save bootstrap @popperjs/core
第二步安装开发依赖 SASS (由于使用scss必须开发环境安装SASS)
npm i sass --save-dev 或 npm i sass -D
第三步 修改 vite.config.js 文件 (增加一个alias映射bootstrap)
import { fileURLToPath, URL } from 'node:url';
// 导入path
import path from 'node:path';
// 配置依赖
export default defineConfig({
mode: 'production',
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
// 增加~bootstrap
'~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap'),
}
},
server: {
open: true
}
})
第四步 在style.css中导入样式 或 在 assets目录中新建 my.scss,再导入样式
@import "../node_modules/bootstrap";
第五步 在src/main.js 中导入 my.scss
// 导入自己的scss文件
import './style.css' //路径根据这个文件地址来(在style.css中导入样式 或 在 assets目录中新建 my.scss,再导入样式)
// 导入所有bootstrapjs
import * as bootstrap from 'bootstrap'
pako是什么库?
答:pako 是一个用于数据压缩/解压的 JavaScript 库(基于 zlib)。
下载第三方插件“pako”:
npm install pako
或
pnpm add pako
或
yarn add pako
完整导入 和 按需导入:
// 完整导入
import * as pako from 'pako';
// 按需导入(只导入需要的即可)
import { ungzip, ungzip, deflate, inflate } from 'pako';
TypeScript 支持(可选)
pako 自带类型声明,TS 项目无需额外安装 @types/pako,直接导入即可使用。
核心 API 说明:pako主要处理 gzip 和 deflate 两种压缩格式
| 方法 | 作用 | 适用场景 |
|---|---|---|
| pako.gzip() | 压缩数据为 gzip 格式 | 网络传输、大文件压缩 |
| pako.ungzip() | 解压 gzip 格式数据 | 解析服务端压缩响应 |
| pako.deflate() | 压缩数据为 deflate 格式 | 本地存储(体积更小) |
| pako.inflate() | 解压 deflate 格式数据 | 读取本地压缩存储 |
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);
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);
场景 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>
场景 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>
场景 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>
注意事项
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 报错。
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是什么插件?
答:crypto-js 是一个 JavaScript 加密库,封装了常见的加密算法,如 AES、MD5、SHA 系列、DES 等。
下载第三方插件“pako”:
npm install crypto-js
或
pnpm add crypto-js
或
yarn add crypto-js
完整导入 和 按需导入:
// 完整导入
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';
// 其他加密算法...
简单示例
加密方法:
/**
* 加密数据
* @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
}
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;
};
}
封装加密工具函数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');
};
封装加密工具函数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>
9
file-saver是什么插件?
答:FileSaver.js是一个流行的 JavaScript 库,用于在浏览器中保存文件/本地下载(如文本、图片、PDF 、JSON、Blob等)到用户本地设备。
下载第三方插件“file-saver”:
npm install file-saver
或
pnpm add file-saver
或
yarn add file-saver
TypeScript 支持(可选)
npm install @types/file-saver --save-dev
核心 API 与基础用法
saveAs(blob/文件对象/URL, 文件名, 可选配置);
封装复用
// 导入
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>
场景 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>
场景 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>
场景 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>
带样式 + 新窗口下载
<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 Virtual是什么插件:
TanStack Virtual 是一款无头界面工具,用于虚拟化 JS/TS、React、Vue、Svelte、Solid、Lit 和 Angular 中的大量元素列表。anStack Virtual 的核心是虚拟化器。虚拟化器可以选择垂直(默认)或水平轴,这使得通过将两个轴配置结合实现垂直、水平甚至网格状虚拟化。
下载第三方插件“@tanstack/vue-virtual”:
npm install @tanstack/vue-virtual
或
pnpm add @tanstack/vue-virtual
或
yarn add @tanstack/vue-virtual
基本配置项:
/*************** 配置项 ***************/
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();
【固定高度】使用示例:
<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>
【动态高度】使用示例:
关键点:
<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);
}
// 注册全局
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>
/**
* 自定义文字高亮指令(带节流+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, '&') // & → &
.replace(/</g, '<') // < → <
.replace(/>/g, '>') // > → >
.replace(/"/g, '"') // " → "
.replace(/'/g, '''); // ' → '
}
/**
* 使用说明:
* 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 }"
<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
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/, ""),
},
},
},
});
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 [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页面)
* @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:change | value改变时触发 (输入框)事件 |
| 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();
}
// --- > 设置
// 在settings.json添加一下配置
"less.compile": {
"outExt": ".wxss"
},
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,
});
// 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",
},
},
],
});
}
}
/** 局部播放 **/
// 音乐播放器
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();
}
};
// 音频播放
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 仓库
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
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
}
创建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);
}
使用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
* @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,
},
};
}
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 编码的 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();
})
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());
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: "李四" }
/**
* 筛选出当前用户的文件列表 (最优方案,性能最好)
* @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;
}
/* 设置自定义属性 */
1. 元素.setAttribute((属性名), (属性值));
/* insertBefore ---> 把li插入到ul的ul.children[0]之前 */
2. ul.insertBefore(li, ul.children[0]);
3. document.documentElement.clientWidth ---> 获取可视窗口的宽度
document.documentElement.clientHeight ---> 获取可视窗口的高度
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() | 阻止事件冒泡(如子元素点击不触发父元素事件) |
<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>
<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>
核心: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>
/* 鼠标滚轮事件 */
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();
});
document.addEventListener('mousedown', (e) => {
switch(e.button) {
case 0:
console.log('左键按下');
break;
case 1:
console.log('中键按下(滚轮点击)');
break;
case 2:
console.log('右键按下');
break;
}
});
示例:子元素点击不触发父元素事件
const parent = document.getElementById('parent');
const child = document.getElementById('child');
parent.addEventListener('click', () => console.log('父元素点击'));
child.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止冒泡
console.log('子元素点击');
});
const dropTarget = document.getElementById('dropTarget');
dropTarget.addEventListener('dragover', (e) => {
e.preventDefault(); // 必须阻止默认行为,否则drop不触发
});
dropTarget.addEventListener('drop', (e) => {
console.log('元素拖入目标');
});
let timer = null;
document.addEventListener('mousemove', (e) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
// 执行复杂逻辑(如更新DOM)
console.log('鼠标位置:', e.pageX, e.pageY);
}, 16); // 约60帧/秒
});
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)" // 重要
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
},
})
};
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);
});
}
/* 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点击时,抖动问题 ================ */
/* 允许浏览器优化触摸事件,避免滑动被阻塞 */
touch-action: manipulation;
/* 禁用 Safari 原生的触摸高亮 */
-webkit-tap-highlight-color: transparent;
/** ============== 解决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;
/** ============== 基础用法 ================ */
/* 当屏幕宽度大于等于 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 属性或属性值 ================ */
@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”。 |
| x | y |
| [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]+)(<+)(?:]((%5Ba-z%5D+)(%5B%5E%3C%5D+)(?:)(.*)<\/\1> |
| 删除代码\注释 | (?<!http: |
| Unicode编码中的汉字范围 | /^[\u2E80-\u9FFF]+$/ |
//手机号正则表达式
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