[{"content":"\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;div class=\u0026#34;compliance-bar\u0026#34; v-for=\u0026#34;item in averageScore\u0026#34; :key=\u0026#34;item.name\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;bar-label\u0026#34;\u0026gt; {{ item.name }} \u0026lt;div\u0026gt; \u0026lt;span :style=\u0026#34;`color:${item.color}`\u0026#34;\u0026gt;{{ item.value + `${item.base === \u0026#39;/ 100%\u0026#39; ? \u0026#39;%\u0026#39; : \u0026#39;\u0026#39;}` }}\u0026lt;/span\u0026gt; {{ item.base }} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;bar-bg\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;bar-fill\u0026#34; :style=\u0026#34;{ width: item.base === \u0026#39;/ 100%\u0026#39; ? item.value + \u0026#39;%\u0026#39; : item.value / 5 * 100 + \u0026#39;%\u0026#39;, backgroundColor: item.color }\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; 1 2 3 4 5 6 const averageScore = [ { name: \u0026#34;Qualitative Assessment (QA)\u0026#34;, value: 2.4, base: \u0026#39;/ 5\u0026#39;, color: \u0026#34;#ee6262\u0026#34; }, { name: \u0026#34;Facility Proximity (FP)\u0026#34;, value: 78.6, base: \u0026#39;/ 100%\u0026#39;, color: \u0026#34;#ffe2a4\u0026#34; }, { name: \u0026#34;Infrastructure Completion (IC)\u0026#34;, value: 91.97, base: \u0026#39;/ 100%\u0026#39;, color: \u0026#34;#b8afff\u0026#34; } ]; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 .compliance-bar { @include MarginBottom(20); .bar-label { display: flex; @include hHeight(17); font-family: PingFang SC; @include FontSize(12); text-transform: capitalize; @include MarginBottom(7); justify-content: space-between; color: #D3D3D4; } .bar-bg { width: 100%; @include hHeight(8); background: #39404a; .bar-fill { height: 100%; @include BorderRadius(4); } } } ","date":"2025-06-03T01:05:52+08:00","permalink":"https://linsk27.github.io/p/0603-%E6%89%8B%E6%92%B8%E5%A5%BD%E7%9C%8B%E6%9F%B1%E7%8A%B6%E5%9B%BE%E7%9A%84%E8%AE%B0%E5%BD%95/","title":"手撸好看柱状图的记录"},{"content":"Docker 笔记：常用命令整理 镜像管理 查看镜像：docker images 拉取镜像：docker pull xxx 删除镜像：docker rmi xxx 容器管理 查看容器：docker ps / docker ps -a 进入容器：docker exec -it 容器名 bash 停止容器：docker stop 容器名 删除容器：docker rm 容器名 docker-compose 常用 启动：docker-compose up -d 停止：docker-compose down 查看：docker-compose ps 拉镜像：docker-compose pull 常用组合示例 docker run -d -p 8080:80 nginx docker exec -it my-container bash docker-compose up \u0026ndash;build 清理空间 docker system prune -a 我的fre123部署流程 以阿里云为例 容器部署 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 回到 home 目录，确保路径正确 cd ~ # 克隆项目 git clone https://github.com/fre123-com/fre123-nav.git # 进入 deploy 目录（docker-compose.yml 文件所在） cd fre123-nav/deploy # 拉取镜像 sudo docker-compose pull # 启动服务 sudo docker-compose up -d 成功部署 1 2 3 4 5 6 7 [root@iZbp15al0g417943ohz4h1Z deploy]# docker-compose ps WARN[0000] /root/fre123-nav/deploy/docker-compose.yaml: version is obsolete NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS fre123-mongo mongo:3.6 \u0026#34;docker-entrypoint.s…\u0026#34; fre123-mongo 10 seconds ago Up 7 seconds 0.0.0.0:27077-\u0026gt;27017/tcp, :::27077-\u0026gt;27017/tcp fre123-nav-admin fre123dev/fre123-nav-admin:v0.1.0 \u0026#34;/bin/bash nginx_sta…\u0026#34; fre123-nav-admin 10 seconds ago Up 7 seconds 0.0.0.0:3001-\u0026gt;80/tcp, :::3001-\u0026gt;80/tcp fre123-nav-api fre123dev/fre123-nav-api:v0.1.0 \u0026#34;pipenv run api pro\u0026#34; fre123-nav-api 10 seconds ago Up 6 seconds 0.0.0.0:8765-\u0026gt;8765/tcp, :::8765-\u0026gt;8765/tcp fre123-nav-web fre123dev/fre123-nav-web:v0.1.0 \u0026#34;/bin/sh start.sh\u0026#34; fre123-nav-web 10 seconds ago Up 7 seconds 0.0.0.0:3000-\u0026gt;3000/tcp, :::3000-\u0026gt;3000/tcp 下一步：浏览器访问 你可以现在在浏览器访问以下地址：\n1 2 http://\u0026lt;你的阿里云服务器公网IP\u0026gt;:3000 # 主站前端 http://\u0026lt;你的阿里云服务器公网IP\u0026gt;:3001 # 管理后台 注意：确保端口已在阿里云安全组开放 如果访问不了，可能是安全组没放行这些端口： 3000（前端） 3001（管理后台） 8765（后端 API，如果你需要访问） 27077（MongoDB 端口，通常不建议暴露） 你可以登录 阿里云控制台 → 安全组 找到对应实例的安全组，手动添加规则（其他服务器同理） 服务器防火墙设置 除了要在阿里云开放安全组，你还需要设置服务器本地防火墙开放，具体操作步骤可查看初步接触Nginx的文章 🐳 Nuxt 博客项目从 Windows 上传到阿里云服务器并用 Docker 部署上线教程 本教程适用于：在 Windows 本地开发 Nuxt 3 项目，在阿里云服务器（Linux）上通过 Docker 打包、运行并对外提供服务。\n前置条件 本地开发环境（Windows） 上已有一个可正常构建的 Nuxt 3 项目。 阿里云服务器（推荐 CentOS 或 Ubuntu）已安装 Docker（无需本地安装 Docker）。 拥有服务器的 SSH 登录权限（FinalShell、XShell、PuTTY 等工具）。 （可选）已注册 Docker Hub 账号，用于推送镜像分享给他人。 将项目上传到阿里云服务器 方法一：FinalShell 右键上传 打开 FinalShell，连接到你的阿里云服务器。 在左侧文件管理器中定位到本地项目文件夹。 右键选中项目根目录，选择 \u0026ldquo;上传到服务器\u0026rdquo;，目标路径可选 /root/nuxt-blog。 方法二：命令行 scp 1 2 # 在 Windows PowerShell 或 Git Bash 中运行： scp -r C:/path/to/your/nuxt-blog root@\u0026lt;服务器IP\u0026gt;:/root/nuxt-blog 在服务器上编写 Dockerfile SSH 登录服务器：\n1 ssh root@\u0026lt;服务器IP\u0026gt; 进入项目目录：\n1 cd /root/nuxt-blog 创建并编辑 Dockerfile（适用于Nuxt3）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 # ---------- 构建阶段 ---------- FROM node:18 AS builder # 设置工作目录 WORKDIR /app # 增加 Node.js 可用内存（防止内存不足） ENV NODE_OPTIONS=\u0026#34;--max-old-space-size=3072\u0026#34; # 拷贝依赖配置并安装 COPY package.json yarn.lock ./ RUN yarn install # 拷贝全部项目源码 COPY . . # 避免 nuxt 权限问题 RUN chmod +x node_modules/.bin/nuxt # 构建项目 RUN node_modules/.bin/nuxt build # ---------- 运行阶段 ---------- FROM node:18 AS runner # 设置工作目录 WORKDIR /app # 仅复制必要文件，减小镜像体积 COPY --from=builder /app/.output .output COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./ # Nuxt 默认端口 EXPOSE 3000 # 启动服务 CMD [\u0026#34;node\u0026#34;, \u0026#34;.output/server/index.mjs\u0026#34;] 构建 Docker 镜像 在项目根目录执行：\n1 2 # \u0026lt;用户名\u0026gt; 换成你的 Docker Hub 用户名或任意标识 docker build -t \u0026lt;用户名\u0026gt;/nuxt-blog:v1.0 . -t：为镜像打标签，格式为 仓库名:版本。 .：表示当前目录的 Dockerfile。 运行 Docker 容器 1 2 3 4 docker run -d \\ -p 3000:3000 \\ --name nuxt-blog-container \\ \u0026lt;用户名\u0026gt;/nuxt-blog:v1.0 -d：后台运行容器。 -p 3000:3000：映射宿主机 3000 端口到容器 3000 端口。 --name：指定容器名称，方便管理。 测试访问：打开浏览器，访问 http://\u0026lt;服务器IP\u0026gt;:3000，能正常看到 Nuxt 页面即部署成功。\n（可选）推送镜像到 Docker Hub 登录：\n1 docker login 推送：\n1 docker push \u0026lt;用户名\u0026gt;/nuxt-blog:v1.0 之后其他人只需：\n1 2 docker pull \u0026lt;用户名\u0026gt;/nuxt-blog:v1.0 docker run -d -p 3000:3000 \u0026lt;用户名\u0026gt;/nuxt-blog:v1.0 即可一键运行。\n（可选）用 Nginx 反向代理统一入口 若你希望通过域名（如 blog.linsou.asia）访问，并隐藏端口，可在服务器上安装并配置 Nginx：\n安装 Nginx：\n1 2 sudo yum install -y nginx # CentOS # 或 sudo apt install -y nginx # Ubuntu 新建配置 /etc/nginx/conf.d/nuxt-blog.conf：\n1 2 3 4 5 6 7 8 9 10 server { listen 80; server_name blog.linsou.asia; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } 校验并重载：\n1 2 sudo nginx -t sudo systemctl reload nginx 访问 http://blog.linsou.asia 即可直达 Nuxt 应用。\n恭喜你！🎉 通过本教程，你已掌握从 Windows 传输项目、在 Linux 服务器上打包 Docker 镜像并部署 Nuxt 应用的全流程。\n","date":"2025-06-03T00:58:55+08:00","permalink":"https://linsk27.github.io/p/0603-%E5%88%9D%E6%AD%A5%E6%8E%A5%E8%A7%A6docker/","title":"初步接触Docker"},{"content":" 初步接触 Nginx 什么是 Nginx？ Nginx（engine x） 是一个高性能的 HTTP 和反向代理服务器，同时也是一个 IMAP/POP3/SMTP 邮件代理服务器。\n由 Igor Sysoev 编写，最初用于俄罗斯访问量最大的网站之一。如今，它广泛应用于各类网站的：\n静态资源服务（如 HTML、CSS、JS、图片等） 反向代理（把请求转发到后端服务） 负载均衡（分发请求以提升性能与可用性） HTTPS / SSL 支持 动态请求的缓存与压缩处理 为什么使用 Nginx？ Nginx 被广泛采用的原因包括：\n🚀 高性能：可以支撑成千上万个并发连接 ⚙️ 资源占用小：内存占用远低于 Apache 🔁 反向代理 + 负载均衡：非常适合部署在微服务架构中 🔒 支持 HTTPS / SSL 🔧 模块化配置：配置灵活，易于扩展 安装 Nginx 1. 安装依赖 1 2 3 4 yum install gcc gcc-c++ yum install -y pcre pcre-devel yum install -y zlib zlib-devel yum install -y openssl openssl-devel 2. 下载并解压 Nginx 1 2 3 4 cd /usr/local wget http://nginx.org/download/nginx-1.22.1.tar.gz tar -zxvf nginx-1.22.1.tar.gz cd nginx-1.22.1 3. 编译安装 1 2 3 ./configure --with-http_ssl_module # 添加 SSL 模块支持 make # 编译 make install # 安装 🚀 常用命令整理 在 /usr/local/nginx/sbin 目录下运行：\n1 2 3 ./nginx # 启动 Nginx ./nginx -s reload # 热重载配置 ./nginx -s stop # 停止 Nginx 检查是否启动成功 1 ps -ef | grep nginx 输出示例：\n1 2 root 706455 1 0 12:34 ? nginx: master process ./nginx nobody 706456 ... 0 12:34 ? nginx: worker process 判断启动状态：\n✔️ 正常：出现 master process 和 worker process ❌ 异常：只有 master 没有 worker，说明配置有误 ⚠️ 多个进程：说明重复启动了，建议先执行 pkill nginx 配置与测试 📄 配置文件示例（nginx.conf） 路径：/usr/local/nginx/conf/nginx.conf\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 # 定义工作进程数量，通常设置为 CPU 核心数，这里示例用1 worker_processes 1; events { # 每个工作进程允许的最大连接数（含客户端和服务器端连接） worker_connections 1024; } http { # 引入 MIME 类型映射文件，用来识别文件扩展名对应的内容类型 include mime.types; # 默认的 MIME 类型，文件类型未匹配时使用 default_type application/octet-stream; # 启用高效文件传输方式，减少系统调用次数 sendfile on; # 连接保持活动的超时时间（秒），超过此时间关闭连接 keepalive_timeout 65; server { # 监听端口8080，HTTP服务默认端口是80，这里改为8080示例 listen 8080; # 服务器名称，支持域名或IP，这里用localhost作为本机测试 server_name localhost; location / { # 网站根目录，Nginx 会从这里查找文件 root /usr/local/nginx/html/dist; # 默认首页文件，访问目录时默认加载的文件 index index.html index.htm; } # 定义错误页面，当出现500、502、503、504错误时，跳转到该页面 error_page 500 502 503 504 /50x.html; } } 测试是否成功 1 curl http://\u0026lt;你的公网IP\u0026gt;:8080 如果返回页面内容，说明 Nginx 成功响应 否则检查防火墙 / 端口是否开放 ⚠️ 注意事项 监听端口为 8080（非默认 80），需特别注意防火墙设置 如果你使用的是 阿里云服务器，请在控制台： 找到安全组 → 编辑入方向规则 → 开放 8080 端口 即使云服务器厂商（如阿里云、腾讯云等）的“安全组”已经开放端口，如果 服务器操作系统内部 还有防火墙规则没有放行对应端口，也会导致无法访问。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 systemctl status firewalld # 若正在运行，执行查看端口开放情况 firewall-cmd --list-all # 若未开放指定端口，可执行 firewall-cmd --permanent --add-port=8080/tcp firewall-cmd --reload # 再次执行查看端口开放情况 firewall-cmd --list-all ##实操 [root@iZbp15al0g417943ohz4h1Z ~]# firewall-cmd --list-all public (active) target: default icmp-block-inversion: no interfaces: eth0 sources: services: cockpit dhcpv6-client ssh ports: 20/tcp 21/tcp 22/tcp 80/tcp 443/tcp 8888/tcp 39000-40000/tcp 9999/tcp 8080/tcp # 没有8989端口 protocols: forward: no masquerade: no forward-ports: source-ports: icmp-blocks: rich rules: [root@iZbp15al0g417943ohz4h1Z ~]# firewall-cmd --permanent --add-port=8989/tcp success [root@iZbp15al0g417943ohz4h1Z ~]# firewall-cmd --reload ^[[Asuccess [root@iZbp15al0g417943ohz4h1Z ~]# firewall-cmd --list-all public (active) target: default icmp-block-inversion: no interfaces: eth0 sources: services: cockpit dhcpv6-client ssh ports: 20/tcp 21/tcp 22/tcp 80/tcp 443/tcp 8888/tcp 39000-40000/tcp 9999/tcp 8080/tcp 8989/tcp protocols: forward: no masquerade: no forward-ports: source-ports: icmp-blocks: rich rules: 若你更喜欢默认 HTTP 端口 80，可将 listen 8080 改为 listen 80 Nginx 配置 lindablog.xyz 域名启用 HTTPS 一、准备条件 部署项目文件位置：/usr/local/nginx/html/dist 阿里云申请 SSL 凭证档案（我申请的是免费证书3月一换）： jnalindablog.xyz.pem（凭证） jnalindablog.xyz.key（私钥） 二、建立 SSL 凭证目录并上传ssl凭证档案 1 mkdir -p /usr/local/nginx/ssl 三、撰写 Nginx HTTPS 配置档 在 /usr/local/nginx/conf/nginx.conf 中加入：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; # 加入這一行：引用你自定義的 server 配置檔案 include /usr/local/nginx/conf/conf.d/*.conf; } 在 /usr/local/nginx/conf/conf.d/lindablog.conf 中加入：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 server { # 监听 443 端口并启用 SSL/TLS，提供 HTTPS 服务 listen 443 ssl; # 服务器域名，配置成你的网站域名 server_name lindablog.xyz; # SSL 证书文件路径，公钥证书 ssl_certificate /usr/local/nginx/ssl/jnalindablog.xyz.pem; # SSL 私钥文件路径，和证书匹配的私钥 ssl_certificate_key /usr/local/nginx/ssl/jnalindablog.xyz.key; # 允许使用的 TLS 协议版本，推荐启用 1.2 和 1.3 ssl_protocols TLSv1.2 TLSv1.3; # 加密套件，排除掉弱加密算法，确保连接安全 ssl_ciphers HIGH:!aNULL:!MD5; # 网站根目录，存放前端静态文件 root /usr/local/nginx/html/dist; # 默认首页文件，访问根路径时默认加载 index index.html; location / { # try_files 尝试访问请求的 URI，如果不存在，则返回 index.html # 主要用于 SPA（单页应用）支持路由刷新 try_files $uri $uri/ /index.html; } } server { # 监听 HTTP 的 80 端口，用于重定向所有请求到 HTTPS listen 80; # 服务器域名 server_name lindablog.xyz; # 所有访问 80 端口的请求，都重定向（301永久重定向）到对应的 HTTPS 地址 return 301 https://$host$request_uri; } 四、测试与重载 Nginx 1 2 /usr/local/nginx/sbin/nginx -t /usr/local/nginx/sbin/nginx -s reload 五、验证 HTTPS 启用 浏览器访问 https://lindablog.xyz，网址列显示锁头符号 🔒 点击锁头查看凭证细节或者右键检查找到隐私与安全 使用线上工具检查： SSL Labs 测试 SSL Shopper 检查 备注 .pem 与 .crt 可通用于 Nginx ","date":"2025-04-23T12:25:57+08:00","permalink":"https://linsk27.github.io/p/0423-%E5%88%9D%E6%AD%A5%E6%8E%A5%E8%A7%A6nginx/","title":"初步接触Nginx"},{"content":"什么是Echarts ECharts（全称：Enterprise Charts）是由百度开源的一个基于 JavaScript 的可视化图表库，专门用于构建交互性强、表现丰富的数据可视化图表。\n它能做什么？ ECharts 提供了多种常用和高级图表类型，适用于数据分析、仪表盘、报表系统、可视化大屏等场景，例如：\n折线图（line） 柱状图（bar） 饼图（pie） 散点图（scatter） 地图（map，支持中国地图和世界地图） 仪表盘（gauge） 热力图（heatmap） 自定义图形（graph、tree、sunburst 等） 它的核心特点： 特性 说明 高性能 基于 Canvas 渲染，适合大数据量可视化 响应式 能适应不同屏幕尺寸，自适应缩放 交互性强 支持 tooltip、点击事件、缩放、图例控制等 主题丰富 提供多种主题，也可自定义 开源免费 使用 Apache-2.0 协议，商业项目也可免费用 安装方式（前端项目）： 如果你用的是 Nuxt、Vue、React 等框架，常见引入方式有两种：\nnpm/yarn 安装： 1 2 3 yarn add echarts # 或者 npm install echarts 直接在 HTML 中引用（CDN）： 1 \u0026lt;script src=\u0026#34;https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 其他类似图表库 虽然 ECharts 很强大，但近几年市面上也涌现了不少其它优秀的图表库，例如：\nAntV/G2 系列：蚂蚁金服出品，视觉效果更现代，支持更多数据关系分析，且生态体系逐步完善。\nD3.js：功能灵活，适合自定义程度非常高的可视化开发，不过上手难度和开发成本较高。\nHighcharts、Chart.js：Highcharts 商业授权较为严格，而 Chart.js 则适合中小型数据量和基本交互需求。\n如何写一个图表 图表步骤 初始化 echarts 实例\n1 2 3 4 let myChart = null onMounted(()=\u0026gt;{ myChart = echarts.init(target.value) }) 构建 option 配置对象\n1 2 const options = { } 通过 实例.setOption(option) 挂载到对应盒子上\n1 myChart.setOption(options) 图表配置 柱形图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 const options = { // X轴展示数据 xAxis:{ show: false, // x方向作为值展示 type: \u0026#39;value\u0026#39;, // 设置最大值的配置 max: function (value) { // 动态最大值设置为所有数据中的最大值1.2倍，防止样式问题 return parseInt(value.max * 1.2) } }, // y轴展示数据 yAxis:{ // 作为列名展示 type: \u0026#39;category\u0026#39;, // 列名数组，默认从下到上 data: props.data.regions?.map((item) =\u0026gt; item.name), // 反向展示 inverse: true, // 不展示线 axisLine: { show: false }, // 不展示刻度 axisTick: { show: false }, // 展示列名并且label颜色设置 axisLabel: { color: \u0026#39;#9eb1cd\u0026#39; }, }, // 图表绘制的位置，对应上下左右 grid:{ top: 0, right: 0, left: 0, bottom: 0, // 标签包含进去 containLabel: true }, // 核心配置 series:[ { // 柱形图 type: \u0026#39;bar\u0026#39;, // 展示的数据 data: props.data.regions?.map((item) =\u0026gt; ({ name: item.name, value: item.value })), // 柱状条背景开关 showBackground: true, // 柱状条背景色 backGroundStyle: { color: \u0026#39;rgba(180, 180, 180, 0.2)\u0026#39; }, // 每个轴的样式 itemStyle: { color: \u0026#39;#5d98CE\u0026#39;, barBorderRadius: 10, shadowColor: \u0026#39;rgba(0,0,0,0.3)\u0026#39;, shadowBlur: 5, }, // 柱子宽度 barWidth: 12, // 字体的显示 label: { show: true, position: \u0026#39;right\u0026#39;, textStyle: { color: \u0026#39;#fff\u0026#39;, } // 为数据加上百分比，c代表数据 formatter:\u0026#39;{c}%\u0026#39; } } ], } 雷达图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 const option = { // 雷达坐标系配置 radar: { // 坐标系（外部）名字配置 name: { textStyle: { color: \u0026#39;#05D5FF\u0026#39;, fontSize: 14 } }, // 雷达图形状 shape: \u0026#39;polygon\u0026#39;, center: [\u0026#39;50%\u0026#39;, \u0026#39;50%\u0026#39;], radius: \u0026#39;80%\u0026#39;, // 起始位置(开始角度) startAngle: 120, // 轴线配置 axisLine: { lineStyle: { color: \u0026#39;rgba(5,213,255,0.8)\u0026#39; } }, // 网格线，为true才能形成闭环 splitLine: { show: true, lineStyle: { width: 1, color: \u0026#39;rgba(5,213,255,0.8)\u0026#39; } }, // 指示器,就是name部分的坐标系（外部）名字 indicator: props.data.risks.map((item) =\u0026gt; ({ name: item.name, max: 100 })), // 拆分区域，false网格内颜色区分更明显 splitArea: { show: false } }, // 坐标极点 polar: { center: [\u0026#39;50%\u0026#39;, \u0026#39;50%\u0026#39;], radius: \u0026#39;0%\u0026#39; }, // 坐标角度 angleAxis: { min: 0, // 分割线分割间隔 interval: 5, // 刻度显示是否为逆时针增长, clockwise: false }, // 径向轴 radiusAxis: { min: 0, interval: 20, splitLine: { show: false } }, // 图表核心配置 series: [ { type: \u0026#39;radar\u0026#39;, // 指定形状 symbol: \u0026#39;circle\u0026#39;, // 内部数据拐角大小 symbolSize: 10, itemStyle: { normal: { color: \u0026#39;#05D5FF\u0026#39; } }, areaStyle: { normal: { color: \u0026#39;#05D5FF\u0026#39;, opacity: 0.5 } }, lineStyle: { width: 2, color: \u0026#39;#\u0026#39; }, label: { normal: { // 内部数据文本 show: true, color: \u0026#39;#fff\u0026#39; } }, data: [ { value: props.data.risks.map((item) =\u0026gt; item.value) } ] } ] } 环形图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 const getSeriesData = () =\u0026gt; { const series = [] props.data.abnormals.forEach((item, index) =\u0026gt; { // 上层 series.push({ name: item.name, type: \u0026#39;pie\u0026#39;, // 逆时针排列 clockWise: false, // 取消悬浮动画 hoverAnimation: false, // 实现半径依次递减 radius: [73 - index * 15 + \u0026#39;%\u0026#39;, 68 - index * 15 + \u0026#39;%\u0026#39;], center: [\u0026#39;50%\u0026#39;, \u0026#39;50%\u0026#39;], label: { show: false }, data: [ { value: item.value, name: item.name }, { value: 1000, itemStyle: { color: \u0026#39;rgba(0,0,0,0)\u0026#39;, borderWidth: 0, }, tooltip: { show: false }, hoverAnimation: false } ] }) // 底层 series.push({ name: item.name, type: \u0026#39;pie\u0026#39;, // 不希望收到任何事件 silent: true, z: 1, // 逆时针排列 clockWise: false, // 取消悬浮动画 hoverAnimation: false, // 实现半径依次递减 radius: [73 - index * 15 + \u0026#39;%\u0026#39;, 68 - index * 15 + \u0026#39;%\u0026#39;], center: [\u0026#39;50%\u0026#39;, \u0026#39;50%\u0026#39;], label: { show: false }, data: [ { value: 7.5, name: item.name, itemStyle: { color: \u0026#39;rgba(3,31,62)\u0026#39;, borderWidth: 0, }, tooltip: { show: false, }, hoverAnimation: false }, { value: 2.5, itemStyle: { color: \u0026#39;rgba(0,0,0,0)\u0026#39;, borderWidth: 0, }, tooltip: { show: false, }, hoverAnimation: false } ] }) }); return series } const option = { // 图例配置 legend: { show: true, // 图例颜色块形状 icon: \u0026#39;circle\u0026#39;, top: \u0026#39;8%\u0026#39;, left: \u0026#39;52%\u0026#39;, // 图例数据文本 data: props.data.abnormals.map((item) =\u0026gt; item.name), // 负数以列展示 width: -5, // 色块宽高 itemWidth: 10, itemHeight: 10, // 图例间距 itemGap: 6, textStyle: { fontSize: 12, lineHeight: 5, color: \u0026#39;#fff\u0026#39; } }, // 悬浮提示层 tooltip: { show: true, // 触发器 trigger: \u0026#39;item\u0026#39;, // 展示内容,a代表系列名，b代表数据名，c代表数据值,d代表百分比比例 formatter: \u0026#39;{a}\u0026lt;br\u0026gt;{b}:{c}({d}%)\u0026#39; }, yAxis: [ { type: \u0026#39;category\u0026#39;, // 反向展示 inverse: true, axisLine: { show: false } } ], xAxis: [ { show: false } ], // 由于饼图过多，通过方法进行配置 series: getSeriesData() } 关系图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 const option = { xAxis: { show: false, type: \u0026#39;value\u0026#39; }, yAxis: { show: false, type: \u0026#39;value\u0026#39; }, series: [ { type: \u0026#39;graph\u0026#39;, // 表示不需要任何布局类型 layout: \u0026#39;none\u0026#39;, // 该系列需要使用的坐标系：“二维直角坐标系” coordinateSystem: \u0026#39;cartesian2d\u0026#39;, // 节点大小 symbolSize: 26, z: 3, // 边界的萧条标签文字 edgeLabel: { // 默认 normal: { show: true, color: \u0026#39;#fff\u0026#39;, testStyle: { fontSize: 14 }, // 文本模拟内容 formatter: function (params) { return params.data.speed } } }, label: { normal: { show: true, position: \u0026#39;bottom\u0026#39;, color: \u0026#39;#5E5E5E\u0026#39; } }, // 边两端的标记类型,数据流动图标类型 edgeSymbol: [\u0026#39;none\u0026#39;, \u0026#39;arrow\u0026#39;], edgeSymbolSize: 8, data: props.data.relations.map(item =\u0026gt; { if (item.id !== 0) { return { name: item.name, category: 0, active: true, speed: `${item.speed}kb/s`, value: item.value, } } else { return { name: item.name, value: item.value, symbolSize: 100, itemStyle: { color: { colorStops: [ { offset: 0, color: \u0026#39;#157eff\u0026#39; }, { offset: 1, color: \u0026#39;#35c2ff\u0026#39; }, ] } }, label: { normal: { fontSize: 14 } } } } }), // 节点间的数据关系 links: props.data.relations.map((item, index) =\u0026gt; ({ source: item.source, target: item.target, speed: `${item.speed}kb/s`, lineStyle: { normal: { color: \u0026#39;#12b5d0\u0026#39;, // 曲线率 curveness: 0.2 } }, label: { show: true, position: \u0026#39;middle\u0026#39;, // 是否对文字进行偏移 offset: [10, 0] } })) }, { type: \u0026#39;lines\u0026#39;, // 该系列需要使用的坐标系：“二维直角坐标系” coordinateSystem: \u0026#39;cartesian2d\u0026#39;, z: 1, // 线条的特效配置 effect: { show: true, smooth: false, trailLength: 0, symbol: \u0026#39;arrow\u0026#39;, color: \u0026#39;rgba(55,155,255,0.6)\u0026#39;, symbolSize: 12 }, lineStyle: { normal: { curveness: 0.2 } }, data: [ [{ coord: [0, 300] }, { coord: [50, 200] }], [{ coord: [0, 100] }, { coord: [50, 200] }], [{ coord: [50, 200] }, { coord: [100, 100] }], [{ coord: [50, 200] }, { coord: [100, 300] }] ] } ] } 词云图 需要安装依赖后导入import(\u0026quot;echarts-wordcloud\u0026quot;)\n注意：借助浏览器的插件在Nuxt框架下需要判断让它在客户端导入，不然会报错！\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const option ={ series: [ { type: \u0026#39;wordCloud\u0026#39;, // 文字大小范围 sizeRange: [8, 46], // 文字旋转 rotationRange: [0, 0], gridSize: 0, layoutAnimation: true, textStyle: { color: randomRGB }, // 高亮字体 emphasis: { textStyle: { fontWeight: \u0026#39;bold\u0026#39;, color: \u0026#39;#000\u0026#39; } }, data: props.data.datas } ] } 封装composable用于操作echarts生成 按需加载插件，节省初始包体积。 避免 SSR 报错（import 仅在客户端执行）。 onMounted：监听 resize 事件，onBeforeUnmount：解除 resize 监听，释放图表实例 销毁 ECharts 实例：释放资源，避免内存不断积累。 正确时机：组件即将卸载时执行，最适合做 cleanup。 markRaw 处理 chartInstance，避免将 ECharts 实例做响应式包裹，提升性能，避免不必要的跟踪。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 // 导入 echarts 主库 import * as echarts from \u0026#34;echarts\u0026#34; import { markRaw } from \u0026#39;vue\u0026#39; // 添加这句 // 声明 process.client（为了让 TypeScript 不报错） // 在 Nuxt 中，process.client 表示是否处于客户端运行环境 declare const process: { client: boolean; } // 导出一个 composable，用于封装 echarts 的初始化和操作逻辑 export const useEcharts = () =\u0026gt; { // 图表容器的 DOM 引用（挂载到页面上的 \u0026lt;div ref=\u0026#34;chartRef\u0026#34;\u0026gt;） const chartRef = ref\u0026lt;HTMLElement | null\u0026gt;(null) // 图表实例对象（echarts.init 的返回值） let chartInstance: echarts.ECharts | null = null // 初始化图表，支持普通图表或词云（wordcloud） const initChart = async (type?: \u0026#34;wordcloud\u0026#34;) =\u0026gt; { if (!chartRef.value) return // 容器还没挂载，跳过 // 如果是词云图表，并且是在客户端环境下，动态加载 echarts-wordcloud 插件 if (type === \u0026#34;wordcloud\u0026#34; \u0026amp;\u0026amp; process.client) { await import(\u0026#34;echarts-wordcloud\u0026#34;) } // 初始化 echarts 实例，绑定到 DOM 容器上, 使用 markRaw 避免响应式干扰 chartInstance = markRaw(echarts.init(chartRef.value)) } // 设置图表的配置项（option） const setOptions = (options: any) =\u0026gt; { chartInstance?.setOption(options) } // 响应窗口大小变化，重置图表大小 const resize = () =\u0026gt; { chartInstance?.resize() } // 组件挂载时绑定 resize 事件监听 onMounted(() =\u0026gt; { if (process.client) { window.addEventListener(\u0026#34;resize\u0026#34;, resize) } }) // 组件销毁前解绑事件监听，并销毁 echarts 实例释放资源 onBeforeUnmount(() =\u0026gt; { if (process.client) { window.removeEventListener(\u0026#34;resize\u0026#34;, resize) chartInstance?.dispose() } }) // 返回方法和 DOM 引用，供组件内使用 return { chartRef, // DOM 引用，组件中通过 ref=\u0026#34;chartRef\u0026#34; 绑定 initChart, // 初始化图表方法 setOptions, // 设置图表配置项方法 resize // 手动触发图表 resize } } 用nuxt3写echarts开发注意事项 SSR渲染问题 图表组件依赖 DOM 和浏览器环境，不能 SSR，用 \u0026lt;ClientOnly\u0026gt; 包裹图表部分，完美规避服务端渲染的问题。 ","date":"2025-04-08T17:44:12+08:00","permalink":"https://linsk27.github.io/p/0408-%E5%88%9D%E6%AD%A5%E6%8E%A5%E8%A7%A6%E5%8F%AF%E8%A7%86%E5%8C%96echarts/","title":"初步接触可视化Echarts"},{"content":"关键概念 导入功能的提升 Components 文件夹组件自动导入 在 Nuxt 中，你可以在 components/ 目录中创建这些组件，它们将自动在整个应用程序中可用，无需显式地导入。 Auto-imports 自动导入(响应式 API、生命周期) 1 2 3 4 5 \u0026lt;script setup lang=\u0026#34;ts\u0026#34;\u0026gt; /* ref() and computed() are auto-imported */ const count = ref(1) const double = computed(() =\u0026gt; count.value * 2) \u0026lt;/script\u0026gt; Built-in Auto-imports 内置自动导入 1 2 3 \u0026lt;script setup lang=\u0026#34;ts\u0026#34;\u0026gt; /* useFetch() is auto-imported */ const {(data, refresh, status)} = await useFetch(\u0026#39;/api/hello\u0026#39;) \u0026lt;/script\u0026gt; Vue.js 路由 1. 通过pages/每个组件生成路由 1 2 3 4 5 -| pages/ ---| about.vue ---| index.vue ---| posts/ -----| [id].vue 2. \u0026lt;NuxtLink\u0026gt;标签进行路由跳转 1 2 3 4 5 6 7 8 9 10 11 \u0026lt;template\u0026gt; \u0026lt;header\u0026gt; \u0026lt;nav\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt;\u0026lt;NuxtLink to=\u0026#34;/about\u0026#34;\u0026gt;About\u0026lt;/NuxtLink\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;NuxtLink to=\u0026#34;/posts/1\u0026#34;\u0026gt;Post 1\u0026lt;/NuxtLink\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;NuxtLink to=\u0026#34;/posts/2\u0026#34;\u0026gt;Post 2\u0026lt;/NuxtLink\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/nav\u0026gt; \u0026lt;/header\u0026gt; \u0026lt;/template\u0026gt; 3. Route Parameters 路由参数 1 2 3 4 \u0026lt;script setup lang=\u0026#34;ts\u0026#34;\u0026gt; const route = useRoute() // When accessing /posts/1, route.params.id will be 1 console.log(route.params.id) \u0026lt;/script\u0026gt; 4. Route Middleware 路由中间件 导航到特定路由之前提取要运行的代码 路由中间件在 Nuxt 应用程序的 Vue 部分运行。尽管名称相似，但它们与服务器中间件完全不同，后者在应用程序的 Nitro 服务器部分运行。 1 2 3 4 5 6 export default defineNuxtRouteMiddleware((to, from) =\u0026gt; { // isAuthenticated() is an example method verifying if a user is authenticated if (isAuthenticated() === false) { return navigateTo(\u0026#34;/login\u0026#34;); } }); 1 2 3 4 5 6 7 8 9 \u0026lt;script setup lang=\u0026#34;ts\u0026#34;\u0026gt; definePageMeta({ middleware: \u0026#34;auth\u0026#34;, }); \u0026lt;/script\u0026gt; \u0026lt;template\u0026gt; \u0026lt;h1\u0026gt;Welcome to your dashboard\u0026lt;/h1\u0026gt; \u0026lt;/template\u0026gt; 5. Route Validation 路由验证 Nuxt 通过你想要验证的每个页面中 definePageMeta（）中的 validate 属性提供路由验证。 validate 属性接受路由作为参数。您可以返回一个布尔值，以确定这是否是要使用此页面呈现的有效路由。如果返回 false，但找不到其他匹配项，则会导致 404 错误。您也可以直接返回带有 statusCode/statusMessage 的对象，以立即响应错误（不会检查其他匹配项）。 1 2 3 4 5 6 7 8 \u0026lt;script setup lang=\u0026#34;ts\u0026#34;\u0026gt; definePageMeta({ validate: async (route) =\u0026gt; { // Check if the id is made up of digits return typeof route.params.id === \u0026#39;string\u0026#39; \u0026amp;\u0026amp; /^\\d+$/.test(route.params.id) } }) \u0026lt;/script\u0026gt; Rendering Modes 渲染模式 浏览器和服务器都可以解释 JavaScript 代码，以将 Vue.js 组件转换为 HTML 元素。此步骤称为 渲染 。 默认情况下，Nuxt 使用通用渲染来提供更好的用户体验、性能并优化搜索引擎索引，但您可以在一行配置中切换渲染模式。 关于服务端渲染和客户端渲染的一个bug，使用setInterval计时器,服务端报错500 1 2 3 setInterval(() =\u0026gt; { request() }, 3000); 1. 问题分析 当你在 Nuxt 的组件或页面中直接使用 setInterval 时，这段代码会在服务端渲染阶段被执行。然而，setInterval 是浏览器环境中的 API，在服务端（Node.js）环境中会报错，提示类似以下内容：\n1 ReferenceError: setInterval is not defined 2. 解决方案 使用 process.client 判断是否在客户端运行\nnuxt 提供了 process.client 变量，用于判断当前代码是否在浏览器环境中运行。你可以通过这个变量确保 setInterval 只在客户端执行。\n1 2 3 4 5 6 7 8 9 10 11 // 确保 setInterval 只在客户端运行 if (process.client) { const intervalId = setInterval(() =\u0026gt; { loadData() }, 3000) // 组件销毁时清除计时器，防止内存泄漏 onUnmounted(() =\u0026gt; { clearInterval(intervalId) }) } 将计时器逻辑放在 mounted 生命周期钩子中\n在 Vue.js 中，mounted 钩子只会在客户端渲染完成后执行，因此可以将计时器逻辑放在这里。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export default { mounted() { this.interval = setInterval(() =\u0026gt; { this.loadData(); }, 3000); }, beforeDestroy() { // 清除计时器，防止内存泄漏 clearInterval(this.interval); }, methods: { loadData() { console.log(\u0026#39;加载数据...\u0026#39;); } } }; 什么是服务器渲染的，什么是客户端渲染的？ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;script setup lang=\u0026#34;ts\u0026#34;\u0026gt; const counter = ref(0); // executes in server and client environments const handleClick = () =\u0026gt; { counter.value++; // executes only in a client environment }; \u0026lt;/script\u0026gt; \u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;p\u0026gt;Count: {{ counter }}\u0026lt;/p\u0026gt; \u0026lt;button @click=\u0026#34;handleClick\u0026#34;\u0026gt;Increment\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; 在初始请求中，计数器 ref 在服务器中初始化，因为它呈现在 \u0026lt;p\u0026gt;标记内。handleClick 的内容在此处永远不会执行。在浏览器中激活期间，counter ref 会重新初始化。handleClick 最终将自身绑定到按钮;因此，可以合理地推断 handleClick 的主体将始终在浏览器环境中运行。\nUniversal Rendering 通用渲染 原理 当浏览器请求启用了通用渲染的 URL 时，Nuxt 会在服务器环境中运行 JavaScript （Vue.js） 代码，并将完全渲染的 HTML 页面返回给浏览器。如果页面是提前生成的，Nuxt 也可以从缓存中返回完全渲染的 HTML 页面。用户会立即获得应用程序的全部初始内容，这与客户端渲染相反。 下载 HTML 文档后，浏览器会解释此内容，Vue.js 将控制该文档。曾经在服务器上运行的相同 JavaScript 代码再次在客户端（浏览器）上运行，现在通过将其侦听器绑定到 HTML 来启用交互性（因此是通用渲染）。 效果 通用渲染允许 Nuxt 应用程序提供快速的页面加载时间，同时保留客户端渲染的优势。此外，由于内容已经存在于 HTML 文档中，因此爬网程序可以毫无开销地为其编制索引。 通用渲染用途广泛，几乎可以适应任何用例，特别适用于任何面向内容的网站：博客、营销网站、投资组合、电子商务网站和市场。 1. 服务器端渲染（SSR） 概念:\n在 SSR 模式下，页面的 HTML 内容是在服务器端动态生成的，然后发送到客户端。客户端接收到 HTML 后，再通过 JavaScript 进行进一步的交互。 SSR 是 Nuxt 3 的默认渲染模式，适合需要 SEO 优化或需要快速首屏加载的应用。 好处\n性能：用户可以立即访问页面的内容，因为浏览器显示静态内容的速度比 JavaScript 生成的内容快得多。同时，Nuxt 在水合过程中保留了 Web 应用程序的交互性。 搜索引擎优化：通用渲染将页面的整个 HTML 内容作为经典服务器应用程序交付给浏览器。Web 爬虫可以直接为页面的内容编制索引，这使得 Universal rendering 成为您想要快速编制索引的任何内容的绝佳选择。 缺点\n开发约束：服务器和浏览器环境不提供相同的 API，因此编写可以在两端无缝运行的代码可能很棘手。幸运的是，Nuxt 提供了指南和特定变量来帮助您确定一段代码的执行位置。 成本：需要运行服务器才能动态呈现页面。这会增加每月成本，就像任何传统服务器一样。但是，由于浏览器接管了客户端导航的通用渲染，服务器调用大大减少了。利用 edge-side 渲染可以降低成本。 2. 静态站点生成（Static Site Generation, SSG） 在 SSG 模式下，页面的 HTML 是在构建时预先生成的，并作为静态文件部署到服务器。这些静态页面可以在没有服务器端逻辑的情况下直接被客户端加载。 SSG 适合内容不经常更新的网站，如博客或文档网站。 3. 单页应用（Single Page Application, SPA） 在 SPA 模式下，页面的 HTML 是在客户端动态生成的，服务器只提供一个初始的 HTML 模板和必要的 JavaScript 文件。 SPA 适合对交互性要求较高的应用，但可能对 SEO 不太友好。 Client-Side Rendering 客户端渲染 相较于通用渲染相反，不做过多叙述\n好处 开发速度：不考虑服务器的兼容性 便宜： 不考虑服务器基础设施成本 离线： 在 Internet 不可用时很好地保持工作 缺点 性能: 需等待浏览器下载、解析、运行js，响应长，影响用户体验 搜索引擎优化: 索引和更新通过客户端渲染交付的内容需要更多时间。不利于引擎爬虫。 您可以在 nuxt.config.ts中使用 Nuxt 启用仅客户端渲染： 1 2 3 export default defineNuxtConfig({ ssr: false }) :warning: 如果你确实使用 ssr： false，你还应该放置一个 HTML 文件， ~/app/spa-loading-template.html 其中包含一些你想用来渲染加载屏幕的 HTML，该屏幕将一直呈现，直到你的应用程序被激活。\nDeploying a Static Client-Rendered App部署静态客户端呈现的应用程序 Hybrid Rendering 混合渲染 Route Rules 路由规则 Edge-Side Rendering 边侧渲染 Nuxt的Server层服务 通过defineEventHandler()定义接口 server文件夹下定义接 名字channel.get.ts，默认get接口 1 2 3 4 5 6 7 // server // - /api // - channel.get.ts import channel from \u0026#34;~/database/channel\u0026#34; //channel 是database定义的默认数据 export default defineEventHandler(()=\u0026gt;{ return channel }) 使用useFetch()调用接口 1 2 3 // 无需导入 // 解构-重命名 const {data:channelList} =await useFetch(\u0026#39;/api/channel\u0026#39;) 我写过的项目的写法的记录 1 2 3 4 5 6 7 8 9 10 11 12 13 import { API_HOT_CATEGORY_LIST } from \u0026#39;~/api/category\u0026#39; import { nodeFetch } from \u0026#39;~/server/utils/handle\u0026#39; export default defineEventHandler(async (event) =\u0026gt; { const body = await readBody(event) const data = await nodeFetch(API_HOT_CATEGORY_LIST, { body, nitroEvent: event }) return { data, status: 0, info: \u0026#39;ok\u0026#39;, } }) ","date":"2025-04-06T19:18:28+08:00","permalink":"https://linsk27.github.io/p/0406-%E5%88%9D%E6%AD%A5%E6%8E%A5%E8%A7%A6nuxt3/","title":"初步接触nuxt3"},{"content":"初学Promise对象，异步基础 推荐实践代码，自己动手调试，更容易理解原理 基础 三个状态（重要） pendding 等待状态 fullfilled 完成状态 rejected 失败状态 状态流向以及回调函数resolve和reject Promise状态流向只有两种，而且状态一旦改变，则不会再变\n调用reolve函数，则 pendding =\u0026gt; fullfilled 调用rejecte函数，则pendding =\u0026gt; rejected 只接收第一个参数调用后改变状态，其他的忽略 1 2 3 4 5 6 7 const promise = new Promise((resolve, reject) =\u0026gt; { resolve(1); reject(2); // 被忽略 }) console.log(promise) // PromiseState : fullfilled // PromiseResult: 1 消费者 .then、.catch 最重要最基础的一个就是 .then\n个人见解就是then就是对Promise对象的一个总结反馈，判断对象的状态并且执行fullfilled后的函数\n.then 的第一个参数是一个函数，该函数将在 promise resolved 且接收到结果后执行。\n.then 的第二个参数也是一个函数，该函数将在 promise rejected 且接收到 error 信息后执行。\n下面例子，可自己通过注释选择resolve还是reject\n1 2 3 4 5 6 7 8 9 10 let promise = new Promise(function(resolve, reject) { // setTimeout(() =\u0026gt; resolve(\u0026#34;done!\u0026#34;), 1000); setTimeout(() =\u0026gt; reject(new Error(\u0026#34;Whoops!\u0026#34;)), 1000); }); // reject 运行 .then 中的第二个函数 promise.then( result =\u0026gt; alert(result), // 不运行 error =\u0026gt; alert(error) // 1 秒后显示 \u0026#34;Error: Whoops!\u0026#34; ); 如果我们只对某个状态的情况感兴趣，那么我们可以只为 .then 提供一个函数参数：\n1 2 3 4 5 6 7 let promise = new Promise(resolve =\u0026gt; { setTimeout(() =\u0026gt; resolve(\u0026#34;done!\u0026#34;), 1000); // setTimeout(() =\u0026gt; reject(\u0026#34;fail!\u0026#34;), 1000); }); promise.then(alert); // 1 秒后显示 \u0026#34;done!\u0026#34; // promise.catch(alert); // 1 秒后显示 \u0026#34;fail!\u0026#34; 实际情况成功的情况较为常见，并且.then和.catch也可以通过链式写法\n1 2 3 4 5 new Promise((resolve, reject) =\u0026gt; { resolve(42); }).then(value =\u0026gt; { console.log(value); // 输出: 42 }); Promise.API await+async Promise 通过链式调用（如 .then() 和 .catch()）管理异步操作的成功或失败； async 修饰的函数总会返回一个 Promise，无论返回的是普通值还是 Promise 本身； await await语句之后的代码处于微队列，用于暂停 async 函数的执行，等待后面的 Promise 解析完成，然后直接获得解析结果，从而让异步代码写起来更像同步代码，避免了冗长的 .then() 链。 await 等待的不一定是promise对象，也有可能为await 1; ==\u0026gt; await Promise.resolve(1)\nPromise.all 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Promise.all([ // 每个数组对象都是**异步微任务**，因此1和2瞬间输出 // 对象5 需要等待 对象4 await执行完毕后执行，即3秒后输出3 new Promise(resolve =\u0026gt; setTimeout(() =\u0026gt; resolve(4), 4000)), // 对象0，value4 console.log(1), // 对象1，undefined（不是promise对象） new Promise(resolve =\u0026gt; setTimeout(() =\u0026gt; resolve(2), 2000)), // 对象2，value2 console.log(2), // 对象3，undefined await new Promise(resolve =\u0026gt; setTimeout(() =\u0026gt; resolve(3), 3000)), // 对象4，value3 console.log(3), // 对象5，undefined ]).then((res) =\u0026gt; { console.log(res); }) // 4秒后所有异步任务执行完毕，输出 [ 0:1, 1:undefined, 2:2, 3:undefined, 4:3, 5:undefined ] 全部完成才会运行.then，如果有某一个失败，则不执行.then\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Promise.allSettled([ new Promise(resolve =\u0026gt; setTimeout(() =\u0026gt; resolve(4), 4000)), console.log(1), new Promise((resolve,reject) =\u0026gt; setTimeout(() =\u0026gt; reject(2), 2000)), // 这里使用reject console.log(2), await new Promise(resolve =\u0026gt; setTimeout(() =\u0026gt; resolve(3), 3000)), console.log(3), ]).then((res) =\u0026gt; { console.log(res); }) [ 0:1, 1:undefined, 2:2, 3:undefined, 4:3, 5:undefined ] Promise.allsettled 如果任意的 promise reject，则 Promise.all 整个将会 reject。当我们需要 所有 结果都成功时，它对这种“全有或全无”的情况很有用\nPromise.allSettled 等待所有的 promise 都被 settle，无论结果如何。结果数组会是这样的：\n对成功的响应，结果数组对应元素的内容为 {status:\u0026quot;fulfilled\u0026quot;, value:result}， 对出现 error 的响应，结果数组对应元素的内容为 {status:\u0026quot;rejected\u0026quot;, reason:error}。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Promise.allsettled([ new Promise(resolve =\u0026gt; setTimeout(() =\u0026gt; resolve(4), 4000)), console.log(1), new Promise((resolve,reject) =\u0026gt; setTimeout(() =\u0026gt; reject(2), 2000)), // 这里使用reject console.log(2), await new Promise(resolve =\u0026gt; setTimeout(() =\u0026gt; resolve(3), 3000)), console.log(3), ]).then((res) =\u0026gt; { console.log(res); }) // 控制台输出 [ 0:{status:fullfiled,value:4}, 1:{status:fullfiled,value:undefined}, 2:{status:rejected,reson:2}, 3:{status:fullfiled,value:undefined}, 4:{status:fullfiled,value:3}, 5:{status:fullfiled,value:undefined}, ] 参考文献 js小红书\nECMAScript 6 入门\n一道让我失眠的 Promise 面试题\n","date":"2025-03-27T22:33:37+08:00","permalink":"https://linsk27.github.io/p/0327-%E5%88%9D%E5%AD%A6promise%E5%AF%B9%E8%B1%A1/","title":"初学Promise对象"},{"content":"XMLHttpRequest、Axios 与 Fetch 在 Vue3 中的深度解析 一、XMLHttpRequest (XHR) 核心特性 历史地位：Web 2.0时代的核心技术，实现AJAX异步请求 兼容性：支持IE6+等老旧浏览器 缺陷：基于回调函数，易产生回调地狱 基础示例 1 2 3 4 5 6 7 8 9 10 11 12 13 const xhr = new XMLHttpRequest(); // 1. 初始化请求（方法、URL、异步标志） xhr.open(\u0026#39;GET\u0026#39;, \u0026#39;/api/data\u0026#39;, true); // 2. 监听状态变化事件 xhr.onreadystatechange = function() { // 3. 判断请求完成且状态码200 if (xhr.readyState === 4 \u0026amp;\u0026amp; xhr.status === 200) { // 4. 手动解析JSON响应 console.log(JSON.parse(xhr.responseText)); } }; // 5. 发送请求 xhr.send(); 代码解析 open()方法： 第三个参数true表示异步请求 readyState状态（标识XMLHttpRequest对象处于什么状态）说明： 0: 未初始化 1: 已建立连接 2: 请求已接收 3: 处理中 4: 完成 手动处理JSON：需通过JSON.parse()转换响应文本 错误处理缺失：未处理4xx/5xx状态码 GET和POST请求数据区别 使用Get请求时,参数在URL中显示,而使用Post方式,则放在send里面 使用Get请求发送数据量小,Post请求发送数据量大 使用Get请求安全性低，会被缓存，而Post请求反之 关于第一点区别，详情见下面两张图：\n参考文献：Ajax原理一篇就够了\n二、Fetch API 核心特性 现代标准：基于Promise的链式调用 简洁语法：支持async/await 注意点：默认不携带cookie 基础示例 1 2 3 4 5 6 7 8 fetch(\u0026#39;/api/data\u0026#39;) .then(response =\u0026gt; { // 手动处理非2xx状态码 if (!response.ok) throw new Error(\u0026#39;HTTP error\u0026#39;); return response.json(); // 自动解析JSON }) .then(data =\u0026gt; console.log(data)) .catch(error =\u0026gt; console.error(\u0026#39;Error:\u0026#39;, error)); 代码解析 response.ok：自动判断2xx状态码 .json()方法：返回Promise对象，自动解析JSON 错误处理： .catch()捕获网络错误 需手动处理业务错误（如404） Vue3整合示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { ref, onMounted } from \u0026#39;vue\u0026#39;; export default { setup() { const data = ref(null); onMounted(async () =\u0026gt; { try { // 使用async/await简化代码 const response = await fetch(\u0026#39;/api/data\u0026#39;); data.value = await response.json(); } catch (error) { console.error(\u0026#39;Fetch Error:\u0026#39;, error); } }); return { data }; } }; 三、Axios 核心特性 功能丰富：拦截器、自动JSON转换 跨平台：支持浏览器和Node.js 生态集成：与Vue3深度适配 基础示例 1 2 3 4 5 6 axios.get(\u0026#39;/api/data\u0026#39;) .then(response =\u0026gt; console.log(response.data)) .catch(error =\u0026gt; { // 统一错误处理 console.error(error.response?.data || error.message); }); 高级用法：创建实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // src/api/index.js import axios from \u0026#39;axios\u0026#39;; const apiClient = axios.create({ baseURL: import.meta.env.VITE_API_URL, timeout: 5000, headers: {\u0026#39;X-Custom-Header\u0026#39;: \u0026#39;foobar\u0026#39;} }); // 请求拦截器 apiClient.interceptors.request.use(config =\u0026gt; { const token = localStorage.getItem(\u0026#39;token\u0026#39;); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); // 响应拦截器 apiClient.interceptors.response.use( response =\u0026gt; response.data, error =\u0026gt; { if (error.response.status === 401) { // 统一处理未授权错误 router.push(\u0026#39;/login\u0026#39;); } return Promise.reject(error); } ); export default apiClient; 四、Vue3最佳实践 1. 组合式API封装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { ref } from \u0026#39;vue\u0026#39;; export function useFetch(url) { const data = ref(null); const loading = ref(false); const error = ref(null); const fetchData = async () =\u0026gt; { try { loading.value = true; const response = await fetch(url); data.value = await response.json(); } catch (err) { error.value = err.message; } finally { loading.value = false; } }; return { data, loading, error, fetchData }; } 2. Pinia状态管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // stores/posts.js import { defineStore } from \u0026#39;pinia\u0026#39;; import axios from \u0026#39;axios\u0026#39;; export const usePostStore = defineStore(\u0026#39;posts\u0026#39;, { state: () =\u0026gt; ({ posts: [], loading: false, error: null }), actions: { async fetchPosts() { this.loading = true; try { const res = await axios.get(\u0026#39;/api/posts\u0026#39;); this.posts = res.data; } catch (error) { this.error = error.message; } finally { this.loading = false; } } } }); 五、技术选型对比表 特性 XMLHttpRequest Fetch Axios 语法复杂度 ⭐️（回调地狱） ⭐️⭐️⭐️（Promise） ⭐️⭐️⭐️⭐️（链式调用） 浏览器兼容性 IE6+ IE10+ IE11+ 自动JSON转换 ❌ ✅ ✅ 拦截器支持 ❌ ❌ ✅ 取消请求 手动实现 ✅（AbortController） ✅ 默认携带Cookie ❌ ❌ ✅ 六、Vue3项目推荐方案 小型项目：fetch + 组合式API封装 中大型项目：Axios实例 + Pinia状态管理 SSR项目：Nuxt3的useFetch组合式函数 TypeScript项目：Axios + 类型守卫 提示：在Vue3.2+版本中，推荐使用\u0026lt;script setup\u0026gt;语法配合组合式API进行开发，可获得更好的类型推导支持。\n","date":"2025-03-22T12:01:56+08:00","permalink":"https://linsk27.github.io/p/0322-%E4%B8%89%E7%A7%8D%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8/","title":"XMLHttpRequest、Axios 与 Fetch 在 Vue3 中的深度解析"},{"content":"链表环检测算法（LeetCode 141） 我的实现方法：哈希表检测法 原始思路 通过哈希表记录已访问过的节点，当遍历到已存在的节点时，说明存在环\n初始代码 1 2 3 4 5 6 7 8 9 var hasCycle = function (head) { let seen = new Map() while(head){ seen.set(head, true); head = head.next; if(seen.get(head)) return true } return false }; 代码优化建议 数据结构优化：Map → Set 更适合存储唯一值 逻辑顺序调整：先检查再移动指针更严谨 优化后代码 1 2 3 4 5 6 7 8 9 var hasCycle = function(head) { const seen = new Set(); while (head) { if (seen.has(head)) return true; // 先检查当前节点 seen.add(head); // 再记录访问状态 head = head.next; // 最后移动指针 } return false; }; 问题分析 原始代码存在两个潜在风险：\n检查的是head.next而非当前节点 移动指针后可能跳过最后一个节点的检查 扩展方法：快慢指针法（Floyd判圈算法） 核心思想 使用速度不同的双指针：\n快指针每次走两步 慢指针每次走一步 若存在环则必然相遇 代码实现 1 2 3 4 5 6 7 8 9 var hasCycle = function(head) { let slow = head, fast = head; while (fast \u0026amp;\u0026amp; fast.next) { slow = slow.next; fast = fast.next.next; if (slow === fast) return true; } return false; }; 对比分析 方法 时间复杂度 空间复杂度 适用场景 哈希表检测法 O(n) O(n) 需要记录访问路径的场景 快慢指针法 O(n) O(1) 空间敏感型场景 ","date":"2025-03-21T23:56:52+08:00","permalink":"https://linsk27.github.io/p/0321-%E9%93%BE%E8%A1%A8%E5%BE%AA%E7%8E%AF-%E5%93%88%E5%B8%8C%E6%96%B9%E6%B3%95/","title":"链表循环--哈希方法"},{"content":"Webpack 初步接触 一、什么是 Webpack？ Webpack 是一个 静态模块打包工具，用于将多个模块、文件和资源进行打包，以便在浏览器中高效加载和运行。\n核心功能： 模块打包\n整合 JS、CSS、图片等资源，提升代码可维护性和复用性。 依赖管理\n自动解析模块依赖关系，确保正确加载顺序，降低手动管理复杂度。 代码转换与优化\n通过 Loaders 转换代码（如 ES6 → ES5），并优化输出体积。 开发支持\n支持 HMR（热更新）、Source Maps 调试、代码分割等，提升开发效率。 生态系统\n丰富的插件（Plugins）和加载器（Loaders）扩展功能。 二、Webpack 构建流程 构建过程是串行的，主要分为以下阶段：\n初始化参数\n合并配置文件与命令行参数，生成最终配置。 启动编译\n初始化 Compiler 对象，加载插件并调用 run 方法。 确定入口\n根据 entry 配置定位所有入口文件。 编译模块\n递归处理模块依赖： 调用 Loaders 转换文件内容（如编译 Sass → CSS） 解析模块依赖关系，直至所有模块处理完成。 输出资源\n将模块组合为 Chunk，生成最终文件（如 JS/CSS 文件）。 写入文件系统\n根据 output 配置输出文件到指定路径。 插件机制 Webpack 在构建生命周期广播事件（如 compile、emit），插件通过监听事件调用 API 修改输出结果。\n三、常见 Loaders Loader 功能描述 raw-loader 导入文件原始内容（如文本文件）。 file-loader 将文件输出到指定目录，返回 URL 路径。 url-loader 小于阈值的文件转为 Base64，否则调用 file-loader。 babel-loader 转换 ES6+ 代码为向后兼容的 JavaScript。 ts-loader 编译 TypeScript 为 JavaScript。 sass-loader 编译 Sass/SCSS 为 CSS。 css-loader 解析 CSS 文件中的 @import 和 url()。 style-loader 将 CSS 注入到 DOM 的 \u0026lt;style\u0026gt; 标签中。 eslint-loader 静态检查 JavaScript 代码规范。 vue-loader 处理 Vue 单文件组件（.vue）。 四、常见 Plugins Plugin 功能描述 DefinePlugin 定义全局常量（如环境变量）。 HtmlWebpackPlugin 自动生成 HTML 文件并注入资源。 MiniCssExtractPlugin 将 CSS 提取为独立文件。 UglifyJsPlugin 压缩 JavaScript 代码。 CleanWebpackPlugin 构建前清理输出目录。 WebpackBundleAnalyzer 可视化分析打包体积。 SpeedMeasurePlugin 统计各 Loader/Plugin 的构建耗时。 五、Loader 与 Plugin 的区别 特性 Loader Plugin 核心功能 转换特定类型文件（如编译 Sass）。 扩展 Webpack 功能（如代码压缩、资源注入）。 使用场景 处理单个文件的转换逻辑。 监听构建事件，执行全局操作。 配置方式 在 module.rules 中定义，按文件类型匹配。 在 plugins 数组中实例化，可传入参数。 实现原理 基于函数链式调用处理文件内容。 基于 Tapable 事件流，在生命周期钩子中干预构建。 ","date":"2025-03-21T11:49:00+08:00","image":"https://linsk27.github.io/p/0321-webpack%E5%88%9D%E6%AD%A5%E6%8E%A5%E8%A7%A6/webpack_hu_6d0aaff99a9c177.png","permalink":"https://linsk27.github.io/p/0321-webpack%E5%88%9D%E6%AD%A5%E6%8E%A5%E8%A7%A6/","title":"Webpack初步接触"},{"content":"\n低于O(n^2)的写法，使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /** *原理： * 1. 遍历nums *\t2. 通过hash.has()搜索哈希表内的对应值 *\t3. 没有对应值则存入哈希表，继续下一个数据搜索哈希表内对应值 *\t4. 搜索到对应值，终止循环 * 例子：nums:[2,8,11,15,3,7] target:9 */ var twoSum = function (nums, target) { let hash = new Map() for (let i = 0; i \u0026lt; nums.length; i++) { // 首次哈希表为空，搜索不到数据 // 如果哈希表内搜索不到nums[i]对应相加数据为target的答案，则先存入哈希表，让下个循环的nums[i]进行搜索 if (!hash.has(target - nums[i])) hash.set(nums[i],i) // 哈希表搜索到答案则直接返回 else return [hash.get(target-nums[i]), i] } }; let hash = new Map(); 是 JavaScript 中使用 Map 对象创建一个哈希表（Hash Table）的写法。Map 是 ES6 引入的一种数据结构，用于存储键值对（key-value pairs），类似于普通对象（{}），但功能更强大且灵活。\n1. Map 的核心特性 特性 描述 键的类型任意 键可以是任何数据类型（对象、函数、基本类型等），而普通对象的键只能是字符串或 Symbol。 有序性 键值对的插入顺序会被保留，遍历时按插入顺序返回。 可直接获取大小 通过 hash.size 直接获取键值对数量，而普通对象需要 Object.keys(obj).length。 高性能增删查 在频繁增删键值对时，性能通常优于普通对象。 2. 基本用法 初始化与操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 创建一个空 Map let hash = new Map(); // 添加键值对：使用 set(key, value) hash.set(\u0026#34;name\u0026#34;, \u0026#34;Alice\u0026#34;); // 键是字符串 hash.set(42, \u0026#34;Answer\u0026#34;); // 键是数字 hash.set({}, \u0026#34;empty object\u0026#34;); // 键是对象 // 获取值：使用 get(key) console.log(hash.get(\u0026#34;name\u0026#34;)); // 输出 \u0026#34;Alice\u0026#34; // 检查键是否存在：使用 has(key) console.log(hash.has(42)); // true // 删除键值对：使用 delete(key) hash.delete(42); // 清空所有键值对：使用 clear() hash.clear(); 遍历 Map 1 2 3 4 5 6 7 8 9 10 11 // 使用 for...of 遍历键值对 for (const [key, value] of hash) { console.log(key, value); } // 获取所有键：keys() console.log(hash.keys()); // 输出所有键的迭代器 // 获取所有值：values() console.log(hash.values()); // 输出所有值的迭代器 // 获取所有键值对：entries() console.log(hash.entries()); 3. 与普通对象 {} 的区别 场景 普通对象 {} Map 键的类型 仅支持字符串或 Symbol 支持任意类型（包括对象、函数等） 顺序保证 ES6 后保持插入顺序（但不绝对可靠） 严格按插入顺序遍历 大小获取 需手动计算 Object.keys(obj).length 直接通过 map.size 性能 频繁增删时性能较差 频繁增删时性能更好 继承原型链属性 可能意外继承原型链上的属性 不会继承原型链属性 4. 使用场景 需要键为复杂类型（如对象、函数）时：\n1 2 const funcKey = () =\u0026gt; {}; hash.set(funcKey, \u0026#34;Function as key\u0026#34;); 需要保持插入顺序：\n1 2 3 4 const orderedMap = new Map(); orderedMap.set(\u0026#34;second\u0026#34;, 2); orderedMap.set(\u0026#34;first\u0026#34;, 1); console.log([...orderedMap.keys()]); // [\u0026#34;second\u0026#34;, \u0026#34;first\u0026#34;] 需要高效增删操作（如缓存、临时数据存储）：\n1 2 3 const cache = new Map(); cache.set(\u0026#34;user:123\u0026#34;, { name: \u0026#34;Alice\u0026#34; }); cache.delete(\u0026#34;user:123\u0026#34;); // 快速删除 ","date":"2025-03-16T23:10:13+08:00","permalink":"https://linsk27.github.io/p/%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C--%E5%93%88%E5%B8%8C%E6%96%B9%E6%B3%95/","title":"两数之和--哈希方法"},{"content":"SEO本质 解决两个匹配问题：\n内容匹配：让网站内容精准对应搜索者的真实需求（比如用户搜「如何修复冰箱不制冷」，你的网页必须提供具体解决方案，而非只介绍冰箱品牌） 技术匹配：让搜索引擎能快速理解并推荐你的内容（比如用HTML标签明确标注文章主题、确保手机端加载速度低于2秒） 优化SEO的底层逻辑 不优化的结果 优化后的效果 对应技术手段 用户搜不到你（关键词缺失） 目标关键词进入前3页 关键词研究工具（如Google Keyword Planner） 搜索引擎误判内容（技术缺陷） 爬虫高效抓取内容 优化robots.txt文件、XML网站地图 用户点开就离开（体验差） 停留时间提升50%+ 内容分段落+配图、添加目录导航 被算法判定为低质内容 获得「优质内容」权重加分 增加专业参考文献、作者资质说明 外链数量为0 获得行业权威网站推荐 制作可被引用的研究报告/工具 必须优化SEO的数学证明 设某关键词月搜索量1000次：\n第1名点击率 ≈ 35% → 350次访问/月 第6名点击率 ≈ 4.5% → 45次访问/月 排名提升5位，流量增长678% 流量价值计算（假设转化率2%，客单价$100）：\n第1名月收益：350×2%×$100 = $700 第6名月收益：45×2%×$100 = $90 SEO带来的边际收益差：$610/月 现代SEO的三个死亡红线 关键词堆砌（密度\u0026gt;2%即可能被惩罚） 忽视用户实际停留时间（Google已用屏幕停留时长替代跳出率作为核心指标） 忽略结构化数据（没有Schema标记的网页，点击率平均低37%） ","date":"2025-03-15T13:29:13+08:00","image":"https://linsk27.github.io/p/0315-%E4%BB%80%E4%B9%88%E6%98%AFseo/SEO_hu_a90e3b1c0e2e4f9b.jpeg","permalink":"https://linsk27.github.io/p/0315-%E4%BB%80%E4%B9%88%E6%98%AFseo/","title":"什么是SEO？"},{"content":"\u0026lt;link rel=\u0026quot;canonical\u0026quot; href=\u0026quot;xxxx/t\u0026quot;\u0026gt;\n主要目的：解决 重复内容（Duplicate Content） 问题，并明确告知搜索引擎哪个是页面的“规范版本”（Canonical URL）。\n1. 什么是Canonical标签？ 作用：当同一内容（页面）可通过多个不同URL访问时（例如带参数的URL、分页、移动端/PC端不同URL等），搜索引擎可能认为这些是重复内容，导致权重分散。通过 rel=\u0026quot;canonical\u0026quot; 标签，可以指定一个 规范URL，告诉搜索引擎“这个URL是主要版本，请优先收录和排名”。\n语法：\n1 \u0026lt;link rel=\u0026#34;canonical\u0026#34; href=\u0026#34;https://example.com/规范URL\u0026#34; /\u0026gt; 2. 为什么需要添加Canonical标签？ 场景举例 URL参数导致的重复内容\n例如：\nhttps://example.com/product?id=1（排序参数）\nhttps://example.com/product?id=1\u0026amp;sort=price 搜索引擎可能认为这是两个不同页面，但实际内容相同。通过设置规范URL为 https://example.com/product?id=1，可集中权重。 分页内容\n例如：\nhttps://example.com/blog?page=1\nhttps://example.com/blog?page=2 可以在分页页面中设置规范URL指向主页面（如 https://example.com/blog），或为每个分页指定各自的规范URL。 移动端与桌面端不同URL\n例如：\nhttps://m.example.com/page（移动端）\nhttps://www.example.com/page（桌面端） 可以在移动端页面添加规范标签指向桌面端URL，或反之（根据SEO策略决定）。 3. 如何正确使用Canonical标签？ 规范URL必须是可访问的：确保 href 中的URL是真实存在的、200状态码的页面。 绝对URL优先：建议使用完整URL（如 https://example.com/t），而非相对路径（/t）。 避免链轮问题：多个页面不能互相指向对方作为规范URL（例如A→B，B→A），会导致搜索引擎无法识别。 分页处理： 如果分页内容独立，每个分页应指定自己的规范URL。 如果希望集中权重到主页面，所有分页的规范URL指向主页面。 4. 与301重定向的区别 301重定向：将用户和搜索引擎永久跳转到新URL，旧URL不再存在。 适用场景：页面迁移、URL结构变更。 Canonical标签：仅提示搜索引擎规范URL，用户仍可访问原URL。 适用场景：内容相同或高度相似的多个URL需要共存（如参数化URL）。 5. 常见错误 指向非规范域名：例如将 https://example.com/t 的规范URL设为 https://otherdomain.com/t，可能导致权重流失。 忽略动态参数：若规范URL带参数，需确保参数不影响内容（如 ?utm_source=... 可保留，但排序参数需排除）。 在非HTML页面使用：如PDF、图片等非HTML文件中添加无效。 6. 验证是否生效 使用 Google Search Console 的“URL检查”工具，查看Google是否识别了规范URL。\n通过浏览器开发者工具查看页面源代码，确认标签正确添加。\n点击Ctrl+U\n7. Nuxt3举例实现方法 优化前 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 步骤1:获取本地地址或.env路径(可用其他方法实现) const requestURL = useRequestURL().href??\u0026#39;\u0026#39; // 动态设置 SEO Meta 标签 useHead({ title: fit[0].seo_setting.title??\u0026#34;\u0026#34;, // 标题 meta: [ { name: \u0026#39;description\u0026#39;, content: fit[0].seo_setting.desc??\u0026#34;\u0026#34; }, // 描述 { name: \u0026#39;keywords\u0026#39;, content: fit[0].seo_setting.keyword??\u0026#39;\u0026#39; }, // 关键词 // 社交媒体优化（OpenGraph） { property: \u0026#39;og:title\u0026#39;, content: fit[0].seo_setting.title??\u0026#39;\u0026#39; }, { property: \u0026#39;og:description\u0026#39;, content: fit[0].seo_setting.desc??\u0026#39;\u0026#39; } ], link:[ {rel:\u0026#39;canonical\u0026#39;,href:requestURL} ] }); 潜在问题 useRequestURL().href的使用风险： 在 SSR（服务器端渲染） 时可能无法正确获取完整 URL（例如在静态导出或反向代理场景下）。如果 href 为空字符串，生成的 canonical 标签会变成 \u0026lt;link rel=\u0026quot;canonical\u0026quot; href=\u0026quot;\u0026quot;\u0026gt;，这会被搜索引擎视为无效或错误的规范 URL，导致 SEO 权重流失\n动态数据为空的兜底逻辑 fit[0].seo_setting.title??\u0026quot;\u0026quot; 依赖外部数据（如 API 响应），如果 fit 数组为空或字段缺失，会导致 title、description 等关键标签为空字符串，搜索引擎会认为页面信息不完整，影响排名。\n代码健壮性改进建议 确保 fit[0].seo_setting 存在且字段完整，避免渲染时崩溃.\n优化后 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 获取本地地址 const requestURL = process.server ? useRequestURL().href // SSR 环境：直接获取完整 URL : window.location.href; // 客户端：使用浏览器 URL // 动态设置 SEO Meta 标签 useHead({ title: fit[0].seo_setting.title??\u0026#34;默认标题\u0026#34;, // 标题 meta: [ { name: \u0026#39;description\u0026#39;, content: fit[0].seo_setting.desc??\u0026#34;默认描述\u0026#34; }, // 描述 { name: \u0026#39;keywords\u0026#39;, content: fit[0].seo_setting.keyword??\u0026#39;默认关键词\u0026#39; }, // 关键词 // 社交媒体优化（OpenGraph） { property: \u0026#39;og:title\u0026#39;, content: fit[0].seo_setting.title??\u0026#39;默认描述\u0026#39; }, { property: \u0026#39;og:description\u0026#39;, content: fit[0].seo_setting.desc??\u0026#39;默认描述\u0026#39; } ], link:[ {rel:\u0026#39;canonical\u0026#39;,href:requestURL} ] }); 代码整体对 SEO 的价值 核心作用 ：\n通过动态设置元标签和规范链接，解决 、关键词匹配 、权重集中 、跨平台展示三大 SEO 核心问题。\n直接收益 ：\n提升搜索排名（通过标题/描述优化）、减少重复内容惩罚（通过 canonical)、增强社交传播效果（通过 OpenGraph社交媒体优化）。\n","date":"2025-03-14T13:29:13+08:00","image":"https://linsk27.github.io/p/seo%E4%BC%98%E5%8C%96%E6%89%8B%E6%AE%B5---canonical/SEO_hu_a90e3b1c0e2e4f9b.jpeg","permalink":"https://linsk27.github.io/p/seo%E4%BC%98%E5%8C%96%E6%89%8B%E6%AE%B5---canonical/","title":"SEO优化手段---Canonical"},{"content":"el-table中sort的常见方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const sort = async (newIndex: number, oldIndex: number) =\u0026gt; { if (newIndex === oldIndex) return; const newData = [...tableData.value.rows]; const [movedItem] = newData.splice(oldIndex, 1); newData.splice(newIndex, 0, movedItem); newData.forEach((item, index) =\u0026gt; { item.index = index; }); // Vue3 响应式更新方式 tableData.value = { ...tableData.value, rows: [...newData] // 强制替换整个数组触发更新 }; sortData.value.tag_groups = newData.map(item =\u0026gt; ({ tag_group_id: item.tag_group_id, index: item.index })); isData .value += 1; // 每次拖拽后更新 key } ","date":"2025-03-08T00:00:00Z","permalink":"https://linsk27.github.io/p/el-table%E4%B8%ADsort%E6%8B%96%E6%8B%BD%E6%8E%92%E5%BA%8F%E7%9A%84%E5%B8%B8%E8%A7%81%E6%96%B9%E6%B3%95/","title":"el-table中sort(拖拽排序)的常见方法"},{"content":"模板部分 1 2 3 4 5 6 7 8 9 10 11 \u0026lt;template\u0026gt; \u0026lt;!--断层--\u0026gt; \u0026lt;div class=\u0026#34;tooltip-box\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;tooltip-arrow\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 点击复制链接 \u0026lt;/div\u0026gt; \u0026lt;!--伪类写法--\u0026gt; \u0026lt;div class=\u0026#34;copy-tooltip-box\u0026#34;\u0026gt; 点击复制链接 \u0026lt;/div\u0026gt; \u0026lt;/template\u0026gt; Css处理部分 断层写法例子 1 2 3 4 5 6 7 8 9 10 11 .tooltip-arrow { position: absolute; /* 👉 基于父容器（.tooltip-box）定位 */ bottom: -10px; /* 👉 位于提示框下方 10px 处（负值向上移动） */ left: 50%; /* 👉 水平居中 */ transform: translateX(-50%); /* 👉 微调水平位置 */ width: 0; /* 👉 宽度为 0，通过边框生成三角形 */ height: 0; /* 👉 高度为 0 */ border-left: 8px solid transparent; /* 👉 左侧透明边框 */ border-right: 8px solid transparent; /* 👉 右侧透明边框 */ border-top: 10px solid white; /* 👉 顶部实色边框，形成箭头 */ } 使用伪类防止断层写法 推荐方法，防止黑色透明箭头和盒子交接处出现断层深色线条 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 正常的样例盒子 .copy-tooltip-box { bottom: calc(100% + 14px); position: absolute; left: 50%; transform: translateX(-50%); background-color: #323233D9; /* 黑色主题 */ width: 88px; height: 28px; color: #fff; } // 伪类实现方法 .copy-tooltip-box:after { content: \u0026#34;\u0026#34;; position: absolute; top: 100%; /* 箭头位于提示框底部 */ left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; border-top: 10px solid #323233D9; } ","date":"2025-03-07T00:00:00Z","image":"https://linsk27.github.io/p/%E6%82%AC%E6%B5%AE%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E7%9B%92%E5%AD%90%E7%9A%84%E5%86%99%E6%B3%95/hover_hu_8e43e58e2447932.png","permalink":"https://linsk27.github.io/p/%E6%82%AC%E6%B5%AE%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E7%9B%92%E5%AD%90%E7%9A%84%E5%86%99%E6%B3%95/","title":"悬浮出现箭头盒子的写法"},{"content":"推送失败，gitee仓库缺少公钥 1 git push origin lsk_dev 1 2 3 4 5 6 7 8 9 10 11 The authenticity of host \u0026#39;gitee.com (180.76.198.77)\u0026#39; can\u0026#39;t be established. ED25519 key fingerprint is SHA256:+ULzij2u99B9eWYFTw1Q4ErYG/aepHLbu96PAUCoV88. This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])? y Please type \u0026#39;yes\u0026#39;, \u0026#39;no\u0026#39; or the fingerprint: yes Warning: Permanently added \u0026#39;gitee.com\u0026#39; (ED25519) to the list of known hosts. git@gitee.com: Permission denied (publickey). fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. 检查与自己gitee仓库连接失败，没有显示hellow 1 ssh -T git@gitee.com 1 git@gitee.com: Permission denied (publickey). 进行公钥配置 1 ssh-keygen -t rsa -C \u0026#34;14062626+linsk27@user.noreply.gitee.com\u0026#34; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Generating public/private rsa key pair. Enter file in which to save the key (C:\\Users\\lsk69/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in C:\\Users\\lsk69/.ssh/id_rsa Your public key has been saved in C:\\Users\\lsk69/.ssh/id_rsa.pub The key fingerprint is: SHA256:SOV+Ur+E321Pw+9NL77mSgZm6V8lc+W3LJi3rN5qXG8 14062626+linsk27@user.noreply.gitee.com The key\u0026#39;s randomart image is: +---[RSA 3072]----+ | . | | o | | . . . .| | . o . + ..| | . S B o o =| | * +o+o*o| | oo*o+==| | *+.=E*| | o+=O=+*| +----[SHA256]-----+ 获取公钥失败 1 ssh-agent -s 1 2 3 4 5 6 7 8 unable to start ssh-agent service, error :1058 \u0026gt; Fix Error **unable to start ssh-agent service, error: 1058(xxxx)** \u0026gt; \u0026gt; 1. win + R, Go To `services.msc` \u0026gt; \u0026gt; 2. Find And Check Is `OpenSSH Authentication Agent` Service Running \u0026gt; 再次运行公钥获取成功，连接成功 1 2 3 ssh-agent -s ssh -T git@gitee.com 1 Hi linsk27(@linsk27)! You\u0026#39;ve successfully authenticated, but GITEE.COM does not provide shell access. 复制公钥数据到gitee仓库的公钥（设置） 把ssh密钥添加到码云\n打开C盘–\u0026gt;用户–\u0026gt;你的用户名–\u0026gt;找到.ssh文件夹。找到id_rsa.pub（日过有多个用最新的那个），用记事本打开，复制整个文本粘贴到gitee（点头像，进入gitee设置面板，SSH设置，将复制的文本粘贴到公钥，标题会自动生成，然后点击添加，根据提示输入密码就可以了。）\n重新推送成功 1 git push origin lsk_dev 1 2 3 4 5 6 7 8 9 10 11 12 Enumerating objects: 16, done. Counting objects: 100% (16/16), done. Delta compression using up to 16 threads Compressing objects: 100% (15/15), done. Writing objects: 100% (16/16), 2.19 KiB | 2.19 MiB/s, done. Total 16 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) remote: Powered by GITEE.COM [1.1.5] remote: Set trace flag 824c4db5 remote: Create a pull request for \u0026#39;lsk_dev\u0026#39; on Gitee by visiting: remote: https://gitee.com/larrry/competition-system/pull/new/larrry:lsk_dev...larrry:main To gitee.com:larrry/competition-system.git * [new branch] lsk_dev -\u0026gt; lsk_dev ","date":"2024-11-20T00:00:00Z","image":"https://linsk27.github.io/p/gitee%E4%BB%93%E5%BA%93%E7%9A%84%E7%AC%AC%E4%B8%80%E6%AC%A1%E6%8E%A8%E9%80%81%E9%81%87%E8%A7%81%E7%9A%84%E9%94%99%E8%AF%AF/error.svg","permalink":"https://linsk27.github.io/p/gitee%E4%BB%93%E5%BA%93%E7%9A%84%E7%AC%AC%E4%B8%80%E6%AC%A1%E6%8E%A8%E9%80%81%E9%81%87%E8%A7%81%E7%9A%84%E9%94%99%E8%AF%AF/","title":"gitee仓库的第一次推送遇见的错误"},{"content":"工具合集 sunshine是一个提供给moonlight进行串流的工具\nmoonlight是一个可以方便的将Windows电脑画面传输到各主流操作系统的客户端软件上的工具\n虚拟显示器可以实现屏幕扩展的工具。在这里可以与串流实现配合。\n链接：https://pan.baidu.com/s/1_zhprVgFAOsemp3jEvslsA 提取码：lrx1\n实现屏幕复制 安装与配置sunshine 点击sunshine的.exe文件进行运行安装（安装任何东西尽量不安装到系统盘）,然后右击电脑屏幕右下角的sunshine图标，点击open sunshine进入sunshine的管理界面，具体界面类似点2的图。\n先点击齿轮图形（设置），把页面设置为中文简体方便操作，记得点击保存，然后点击apply（应用），此时会自动重启sunshine程序，如果没有启动则右键右下角的sunshine图标，点击Restart。\n同第2点开设置后，点击network板块，根据图片设置UPnP为“启用”，设置ip地址族为“IPv4+IPv6”，这个对后面串流有用。最后把公网加密模式设置为“禁用”，点击保存并且应用，重启sunshine步骤。 安装与配置moonlight 该演示为安卓端，pc端大同小异，同样可看下面教程\n在上面给的网盘内直接安装或者谷歌商店安装moonlight,进入moonlight界面 点击齿轮图形（设置），可以根据自己设备的分辨率进行设置，其中码率对远程串流会有较大影响，个人认为局域网连接可以直接拉满 设备通常打开moonlight后，在相同局域网下，设备会显示同个局域网下的电脑设备，如第一点的图所示。此时可以平板直接点击电脑设备进行连接，然后会出现ping码，通常电脑会出现ping码弹窗，没有的话打开sunshine控制台，点击ping选项进行输入。通常第一次连接之后，只要平板不删除电脑设备，就不会再要求输入ping码。 如果没有显示电脑设备，则可在电脑按下Win+R,输入cmd打开终端，然后输入ipconfig来获取局域网的ipv4或ipv6的地址（我这个网络没有ipv6地址）。然后选择字段为IPv4地址或IPv6 地址的字样才有效。ipv6对远程串流帮助较大。\n然后点击平板端的moonlight，点击加号图标，直接输入ipv4或者ipv6地址，即可显示电脑设备。\n这个时候你的平板设备应该就成功复制了电脑屏幕，并且几乎只有100ms的延迟！ 实现屏幕扩展 配置与安装ParsecVDisplay（虚拟显示器） 在网盘内或者自行选择下载虚拟显示器软件，打开后如图。 点击ADD DISPLAY,添加虚拟显示器，随后点击显示器查看属性。但是通常会有bug，比如显示\\\\.\\DISPLAY1,说明添加与原屏幕冲突，需要再添加一次，然后可以点击这个1的屏幕然后进行删除。 有正确的显示器之后，点击显示器查看显示器参数，如图为\\\\.\\DISPLAY11。同时，对该虚拟显示器的刷新率和帧率参数进行配置。最后点击CUSTOM按键，输入这些参数。 最后打开sunshine控制台，点击齿轮设置，打开video选项，输入显示器的名称\\\\.\\DISPLAY11，点击保存并且应用，然后平板进行moonlight重连，这个时候，就会扩展成功！ 打开Win+I打开电脑设置，进入“显示”模块，对平板显示器进行位置调动，方便扩展。 注意！！！不断连不上可查看此处 当时搞完这个扩展显示器，后面我出现很多次连不上的现象\n失败原因： sunshine设置了video的显示器名称后，就只会抓取该名称的显示器进行显示，如果没有生成该虚拟显示器，则其他设备的moonlight就无法链接到电脑设备\n解决方法： 方式1：把sunshine的video选项里的video清空，这样就会默认复制屏幕。\n方式2：打开虚拟显示器并且创建，保持sunshine的video设置和虚拟显示器的名称设置相同即可！！\n实现远程串流 ","date":"2024-11-02T00:00:00Z","image":"https://linsk27.github.io/p/moonlight-sunshine%E5%AE%9E%E7%8E%B0%E6%89%A9%E5%B1%95%E5%B1%8F%E5%92%8C%E8%BF%9C%E7%A8%8B%E4%B8%B2%E6%B5%81/moonlight_hu_cc6493ee2a05def4.jpg","permalink":"https://linsk27.github.io/p/moonlight-sunshine%E5%AE%9E%E7%8E%B0%E6%89%A9%E5%B1%95%E5%B1%8F%E5%92%8C%E8%BF%9C%E7%A8%8B%E4%B8%B2%E6%B5%81/","title":"moonlight+sunshine实现扩展屏和远程串流"},{"content":"什么是防抖（Debounce）？ 防抖 （多次触发 只执行最后一次） 原理：在高频率触发某个响应事件时，在规定时间间隔内如果没有再次触发，则属于最后一次完成交互，触发相对应的方法\n常用：在用户输入搜索词的过程中，每一次输入都会触发 input 事件，但显然没必要每次都发送请求，只有当用户停止输入后，系统才发送一次最终的请求\n手搓代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 1.封装防抖函数 function debounce(fn, time) { // 4.创建一个标记用来存放定时器的返回值 let timeout = null; return function () { // 5.每当用户触发input事件 把前一个 setTimeout 清楚掉 clearTimeout(timeout); // 6.然后又创建一个新的 setTimeout, 这样就能保证输入字符后等待的间隔内 还有字符输入的话，就不会执行 setTimeout里面的内容 timeout = setTimeout(() =\u0026gt; { // 7.这里进行防抖的内容 fn(); }, time); }; } // 2.获取inpt元素 var inp = document.getElementById(\u0026#34;inp\u0026#34;); // 8. 测试防抖临时使用的函数 function sayHi() { console.log(\u0026#34;防抖成功\u0026#34;); } // 3.给inp绑定input事件 调用封装的防抖函数 传入要执行的内容与间隔事件 inp.addEventListener(\u0026#34;input\u0026#34;, debounce(sayHi, 5000)); 什么是节流（Throttle）？ 节流 （时间间隔内 只允许执行一次） 原理：在高频率触发某个有响应方法的事件时，第一次触发响应事件，计时器开始计时，规定时间内继续交互不触发事件\n常用：鼠标滚轮滑动触发下拉刷新、王者的英雄技能刷新\n手搓代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // 1.封装节流函数 function throttle(fn, time) { //3. 通过闭包保存一个 \u0026#34;节流阀\u0026#34; 默认为false let temp = false; return function () { //8.触发事件被调用 判断\u0026#34;节流阀\u0026#34; 是否为true 如果为true就直接trurn出去不做任何操作 if (temp) { return; } else { //4. 如果节流阀为false 立即将节流阀设置为true temp = true; //节流阀设置为true //5. 开启定时器 setTimeout(() =\u0026gt; { //6. 将外部传入的函数的执行放在setTimeout中 fn.apply(this, arguments); //7. 最后在setTimeout执行完毕后再把标记\u0026#39;节流阀\u0026#39;为false(关键) 表示可以执行下一次循环了。当定时器没有执行的时候标记永远是true，在开头被return掉 temp = false; }, time); } }; } function sayHi(e) { // 打印当前 document 的宽高 console.log(e.target.innerWidth, e.target.innerHeight); } // 2.绑定事件，绑定时就调用节流函数 // 敲黑板！！！ 这里是重点 绑定是就要调用一下封装的节流函数 触发事件是触发封装函数内部的函数 window.addEventListener(\u0026#34;resize\u0026#34;, throttle(sayHi, 2000)); 高效方式实现防抖和节流 使用 Loadash 库实现防抖和节流 lodash 是一个留下的 JavaScript 实用工具库，提供了高效的防抖和节流函数 使用步骤 安装 lodash 1 npm install lodash 使用防抖和节流 1 2 3 4 5 6 7 8 9 10 import debounce from \u0026#34;lodash/debounce\u0026#34;; import throttle from \u0026#34;lodash/throttle\u0026#34;; // 防抖示例 const debouncedSearch = debounce(handleSearch, 300); searchInput.addEventListener(\u0026#34;input\u0026#34;, debouncedSearch); // 节流示例 const throttledScroll = throttle(handleScroll, 200); window.addEventListener(\u0026#34;scroll\u0026#34;, throttledScroll); 设置合适的时间间隔 防抖和节流的核心在于事件频率控制，因此根据实际场景合理设置时间参数非常关键。过长的延迟可能导致响应不及时，而过短的间隔可能会影响性能提升效果。 通常，防抖的延迟在200ms到500ms之间，节流的时间间隔在100ms到300ms之间。\n我写过的 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import _ from \u0026#39;lodash\u0026#39; const loading=ref() // 防抖搜索（核心逻辑） const handleSearch = _.debounce(async (query) =\u0026gt; { if (!query) return loading.value = true try { let res; // 接口调用 res = await props.api({ name: query}) } finally { loading.value = false } }, 300) ","date":"2024-10-26T00:00:00Z","permalink":"https://linsk27.github.io/p/%E4%BB%80%E4%B9%88%E6%98%AF%E9%98%B2%E6%8A%96%E5%92%8C%E8%8A%82%E6%B5%81%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E5%8E%BB%E7%94%A8/","title":"什么是防抖和节流？什么时候去用？"},{"content":"📌 程序员成长的底层逻辑 大学自学常见误区：过度依赖视频教程 → 知识碎片化 + 学习低效 + 上水课坐牢!!!!\n一、计算机基础筑基 1.1 网络协议 📖《图解 TCP/IP》- TCP/IP协议族可视化解析 📖《图解 HTTP》- HTTP协议运作机制图解 🔗 MDN HTTP 文档（权威技术参考——官方文档） 1.2 算法与数据结构 📖《算法图解》- 算法思维可视化入门 🏆 LeetCode 新手村（每日一题保持手感） 1.3 数据库基础 🐬 MySQL 极简教程（CRUD快速上手——老胡的周刊） 🎯 SQLZoo 实战（在线SQL练习平台） 1.4 Linux 入门 🐧 中科大 Linux 101（中文实验室手册） 💻 Linux 命令速查表（PDF可打印版） 二、前端学习黄金路径 2.1 核心三件套 技术栈 学习重点 推荐资源 HTML5 语义化标签 + 表单验证 MDN HTML 权威指南 CSS3 Flex/Grid布局 + 动画 CSS Tricks 终极指南 ES6+ 异步编程 + DOM操作 现代 JavaScript 教程 ECMAScript 6 入门 2.2 高效学习法 盒子模型思维：所有元素皆盒子（margin/padding/border） 渐进式实战：每日1个小项目 → 每周1个综合案例 调试技巧：Chrome DevTools 断点调试 + 移动端模拟 2.3 精品资源库 🎮 交互学习： ▶ Flexbox 青蛙游戏（弹性布局通关） ▶ Grid 花园（网格布局实战）\n📚 文档体系： ▶ ECMAScript 6 入门（阮一峰经典） ▶ 前端九部知识体系（中文渐进教程）\n🚀 项目实战： ▶ 50 Projects in 50 Days（项目思维训练） ▶ Vue.js 挑战（框架预热）\n在线编辑器:https://plnkr.co/edit\n三、开发者效率工具箱 3.1 必备神器 🖋️ Markdown：语法五分钟精通 💻 VS Code： 1 2 3 4 5 6 7 8 // 推荐插件配置 { \u0026#34;必备插件\u0026#34;: [ \u0026#34;Prettier - 代码格式化\u0026#34;, \u0026#34;Live Server - 实时预览\u0026#34;, \u0026#34;GitLens - 代码历史追溯\u0026#34; ] } 3.2 Git 版本控制 🎯 核心工作流： git clone [url] 克隆仓库 git checkout -b feature 创建分支 git push origin feature 推送更改 📚 Git 飞行规则（故障应急手册） 四、避坑指南（Bonus） 盲目追求框架（先扎实原生JS） 重复造轮子（学会阅读源码） 忽视代码规范（ESLint + Prettier） 立即开启你的高效学习之旅吧！\n","date":"2024-10-20T00:00:00Z","permalink":"https://linsk27.github.io/p/%E5%85%A5%E9%97%A8%E5%89%8D%E7%AB%AF%E5%A6%82%E4%BD%95%E5%B0%91%E8%B5%B0%E5%BC%AF%E8%B7%AF/","title":"入门前端？如何少走弯路！！！"},{"content":"修改站点背景颜色 在\u0026quot;assets/scss/variables.scss\u0026quot;中找到 1 --body-background: #f6f6f6; 或在\u0026quot;assets/scss/customs.scss\u0026quot;中找到 1 --body-background: #f6f6f6; 参考自：Hugo | Hugo-stack-theme 主题魔改版\n友链的添加 在\u0026quot;content/page/links\u0026quot;的 index.md 中有详细解释，图标可通过图床或者本地渲染，模仿格式即可，或可直接把文件内容换为 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 --- title: 友情链接 links: - title: 爱吃鱼的小林 - linsk description: 一个大三程序员,博客讲述了hugo中许多美化stack的主题的教程、前端领域知识以及其他 website: https://linsk27.github.io/ image: fish.png menu: main: weight: -50 params: icon: link comments: false --- 显示文章更新时间 手动添加更新时间 hugo 默认位置为 archetypes/default.md 或者主题下目录下 xx 主题/archetypes/posts.md，加lastmod字段，在创建文章模板里添加以下一行，创建时会以主题目录下的模板来创建。 1 lastmod: { { .Date } } 添加lastmod，有个好处就是可自由修改这个字段的时间。 在 \u0026ldquo;config.toml/yaml/json/\u0026rdquo; 中写明调整这里顺序即可 1 frontmatter: lastmod = [\u0026#34;:git\u0026#34;, \u0026#34;lastmod\u0026#34;, \u0026#34;:fileModTime\u0026#34;, \u0026#34;:defalut\u0026#34;] 自动添加更新时间 记得在\u0026quot;config.toml/yaml/json\u0026quot;开启 gitinfo 1 2 #获取git信息 enableGitInfo = true #设为true 这样就提交代码时，就会去读取 git 时间，来更新文章的更新时间! 但是后面我发现了一个问题：每次重新 git push 之后所有文件最近更新时间 mtime 都变成推送的时刻，这是 git 的一个部署配置问题,因此我搜索了许多博客，终于在hugo github action|vecel 部署后文章更新时间异常修复这篇文章找到了解决方法，总的来说：\n在 \u0026ldquo;.github/workflows/xx.yml\u0026rdquo;,新增以下配置，主要是quotePath，默认情况下，文件名包含中文时，git 会使用引号吧文件名括起来，这会导致 action 中无法读取:GitInfo 变量，所以要设置Disable quotePath 1 2 3 4 5 6 - name: Git Configuration run: | git config --global core.quotePath false git config --global core.autocrlf false git config --global core.safecrlf true git config --global core.ignorecase false 使用checkout的话 fetch-depth 需要设为 0，depth默认是为 1，默认只拉取分支最近一次 commit，可能会导致一些文章的 GitInfo 变量无法获取，设为 0 代表拉去所有分支所有提交。 1 2 3 uses: actions/checkout@v4 with: fetch-depth: 0 #设为0 以下是我的最终的 yml 配置文件,大家可以参考\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 name: deploy # 代码提交到main分支时触发github action on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest env: TZ: Asia/Shanghai # 添加正确的时区 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Git Configuration run: | git config --global core.quotePath false git config --global core.autocrlf false git config --global core.safecrlf true git config --global core.ignorecase false - name: Setup Hugo uses: peaceiris/actions-hugo@v3 with: hugo-version: \u0026#34;latest\u0026#34; extended: true - name: Build Web run: hugo -D - name: Deploy Web uses: peaceiris/actions-gh-pages@v4 with: PERSONAL_TOKEN: ${{ secrets.TOKEN }} EXTERNAL_REPOSITORY: linsk27/linsk27.github.io PUBLISH_BRANCH: main PUBLISH_DIR: ./public commit_message: auto deploy 更新时间显示在卡片 stack 主题的文章更新时间在文章底部 若想在文章开头就显示更新时间，修改 \u0026ldquo;layouts/partials/article/components/detail.html\u0026rdquo;，在指定位置引入以下代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;div class=\u0026#34;article-details\u0026#34;\u0026gt; ... \u0026lt;footer class=\u0026#34;article-time\u0026#34;\u0026gt; ... \u0026lt;!-- 更新时间 --\u0026gt; {{- if ne .Lastmod .Date -}} \u0026lt;div class=\u0026#34;article-lastmod\u0026#34;\u0026gt; {{ partial \u0026#34;helper/icon\u0026#34; \u0026#34;clock\u0026#34; }} \u0026lt;time\u0026gt; {{ .Lastmod.Format ( or .Site.Params.dateFormat.lastUpdated \u0026#34;Jan 02, 2006 15:04 MST\u0026#34; ) }} \u0026lt;/time\u0026gt; \u0026lt;/div\u0026gt; {{- end -}} .... \u0026lt;/footer\u0026gt; ... \u0026lt;/div\u0026gt; 这样就会文章开头显示修改时间 显示文章字数统计 在\u0026quot;config.toml/yaml/json\u0026quot;的.params.article中，添加 1 wordCount: true #打开字数统计 注意的是默认 WordCount 是不统计中文字数的，你需要在\u0026quot;config.toml/yaml/json\u0026quot;中添加 1 hasCJKLanguage: true 在\u0026quot;layouts/partials/article/components/details.html\u0026quot;中的 \u0026lt;footer class=\u0026quot;article-time\u0026quot;\u0026gt;内，打开本地 hugo 静态调试，按照位置添加 1 2 \u0026lt;i class=\u0026#34;fa fa-regular fa-message\u0026#34; style=\u0026#34;color: #ff8e2d;\u0026#34;\u0026gt;\u0026lt;/i\u0026gt; 本文有 {{ .WordCount }} 个字 效果如图 修改分类标签的颜色 在\u0026quot;content/categories\u0026quot;创建与分类同名的文件夹，并且在文件夹中创建\u0026quot;_index.md\u0026quot;文件并添加下面的内容,文件内会有一个 Test 的例子，可以模仿 Test 文件夹里面的内容 1 2 3 4 5 6 7 8 ---- title: 这里写分类名称 description: 分类简介，不需要可以删了 image: \u0026#34;categories.png\u0026#34; // 分类的题图 style: background: \u0026#34;#80aba9\u0026#34; //分类标签底色 color: \u0026#34;#fff\u0026#34; ---- 背景脚本放置位置 dev\\themes\\hugo-theme-stack\\layouts\\partials\\footer ","date":"2024-10-19T00:00:00Z","image":"https://linsk27.github.io/p/hugo%E4%B8%BB%E9%A2%98%E7%BE%8E%E5%8C%96%E6%95%99%E7%A8%8B/hugo_hu_a06c8814bf2a76fe.jpg","permalink":"https://linsk27.github.io/p/hugo%E4%B8%BB%E9%A2%98%E7%BE%8E%E5%8C%96%E6%95%99%E7%A8%8B/","title":"【Hugo】主题美化教程"},{"content":" nexus 下载安装 nuxes 下载链接\n链接：https://pan.baidu.com/s/1M-cotx78rZfoPAqLzMquGA 提取码：ogh0 1.点击.exe 文件进行安装后显示 2.任务栏透明（可以都去实现）\n方式一：安装 translucenTtb​ 插件\n方式二：在设置设置任务栏自动隐藏图标\n将桌面的图标都隐藏\n进行 nexus 配置（不做过多的图片解释，可以自己一边调试一边研究，找出自己最爱的风格） 1.点击 nexus 任务栏的 nexus 打开软件\n2.点击位置将停靠设置在底部，并且根据自己的喜好进行偏移参数调整后点击应用\n3.点击“行为”,打开 dock 透明度，将 dock 透明度拉满\n4.点击“效果”，把悬浮之类的效果都可以设置为弹跳，同时设置水波纹效果等\n5. 点击“一般”，勾选隐藏系统任务栏(可勾可不勾)，勾选右下角操作栏消失\n图标的添加 1.右键 nuxes 的自带图标，将没用的图标删除\n2.右键 nexes 的自带 win 图标，点击创建 dock 项目\n3.点击查找，获取对应需要的软件 exe 文件后会自动完善信息，点击确定即可\n4.配置完成\n添加音频条和时钟组件 日后出更新以及教程噢~~ 恭喜你完成本文教程，最终配置效果如封面图，可以通过 wallpaper 找个动态壁纸，制作属于自己好看的桌面！ ","date":"2024-10-15T00:00:00Z","image":"https://linsk27.github.io/p/nexus%E6%A1%8C%E9%9D%A2%E7%BE%8E%E5%8C%96%E6%95%99%E7%A8%8B/nexus6_hu_9c650a811a642bf0.png","permalink":"https://linsk27.github.io/p/nexus%E6%A1%8C%E9%9D%A2%E7%BE%8E%E5%8C%96%E6%95%99%E7%A8%8B/","title":"【Nexus】桌面美化教程"},{"content":"数组去重方法 1. 通过 filter 过滤函数 JavaScript 的 Array.prototype.filter() 方法可以创建一个新数组，其包含通过所提供函数实现的测试的所有元素。利用这个特性，我们可以对数组进行去重。\n1 2 3 4 5 const array = [1, 2, 2, 3, 4, 4, 5]; const uniqueArray = array.filter((item, index, self) =\u0026gt; { return self.indexOf(item) === index; }); console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5] 2. 通过 new Set() 集合进行去重 Set 是 ES6 引入的一种新的数据结构，它类似于数组，但是成员的值都是唯一的，没有重复的值。我们可以利用这个特性进行数组去重。\n1 2 3 const array = [1, 2, 2, 3, 4, 4, 5]; const uniqueArray = [...new Set(array)]; console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5] 3. for 循环遍历判断 虽然 filter 和 Set 是更现代且效率更高的方法，但了解如何使用 for 循环进行去重也很有用。\n1 2 3 4 5 6 7 8 const array = [1, 2, 2, 3, 4, 4, 5]; const uniqueArray = []; for (let i = 0; i \u0026lt; array.length; i++) { if (uniqueArray.indexOf(array[i]) === -1) { uniqueArray.push(array[i]); } } console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5] ","date":"2024-10-14T00:00:00Z","permalink":"https://linsk27.github.io/p/js%E4%B8%AD%E6%95%B0%E7%BB%84%E5%8E%BB%E7%9A%84%E5%B8%B8%E8%A7%84%E5%8E%BB%E9%87%8D%E6%96%B9%E6%B3%95/","title":"js中数组去的常规去重方法"},{"content":" 该文章图片从网上摘录 对linux的初步准备 1.下载最新版ubuntu和UltraISo、DiskGenius工具 ubuntu下载链接：(https://cn.ubuntu.com/download/desktop)\nUltraISo、DiskGenius下载链接：（https://pan.baidu.com/s/17AwjX2AO7E5fHjYWQmINPQ）提取码：7777\n2.进行u盘刻录 插入自备u盘，注！！！u盘进行录入时会覆盖所有u盘数据\n(1).解压并打开ULtralSo，输入注册名【Guanjiu】，输入注册码【A06C-83A7-701D-6CFC】，点击【确定】\n(2).点击【文件】\n(3).点击【打开】\n(4).选中打开下载好的ubuntu.iso\n(5).点击【写入硬盘映像】\n(6).点击【写入】,选择确定继续操作\n(7).u盘刻录成功\n进行磁盘分区 1.解压并且打开DiskGenius分盘工具 （本人由于磁盘管理器无法分盘所以使用了第三方分盘工具）\n(1).鼠标右击一个磁盘（容量大的）,选择【新建分区】\n(2).所分配的新分区不少于20g，点击【开始】\n(3).点击【是】\n(4).点击【完成】\n开始安装系统 1.系统安装（由于安装时没有进行截图，这里的图片摘录自网上 (1).重启电脑，并且在开机画面出现后按本机电脑的【快捷键】（各品牌电脑的启动选择不一样，我的电脑是华硕，点击【ESC】按键）进入启动选择界面\n(2).点击UEFI选项即选择自己的u盘介质\n(3).进入后我这里选择的是第一项【尝试或者安装ubantu】，然后进入安装界面\n(4).选择中文并且安装Ubuntu\n(5).选择chinese后，点击【继续】\n(6).点击【继续】\n(7).选择【其他选项】\n(8).到这个时候会有两种情况，\n第一种是“这台计算机似乎没有安装操作系统”，此时说明为单系统刷机，点击【继续】即可，本文讲的是双系统安装\n第二种是双系统安装，此时会进入分盘界面，选择空闲磁盘进行四次分区\n(9)分区完成后点击【现在安装】，点击【继续】\n(10)输入用户名密码即可开始等待安装\n(11)安装完成后重启\n2.完成安装 (1)重启后，每次开机时会出现三个选项，点击ubuntu进入ubuntu系统，点击win回到原系统\n(2)输入密码进入用户界面\n","date":"2024-10-14T00:00:00Z","permalink":"https://linsk27.github.io/p/linux%E7%9A%84%E5%AE%89%E8%A3%85ubuntu/","title":"Linux的安装（Ubuntu）"},{"content":"一 使用方法 userList数据循环在列表，下拉框显示的值为userList里面的label字段的值，实际值为对应label的uuid值，如下面例子中输出选中小王的值应该为01\n1 2 3 4 5 6 7 8 9 10 \u0026lt;template\u0026gt; \u0026lt;el-select v-model=\u0026#34;form\u0026#34;\u0026gt; \u0026lt;el-option v-for=\u0026#34;item in userList\u0026#34; :key=\u0026#34;item.uuid\u0026#34; :label=\u0026#34;item.username\u0026#34; :value=\u0026#34;item.uuid\u0026#34; /\u0026gt; \u0026lt;/el-select\u0026gt; \u0026lt;/template\u0026gt; 1 2 3 4 5 const userList = ref([{ username:\u0026#39;小王\u0026#39;, uuid:\u0026#39;01\u0026#39; }]) const form = ref\u0026lt;any\u0026gt;() 二 传值问题（匹配功能） 当我们在子组件进行表单操作的时候，需要从父组件往子组件传相关操作数据的值，若传入form的值是小王，则form在下拉框中显示的值为小王,实际值也为小王，若传入的值为01,则下拉框中显示的值为小王，实际值依旧为01\n","date":"2024-10-13T00:00:00Z","permalink":"https://linsk27.github.io/p/el-select%E6%A0%87%E7%AD%BE%E5%AD%98%E5%9C%A8%E7%9A%84%E6%95%B0%E6%8D%AE%E5%8C%B9%E9%85%8D%E9%97%AE%E9%A2%98/","title":"el-select标签存在的数据匹配问题"},{"content":"[vue/no-use-v-if-with-v-for] 这个错误信息是由 ESLint 的 Vue.js 插件 eslint-plugin-vue 产生的。它指出在你的 Vue.js 模板中，v-if 和 v-for 指令被不当地一起使用在了同一个元素上。\n为什么会产生这个错误？ 在 Vue.js 中，当 v-if 和 v-for 同时用在同一个元素上时，Vue 会先处理 v-for，然后处理 v-if。这意味着即使 v-if 最终会过滤掉一些元素，v-for 仍然会遍历整个列表，这可能会导致不必要的计算开销，特别是在处理大型列表时。\n如何修复这个错误？ 根据 ESLint 的建议，你应该将 v-if 移动到包含 v-for 的父元素（或“包装元素”）上。这样可以确保 v-if 的条件判断只执行一次，而不是对 v-for 生成的每个元素都执行。\n错误的示例： 1 2 3 \u0026lt;div v-for=\u0026#34;item in items\u0026#34; v-if=\u0026#34;item.active\u0026#34; :key=\u0026#34;item.id\u0026#34;\u0026gt; {{ item.name }} \u0026lt;/div\u0026gt; 在这个例子中，v-if 会在每次迭代时都执行，检查 item.active 是否为真。\n正确的示例： 1 2 3 4 5 \u0026lt;div v-if=\u0026#34;hasActiveItems\u0026#34;\u0026gt; \u0026lt;div v-for=\u0026#34;item in activeItems\u0026#34; :key=\u0026#34;item.id\u0026#34;\u0026gt; {{ item.name }} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; 在这个修正后的例子中，首先通过一个计算属性 hasActiveItems 或在 data、computed 或 methods 中定义的逻辑来检查是否有活跃的项。如果有，则使用 v-for 来渲染这些活跃的项。这里的 activeItems 应该是一个只包含活跃项的数组。\n总结 通过将 v-if 移动到 v-for 的外部，你可以优化你的 Vue 组件的性能，避免不必要的渲染和计算。这也是 Vue.js 官方文档和 ESLint 插件推荐的最佳实践。\n","date":"2024-10-12T00:00:00Z","permalink":"https://linsk27.github.io/p/v-if%E5%92%8Cv-for%E4%B8%BA%E4%BD%95%E4%B8%8D%E8%AF%A5%E5%90%8C%E6%97%B6%E4%BD%9C%E7%94%A8%E4%BA%8E%E4%B8%80%E4%B8%AA%E6%A8%A1%E5%9D%97/","title":"v-if和v-for为何不该同时作用于一个模块？"}]