文章目录
获取滚动条y轴滚动距离
// 现代项目推荐:优先使用 window.scrollY,降级使用传统方式
const getScrollTop = () => {
return window.scrollY !== undefined
? window.scrollY
: (document.documentElement.scrollTop || document.body.scrollTop);
};
检测视频播放技术
// 检测视频播放技术
function detectVideoTechnology(videoElement) {
const results = {
hasSrc: !!videoElement.src,
hasSrcObject: !!videoElement.srcObject,
currentSrc: videoElement.currentSrc,
networkState: videoElement.networkState,
readyState: videoElement.readyState
};
console.group('视频技术检测');
if (videoElement.srcObject) {
console.log('✅ 使用 WebRTC/MediaStream');
const stream = videoElement.srcObject;
console.log('MediaStream ID:', stream.id);
console.log('轨道:', stream.getTracks().map(t => `${t.kind}:${t.label}`));
// 检查是否是WebRTC
const videoTrack = stream.getVideoTracks()[0];
if (videoTrack && videoTrack.readyState === 'live') {
console.log('🔴 直播流 (WebRTC)');
}
}
else if (videoElement.src && videoElement.src.startsWith('blob:')) {
console.log('✅ 使用 MSE (Media Source Extensions)');
console.log('Blob URL:', videoElement.src);
}
else if (videoElement.src) {
console.log('✅ 使用传统 src URL');
console.log('URL:', videoElement.src);
// 检查格式
if (videoElement.src.includes('.m3u8')) {
console.log('📺 HLS 流');
} else if (videoElement.src.includes('.flv')) {
console.log('🎥 FLV 流');
}
}
else {
console.log('❓ 未知播放方式');
}
console.groupEnd();
return results;
}
// 使用
detectVideoTechnology(document.querySelector('video'));
图片懒加载
原生js
lazyLoad();
function lazyLoad() {
const imgs = document.querySelectorAll('img[data-src]');
let loadedCount = 0;
const io = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
const img = entry.target;
setTimeout(() => {
// 设置图片的src属性为data-src属性的值
img.src = img.dataset.src;
}, 500);
// 停止观察当前图片
io.unobserve(img);
loadedCount++;
if (loadedCount === imgs.length) {
io.disconnect();
}
}
})
}, {
rootMargin: '100px',
});
// 修改这里:遍历每个图片元素并观察
imgs.forEach(function (img) {
io.observe(img);
});
}
vanilla-lazyload
<script src="./js/lazyload.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// 初始化 vanilla-lazyload
const lazyLoadInstance = new LazyLoad({
elements_selector: ".lazy",
threshold: -200
});
});
</script>
楼梯导航功能函数式封装
// 楼梯导航功能函数式封装
const initLoutiNavigation = () => {
// 获取DOM元素
const lis = document.querySelectorAll('.louti ul li');
const loutiContents = document.querySelectorAll('.louti-content');
// 状态变量
let currentIndex = 0;
let isScrolling = false;
let scrollTimeout = null;
const scrollTops = [];
// 计算每个楼层的滚动位置
const calculateScrollPositions = () => {
for (let i = 0; i < loutiContents.length; i++) {
scrollTops.push(loutiContents[i].offsetTop);
}
};
// 处理楼梯导航点击事件
const handleLoutiClick = (index) => {
if (isScrolling) return;
isScrolling = true;
if (scrollTimeout) clearTimeout(scrollTimeout);
// 更新样式
lis[currentIndex].classList.remove('active');
lis[index].classList.add('active');
currentIndex = index;
// 执行滚动
window.scroll({
top: scrollTops[index],
behavior: 'smooth'
});
// 滚动完成后重置状态
scrollTimeout = setTimeout(() => {
isScrolling = false;
}, 1000);
};
// 更新激活状态
const updateActiveLouti = () => {
if (isScrolling) return;
const y = window.scrollY;
let activeIndex = lis.length - 1;
for (let i = 0; i < scrollTops.length; i++) {
if (scrollTops[i] > y + loutiContents[activeIndex].clientHeight / 3) {
activeIndex = i - 1 > 0 ? i - 1 : 0;
break;
}
}
if (currentIndex !== activeIndex) {
lis[currentIndex].classList.remove('active');
lis[activeIndex].classList.add('active');
currentIndex = activeIndex;
}
};
// 绑定点击事件
const bindClickEvents = () => {
for (let i = 0; i < lis.length; i++) {
lis[i].addEventListener('click', () => handleLoutiClick(i));
}
};
// 绑定滚动事件
const bindScrollEvent = () => {
window.addEventListener('scroll', updateActiveLouti);
};
// 初始化函数
const init = () => {
calculateScrollPositions();
bindClickEvents();
bindScrollEvent();
// 延迟执行初始状态检测,避免页面刷新时的闪烁
setTimeout(() => {
updateActiveLouti();
}, 500);
};
// 执行初始化
init();
};
// 页面加载完成后初始化楼梯导航
window.onload = function () {
initLoutiNavigation();
};
// 楼梯显示问题
document.addEventListener('scroll', hideLouti);
// 窗口大小改变
window.addEventListener('resize', hideLouti);
function hideLouti() {
const y = window.scrollY;
if (y > 300 && window.innerWidth > 1400) {
// 楼梯显示
document.querySelector('.louti').style.display = 'block';
return;
}
document.querySelector('.louti').style.display = 'none';
}
获取滚动条距离顶部的距离
// 现代项目推荐:优先使用 window.scrollY,降级使用传统方式
const getScrollTop = () => {
return window.scrollY !== undefined
? window.scrollY
: (document.documentElement.scrollTop || document.body.scrollTop);
};
验证手机号格式
// 验证手机号格式
function validatePhone(phone) {
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(phone);
}
生成随机验证码
// 生成随机验证码
function generateRandomCode(length) {
let code = '';
for (let i = 0; i < length; i++) {
code += Math.floor(Math.random() * 10);
}
return code;
}
转义HTML特殊字符,防止XSS攻击
function escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, function (m) { return map[m]; });
}
格式化日期
// 格式化日期
function formatDate(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffTime = Math.abs(now - date);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 1 || diffDays === 0) {
return '今天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
} else if (diffDays === 2) {
return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
} else if (diffDays <= 7) {
return diffDays + '天前';
} else {
return date.toLocaleDateString('zh-CN');
}
}
获取随机颜色
// 获取随机颜色
function getRandomColor() {
const colors = ['#ffffff', '#ffeb3b', '#4caf50', '#2196f3', '#ff9800', '#e91e63'];
return colors[Math.floor(Math.random() * colors.length)];
}
格式化数字(添加千位分隔符)
// 格式化数字(添加千位分隔符)
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
计算密码强度
function calculateStrength(password) {
let strength = 0;
// 长度因素
if (password.length >= 8) strength += 1;
if (password.length >= 12) strength += 1;
if (password.length >= 16) strength += 1;
// 字符类型因素
const hasLower = /[a-z]/.test(password);
const hasUpper = /[A-Z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSymbol = /[^A-Za-z0-9]/.test(password);
const typesCount = [hasLower, hasUpper, hasNumber, hasSymbol].filter(Boolean).length;
// 至少包含三种不同类型的字符才加分
if (typesCount >= 3) strength += 1;
if (typesCount === 4) strength += 1;
return Math.min(strength, 4); // 最大为4
}
右上角移入显示通知
function showNotification(message) {
// 创建通知元素
const notification = document.createElement('div');
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #42b983;
color: white;
padding: 15px 20px;
border-radius: 5px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
animation: slideIn 0.3s, fadeOut 0.5s 2.5s;
`;
// 添加动画样式
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
`;
document.head.appendChild(style);
document.body.appendChild(notification);
// 3秒后移除通知
setTimeout(() => {
notification.remove();
style.remove();
}, 3000);
}
将屏幕分成10份计算1rem的大小
// 将屏幕分成10份计算1rem的大小
initPage();
function initPage() {
const html = document.documentElement;
// 当前屏幕设备宽
const clientWidth = Math.min(html.clientWidth, 540);
// 计算1rem的大小(不同屏幕尺寸下)
const rem = clientWidth / 10 + "px";
html.style.fontSize = rem;
}
window.addEventListener("resize", initPage);
解锁网页复制/粘贴/右键/切屏限制
["document", "window"].forEach(function (objName) {
// 兼容性检查,避免在不支持 getEventListeners 的环境中报错
if (typeof getEventListeners !== "function") {
return;
}
var obj = window[objName]; // 直接索引简化逻辑
if (!obj) return;
var eventTypes = [
"copy",
"cut",
"paste",
"selectstart",
"contextmenu",
"dragstart",
"mousedown",
"mouseup",
"keydown",
"keyup",
"keypress",
"blur",
"focus",
"visibilitychange",
"mouseleave",
"mouseout",
"pagehide",
"beforeunload",
"unload",
];
var allListeners;
try {
// 获取一次监听器集合用于复用
allListeners = getEventListeners(obj);
} catch (e) {
// 忽略无法获取监听器的情况(如跨域)
return;
}
function removeEventListenerByType(eventType) {
var listeners = allListeners[eventType] || [];
listeners.forEach(function (listener) {
obj.removeEventListener(
eventType,
listener.listener,
listener.useCapture
);
console.log("已移除", objName, "上的", eventType, "事件监听器");
});
}
eventTypes.forEach(removeEventListenerByType);
});
加购抛物线动画
/**
* 抛物线动画
* @param {string} button_cssSelector 按钮的css选择器
* @param {string} shoppingCartId 购物车图标的id
* @param {number} ball_endLeft 小球动画结束后的位置的x坐标
* @param {number} ball_endButtom 小球动画结束后的位置的y坐标
* @example parabolaAnimation(".add-to-cart", "shopping-cart", 45, 60);
*/
function parabolaAnimation(
button_cssSelector,
shoppingCartId,
ball_endLeft,
ball_endButtom
) {
// const addToCartButton = document.querySelector(button_cssSelector);
// addToCartButton.addEventListener("click", clickHandler);
const addToCartButtons = document.querySelectorAll(button_cssSelector);
for (let i = 0; i < addToCartButtons.length; i++) {
addToCartButtons[i].addEventListener("click", clickHandler);
}
function clickHandler() {
const rect = this.getBoundingClientRect();
const left = rect.left;
const buttom = window.innerHeight - rect.top;
const ball = createBall(left, buttom);
// 根据购物车的id,来找到购物车元素,然后知道购物车的位置
// 从而计算得到小球动画结束后的位置
const shoppingCartEl = document.getElementById(shoppingCartId);
const shoppingCartRect = shoppingCartEl.getBoundingClientRect();
let _ball_endLeft =
shoppingCartRect.left + shoppingCartRect.width / 2 - rect.width / 5;
let _ball_endButtom =
window.innerHeight -
shoppingCartRect.bottom +
shoppingCartRect.height / 2;
if (ball_endLeft) {
_ball_endLeft = ball_endLeft;
}
if (ball_endButtom) {
_ball_endButtom = ball_endButtom;
}
ball.isDomRemoved = false;
setTimeout(() => {
ball.style.transition =
"left 1s, bottom 1s cubic-bezier(0.25, -0.76, 0.9, 0.71)";
ball.style.left = _ball_endLeft + "px";
ball.style.bottom = _ball_endButtom + "px";
});
ball.addEventListener("transitionend", transitionendHandler);
}
function transitionendHandler() {
if (!this.isDomRemoved) {
document.body.removeChild(this);
this.isDomRemoved = true;
}
}
function createBall(left, buttom) {
const div = document.createElement("div");
div.style = `
width: 20px;
height: 20px;
background-color: tomato;
border-radius: 50%;
position: fixed;
cursor: pointer;
z-index: 999;
`;
div.style.left = left + "px";
div.style.bottom = buttom + "px";
document.body.appendChild(div);
return div;
}
}
处理轮播图水平方向的拖拽切换
/**
* 处理轮播图水平方向的拖拽切换
* @param {Element} el 拖拽的元素
* @param {Function} callBack 拖拽结束的回调
* @example getHorizontalDrag(el, function(dir){})
*/
function getHorizontalDrag(el, callBack) {
el.addEventListener("mousedown", mousedownHandler);
function mousedownHandler(e) {
// 取消默认行为
e.preventDefault();
// 获取鼠标按下的位置X坐标
const startPageX = e.pageX;
el.addEventListener("mouseup", mouseupHandler);
function mouseupHandler(e) {
// 获取鼠标抬起时,与浏览器(页面)左边的距离
const endPageX = e.pageX;
let moveDistance = endPageX - startPageX;
// 判断是向左还是向右
if (moveDistance > 10) {
callBack("right");
}
if (moveDistance < -10) {
callBack("left");
}
el.removeEventListener("mouseup", mouseupHandler);
}
}
}
获取鼠标在元素内的相对位置
/**
* 获取鼠标在元素内的相对位置
* @param {HTMLElement} el 元素
* @param {number} clientX 鼠标的x坐标
* @param {number} clientY 鼠标的y坐标
* @example const { x, y } = getMouseRelativePositionInElement(
fileContainerEl,
e.clientX,
e.clientY
);
* @returns {{x: number, y: number}}
*/
function getMouseRelativePositionInElement(el, clientX, clientY) {
const { top, left } = el.getBoundingClientRect();
const { scrollTop, scrollLeft } = fileContainerEl;
const x = clientX - left + scrollLeft;
const y = clientY - top + scrollTop;
return { x, y };
}
拖拽滚动
/**
* 拖拽滚动
* @param {*} el 滚动元素
* @param {*} clientY 鼠标位置
*/
function dragScroll(el, clientY) {
const { top, left, width, height } = el.getBoundingClientRect();
let scroll = 0;
// 向下滚动
if (clientY > top + height) {
scrollY = clientY - top - height;
}
// 向上滚动
if (clientY < top) {
scrollY = clientY - top;
}
fileContainerEl.scrollBy({
top: scrollY,
behavior: "auto",
});
}
判断
判断元素是否进入可视区
function isElementEnterViewport(element) {
// 获取元素的位置信息
const rect = element.getBoundingClientRect();
// 浏览器可视区的高
const clientHeight =
document.documentElement.clientHeight || document.body.clientHeight;
// 判断元素是否进入可视区
if (rect.top < clientHeight && rect.bottom > 0) {
// 进入可视区 ,返回真
return true;
} else {
// 没有进入可视区,返回假
return false;
}
}
判断两个元素是否相交
/**
* 判断两个元素是否相交
* @param el1 待判断的元素1
* @param el2 待判断的元素2
* @returns {boolean}
*/
function isIntersect(el1, el2) {
const rect1 = el1.getBoundingClientRect();
const rect2 = el2.getBoundingClientRect();
return (
rect1.left < rect2.right &&
rect1.right > rect2.left &&
rect1.top < rect2.bottom &&
rect1.bottom > rect2.top
);
}
确保是数组
/**
* 确保是数组
* @param value
*/
const ensureArray = (value) => {
if (!value && value != 0) return [];
return Array.isArray(value) ? value : [value];
}
判断是不是boolean
// 判断是不是boolean
const isBoolean = (value) => {
return typeof value === "boolean";
};
判断是不是promise,不考虑鸭子类型
// 判断是不是promise,不考虑鸭子类型
const isPromise = (value) => {
return value instanceof Promise;
};
判断是否支持在线预览
// 判断是否支持在线预览
isPreviewSupported(url) {
const supportedExtensions = ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.pdf']
const fileExtension = url.toLowerCase().substring(url.lastIndexOf('.'))
return supportedExtensions.includes(fileExtension)
},
解析目录路径,将相对路径转换为绝对路径
const path = require('path')
/**
* 解析目录路径,将相对路径转换为绝对路径
* @param {string} dir - 需要解析的目录路径
* @returns {string} 返回基于当前文件所在目录的绝对路径
*/
function resolve(dir) {
return path.join(__dirname, dir)
}
文件相关
从文件URL中提取文件名
// 从文件URL中提取文件名
getFileNameFromUrl(url) {
if (!url) return ''
// 获取URL中最后一段路径,即文件名
const pathSegments = url.split('/')
const fileName = pathSegments[pathSegments.length - 1]
return fileName
}
下载文件方法
downloadFile(url, filename) {
const link = document.createElement('a')
link.href = url
link.download = filename || this.getFileNameFromUrl(url)
link.target = '_blank'
link.click()
}
async function download() {
const data = await $fetch<Blob>(
"https://www.fengfengzhidao.com/_nuxt/DjktnUbq.js",
{
responseType: "blob",
},
);
const url = window.URL.createObjectURL(data);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "DjktnUbq.js");
document.body.appendChild(link);
// 模拟点击并移除
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url); // 释放内存
}
文件上传
async function fileUpload(e: Event) {
const el = e.target as HTMLInputElement;
if (!el.files) {
return;
}
const file = el.files[0] as File;
const form = new FormData();
form.append("file", file);
form.append("title", file.name);
try {
const data = await $fetch("/api/upload", {
method: "POST",
body: form,
});
console.log(data);
} catch (e) {
console.log(e);
}
}
生成随机id0-9a-z长度9-11
const rand = () => {
return Math.random().toString(36).substring(2);
}
分页
组件
<!-- 分页组件 -->
<div class="pagination" v-if="totalPages > 1">
<button
:class="{ active: currentPage === 1 }"
@click="goToPage(1)"
:disabled="currentPage === 1"
>
首页
</button>
<button
:class="{ active: page === currentPage }"
v-for="page in visiblePages"
:key="page"
@click="goToPage(page)"
>
{{ page }}
</button>
<button
:class="{ active: currentPage === totalPages }"
@click="goToPage(totalPages)"
:disabled="currentPage === totalPages"
>
末页
</button>
<span style="margin-left: 20px;">共 {{ allMusicList.length }} 条数据,每页 {{ pageSize }} 条,共 {{ totalPages }} 页</span>
</div>
计算属性
computed: {
// 分页后的音乐列表
pagedMusicList() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.allMusicList.slice(start, end);
},
// 总页数
totalPages() {
return Math.ceil(this.allMusicList.length / this.pageSize);
},
// 计算显示的页码,限制最多显示5个页码按钮
visiblePages() {
const maxVisiblePages = 5;
let startPage = Math.max(1, this.currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(this.totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
const pages = [];
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages;
}
}
切换分页
goToPage(page) {
if (page < 1 || page > this.totalPages || page === this.currentPage) {
return;
}
this.currentPage = page;
// 滚动到表格顶部
document.querySelector('.table-container').scrollIntoView({ behavior: 'smooth' });
},
自动收集所有 mock 模块
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
// 问题描述
// 1. `import.meta.globEager` 已被弃用, 需要升级vite版本,有兼容问题
// 2. `vite-plugin-mock` 插件问题 https://github.com/vbenjs/vite-plugin-mock/issues/56
// const modules: Record<string, any> = import.meta.glob("./**/*.ts", {
// import: "default",
// eager: true,
// });
// const mockModules = Object.keys(modules).reduce((pre, key) => {
// if (!key.includes("/_")) {
// pre.push(...modules[key]);
// }
// return pre;
// }, [] as any[]);
const modules = import.meta.globEager('./**/*.ts');
const mockModules: any[] = [];
Object.keys(modules).forEach((key) => {
if (key.includes('/_')) {
return;
}
mockModules.push(...modules[key].default);
});
/**
* Used in a production environment. Need to manually import all modules
*/
export function setupProdMockServer() {
createProdMockServer(mockModules);
}
全局错误处理
/**
* Used to configure the global error handling function, which can monitor vue errors, script errors, static resource errors and Promise errors
*/
import type { ErrorLogInfo } from '/#/store';
import { useErrorLogStoreWithOut } from '/@/store/modules/errorLog';
import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
import { App } from 'vue';
import projectSetting from '/@/settings/projectSetting';
/**
* Handling error stack information
* @param error
*/
function processStackMsg(error: Error) {
if (!error.stack) {
return '';
}
let stack = error.stack
.replace(/\n/gi, '') // Remove line breaks to save the size of the transmitted content
.replace(/\bat\b/gi, '@') // At in chrome, @ in ff
.split('@') // Split information with @
.slice(0, 9) // The maximum stack length (Error.stackTraceLimit = 10), so only take the first 10
.map((v) => v.replace(/^\s*|\s*$/g, '')) // Remove extra spaces
.join('~') // Manually add separators for later display
.replace(/\?[^:]+/gi, ''); // Remove redundant parameters of js file links (?x=1 and the like)
const msg = error.toString();
if (stack.indexOf(msg) < 0) {
stack = msg + '@' + stack;
}
return stack;
}
/**
* get comp name
* @param vm
*/
function formatComponentName(vm: any) {
if (vm.$root === vm) {
return {
name: 'root',
path: 'root',
};
}
const options = vm.$options as any;
if (!options) {
return {
name: 'anonymous',
path: 'anonymous',
};
}
const name = options.name || options._componentTag;
return {
name: name,
path: options.__file,
};
}
/**
* Configure Vue error handling function
*/
function vueErrorHandler(err: Error, vm: any, info: string) {
const errorLogStore = useErrorLogStoreWithOut();
const { name, path } = formatComponentName(vm);
errorLogStore.addErrorLogInfo({
type: ErrorTypeEnum.VUE,
name,
file: path,
message: err.message,
stack: processStackMsg(err),
detail: info,
url: window.location.href,
});
}
/**
* Configure script error handling function
*/
export function scriptErrorHandler(
event: Event | string,
source?: string,
lineno?: number,
colno?: number,
error?: Error,
) {
if (event === 'Script error.' && !source) {
return false;
}
const errorInfo: Partial<ErrorLogInfo> = {};
colno = colno || (window.event && (window.event as any).errorCharacter) || 0;
errorInfo.message = event as string;
if (error?.stack) {
errorInfo.stack = error.stack;
} else {
errorInfo.stack = '';
}
const name = source ? source.substr(source.lastIndexOf('/') + 1) : 'script';
const errorLogStore = useErrorLogStoreWithOut();
errorLogStore.addErrorLogInfo({
type: ErrorTypeEnum.SCRIPT,
name: name,
file: source as string,
detail: 'lineno' + lineno,
url: window.location.href,
...(errorInfo as Pick<ErrorLogInfo, 'message' | 'stack'>),
});
return true;
}
/**
* Configure Promise error handling function
*/
function registerPromiseErrorHandler() {
window.addEventListener(
'unhandledrejection',
function (event) {
const errorLogStore = useErrorLogStoreWithOut();
errorLogStore.addErrorLogInfo({
type: ErrorTypeEnum.PROMISE,
name: 'Promise Error!',
file: 'none',
detail: 'promise error!',
url: window.location.href,
stack: 'promise error!',
message: event.reason,
});
},
true,
);
}
/**
* Configure monitoring resource loading error handling function
*/
function registerResourceErrorHandler() {
// Monitoring resource loading error(img,script,css,and jsonp)
window.addEventListener(
'error',
function (e: Event) {
const target = e.target ? e.target : (e.srcElement as any);
const errorLogStore = useErrorLogStoreWithOut();
errorLogStore.addErrorLogInfo({
type: ErrorTypeEnum.RESOURCE,
name: 'Resource Error!',
file: (e.target || ({} as any)).currentSrc,
detail: JSON.stringify({
tagName: target.localName,
html: target.outerHTML,
type: e.type,
}),
url: window.location.href,
stack: 'resource is not found',
message: (e.target || ({} as any)).localName + ' is load error',
});
},
true,
);
}
/**
* Configure global error handling
* @param app
*/
export function setupErrorHandle(app: App) {
const { useErrorHandle } = projectSetting;
if (!useErrorHandle) {
return;
}
// Vue exception monitoring;
app.config.errorHandler = vueErrorHandler;
// script error
window.onerror = scriptErrorHandler;
// promise exception
registerPromiseErrorHandler();
// Static resource exception
registerResourceErrorHandler();
}
vue3项目启动入口
async function bootstrap() {
const app = createApp(App);
// Configure store
// 配置 store
setupStore(app);
// Initialize internal system configuration
// 初始化内部系统配置
initAppConfigStore();
// Register global components
// 注册全局组件
registerGlobComp(app);
// Multilingual configuration
// 多语言配置
// Asynchronous case: language files may be obtained from the server side
// 异步案例:语言文件可能从服务器端获取
await setupI18n(app);
// Configure routing
// 配置路由
setupRouter(app);
// router-guard
// 路由守卫
setupRouterGuard(router);
// Register global directive
// 注册全局指令
setupGlobDirectives(app);
// Configure global error handling
// 配置全局错误处理
setupErrorHandle(app);
// https://next.router.vuejs.org/api/#isready
// await router.isReady();
app.mount('#app');
}
bootstrap();
封装统一响应
export class Result {
static successCount(data: any, count, message: string = '' as string) {
return {
code: 0,
result: data,
message,
count,
};
}
static success(data: any, message: string = '' as string) {
return {
code: 0,
result: data,
message,
};
}
static error(message: string = '' as string) {
return {
code: -1,
message,
};
}
static wrapperResponse(p: Promise<any>, msg) {
return p
.then((data) => Result.success(data, msg))
.catch((err) => Result.error(err.message));
}
static wrapperCountResponse(
dataPromise: Promise<any>,
countPromise: Promise<any>,
msg?: string,
) {
return Promise.all([dataPromise, countPromise])
.then(([data, count]) => Result.successCount(data, count, msg))
.catch((err) => Result.error(err?.message || String(err)));
}
}
路由
动态添加路由
- 获取 Pinia 的
permissionStore 实例。
- 通过标记
isDynamicAddedRoute 判断是否已经添加过动态路由,避免重复添加。
- 如果是首次,调用
buildRoutesAction()(异步)生成动态路由数组。
- 遍历并将每个路由通过
router.addRoute 注册到路由表中。
- 额外添加一个
PAGE_NOT_FOUND_ROUTE 作为 404 兜底路由。
- 标记
isDynamicAddedRoute = true,后续不再执行。
const permissionStore = usePermissionStore();
if (!permissionStore.isDynamicAddedRoute) {
const routes = await permissionStore.buildRoutesAction();
routes.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw);
permissionStore.setDynamicAddedRoute(true);
}
生成路由的名称与组件映射
import { asyncRoutes } from '@/router/routes';
const newRoutes = {};
/**
* 递归遍历路由配置,将每个路由的名称与组件映射关系收集到 newRoutes 中
* @param routes - 路由配置数组,每个元素包含 name、component 及可选的 children 属性
* @returns 遍历后的映射数组(主要副作用为填充 newRoutes 对象)
*/
function generateRouteMap(routes) {
return routes.map((item) => {
// 递归处理子路由,确保嵌套路由的组件也被收集
if (item.children && item.children.length > 0) {
generateRouteMap(item.children);
}
newRoutes[item.name] = item.component;
});
}
generateRouteMap(asyncRoutes);
export const ROUTE_MAP = newRoutes;
判断所有的子菜单全部关闭后,主菜单才能关闭
// 判断所有的子菜单全部关闭后,主菜单才能关闭
function checkAllChildrenMenuDisabled(updateMenu) {
const id = updateMenu.id;
let isAllClosed = true;
const subMenu = menuList.filter((item) => item.pid === id);
subMenu.forEach((menu) => {
if (+menu.active === 1) {
isAllClosed = false;
return;
}
});
if (!isAllClosed) {
return false;
}
// 查询三级菜单
subMenu.forEach((menu) => {
isAllClosed = checkAllChildrenMenuDisabled(menu);
if (!isAllClosed) {
return;
}
});
return isAllClosed;
}