前端js常用代码片段

文章目录

获取滚动条y轴滚动距离

js 复制代码
// 现代项目推荐:优先使用 window.scrollY,降级使用传统方式
const getScrollTop = () => {
  return window.scrollY !== undefined 
    ? window.scrollY 
    : (document.documentElement.scrollTop || document.body.scrollTop);
};

检测视频播放技术

js 复制代码
// 检测视频播放技术
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

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

html 复制代码
<script src="./js/lazyload.min.js"></script>

<script>
    document.addEventListener('DOMContentLoaded', function () {
    // 初始化 vanilla-lazyload
    const lazyLoadInstance = new LazyLoad({
        elements_selector: ".lazy",
        threshold: -200
        });
    });
</script>

楼梯导航功能函数式封装

js 复制代码
 // 楼梯导航功能函数式封装
        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';
        }

获取滚动条距离顶部的距离

js 复制代码
// 现代项目推荐:优先使用 window.scrollY,降级使用传统方式
const getScrollTop = () => {
    return window.scrollY !== undefined
        ? window.scrollY
    : (document.documentElement.scrollTop || document.body.scrollTop);
};

验证手机号格式

js 复制代码
// 验证手机号格式
function validatePhone(phone) {
	const phoneRegex = /^1[3-9]\d{9}$/;
	return phoneRegex.test(phone);
}

生成随机验证码

js 复制代码
// 生成随机验证码
function generateRandomCode(length) {
	let code = '';
	for (let i = 0; i < length; i++) {
		code += Math.floor(Math.random() * 10);
	}
	return code;
}

转义HTML特殊字符,防止XSS攻击

js 复制代码
function escapeHtml(text) {
    const map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#039;'
    };

    return text.replace(/[&<>"']/g, function (m) { return map[m]; });
}

格式化日期

js 复制代码
// 格式化日期
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');
    }
}

获取随机颜色

css 复制代码
// 获取随机颜色
function getRandomColor() {
    const colors = ['#ffffff', '#ffeb3b', '#4caf50', '#2196f3', '#ff9800', '#e91e63'];
    return colors[Math.floor(Math.random() * colors.length)];
}

格式化数字(添加千位分隔符)

js 复制代码
// 格式化数字(添加千位分隔符)
function formatNumber(num) {
	return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

计算密码强度

js 复制代码
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
}

右上角移入显示通知

js 复制代码
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的大小

js 复制代码
// 将屏幕分成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);

解锁网页复制/粘贴/右键/切屏限制

js 复制代码
["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);
});

加购抛物线动画

js 复制代码
/**
 * 抛物线动画
 * @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;
  }
}

处理轮播图水平方向的拖拽切换

js 复制代码
/**
       * 处理轮播图水平方向的拖拽切换
       * @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);
          }
        }
      }

获取鼠标在元素内的相对位置

js 复制代码
/**
       * 获取鼠标在元素内的相对位置
       * @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 };
      }

拖拽滚动

js 复制代码
/**
       * 拖拽滚动
       * @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",
        });
      }

判断

判断元素是否进入可视区

js 复制代码
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;
  }
}

判断两个元素是否相交

js 复制代码
/**
       * 判断两个元素是否相交
       * @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
        );
      }

确保是数组

js 复制代码
/**
 * 确保是数组
 * @param value 
 */
const ensureArray = (value) => {
    if (!value && value != 0) return [];
    return Array.isArray(value) ? value : [value];
}

判断是不是boolean

js 复制代码
// 判断是不是boolean
const isBoolean = (value) => {
  return typeof value === "boolean";
};

判断是不是promise,不考虑鸭子类型

js 复制代码
// 判断是不是promise,不考虑鸭子类型
const isPromise = (value) => {
  return value instanceof Promise;
};

判断是否支持在线预览

js 复制代码
// 判断是否支持在线预览
    isPreviewSupported(url) {
      const supportedExtensions = ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.pdf']
      const fileExtension = url.toLowerCase().substring(url.lastIndexOf('.'))
      return supportedExtensions.includes(fileExtension)
    },

解析目录路径,将相对路径转换为绝对路径

js 复制代码
const path = require('path')
/**
 * 解析目录路径,将相对路径转换为绝对路径
 * @param {string} dir - 需要解析的目录路径
 * @returns {string} 返回基于当前文件所在目录的绝对路径
 */
function resolve(dir) {
  return path.join(__dirname, dir)
}

文件相关

从文件URL中提取文件名

js 复制代码
// 从文件URL中提取文件名
    getFileNameFromUrl(url) {
      if (!url) return ''
      // 获取URL中最后一段路径,即文件名
      const pathSegments = url.split('/')
      const fileName = pathSegments[pathSegments.length - 1]
      return fileName
    }

下载文件方法

js 复制代码
downloadFile(url, filename) {
      const link = document.createElement('a')
      link.href = url
      link.download = filename || this.getFileNameFromUrl(url)
      link.target = '_blank'
      link.click()
    }
js 复制代码
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); // 释放内存
}

文件上传

js 复制代码
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

js 复制代码
const rand = () => {
  return Math.random().toString(36).substring(2);
}

分页

组件

js 复制代码
<!-- 分页组件 -->
      <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>

计算属性

js 复制代码
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;
          }
        }

切换分页

js 复制代码
goToPage(page) {
            if (page < 1 || page > this.totalPages || page === this.currentPage) {
              return;
            }
            
            this.currentPage = page;
            
            // 滚动到表格顶部
            document.querySelector('.table-container').scrollIntoView({ behavior: 'smooth' });
          },

自动收集所有 mock 模块

ts 复制代码
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);
}

全局错误处理

ts 复制代码
/**
 * 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项目启动入口

js 复制代码
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();

封装统一响应

ts 复制代码
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)));
  }
}

路由

动态添加路由

  1. 获取 Pinia 的 permissionStore 实例。
  2. 通过标记 isDynamicAddedRoute 判断是否已经添加过动态路由,避免重复添加。
  3. 如果是首次,调用 buildRoutesAction()(异步)生成动态路由数组。
  4. 遍历并将每个路由通过 router.addRoute 注册到路由表中。
  5. 额外添加一个 PAGE_NOT_FOUND_ROUTE 作为 404 兜底路由。
  6. 标记 isDynamicAddedRoute = true,后续不再执行。
ts 复制代码
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);
}

生成路由的名称与组件映射

ts 复制代码
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;

判断所有的子菜单全部关闭后,主菜单才能关闭

ts 复制代码
 // 判断所有的子菜单全部关闭后,主菜单才能关闭
      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;
      }