普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月23日首页

彻底搞懂 SSH 与 Git 的“幕后交易”

作者 ssshooter
2025年12月23日 22:01

很多开发者在配置 GitHub SSH 的时候,通常是按照网上的教程,噼里啪啦一顿复制:先 ssh-keygen,再把公钥贴到 GitHub,最后 git push。如果一切顺利,你会觉得这是一种“魔法”,但一旦报错,往往就一脸懵逼。

其实,这背后是一套非常逻辑严密的“身份代换”过程。

1. 钥匙与锁:非对称加密的“挑战”

SSH 的核心是非对称加密。当你运行 ssh-keygen 时,你其实是产生了一对关系:**私钥(Private Key)**留在你电脑里,相当于你的指纹;**公钥(Public Key)**上传到 GitHub,相当于在 GitHub 门口装了一个只认你指纹的扫描仪。

当你尝试 git push 时,并不是直接发送密码,而是一个“挑战与应答”的过程:

  1. GitHub 拿你的公钥加密一段随机信息发给你。
  2. 只有你电脑里的私钥能解开这段信息并签上名发回去。
  3. GitHub 验证签名成功,门开了。

这就是为什么 SSH 比账号密码安全得多:即便网络被监听,黑客也拿不到你的私钥。

2. 管理员 ssh-agent:你的“钥匙包”

如果你电脑里有很多把钥匙(比如公司的、个人的、不同服务器的),你不可能每次推送代码都手动指定用哪把。

这时候 ssh-add 就出场了。它把你的私钥加载进一个叫 ssh-agent 的后台管家那里。当你发起连接时,这个管家会像拿着一大串钥匙的保安,挨个去试。

但这里有个陷阱:试错是有上限的。如果管家试了 5-6 把钥匙都没对上,GitHub 就会觉得你在暴力破解,直接切断连接。这就是为什么有时候明明钥匙是对的,却依然报错 Too many authentication failures

3. SSH Config:这才是真正的“高级配置”

为了不让管家“盲目试错”,我们需要一个名为 config 的配置文件(在 ~/.ssh/ 目录下)。

这个文件就像一张精确的地图,它告诉 SSH:

  • “如果是去 GitHub,请直接用这把名为 id_ed25519_me 的钥匙。”
  • “不要去试其他的钥匙(IdentitiesOnly yes)。”
  • “甚至如果 22 端口被封了,你可以偷偷走 443 端口。”

有了它,SSH 就不再是猜测,而是精准导航。

4. Git 与 SSH 的“外包关系”

很多人分不清 Git 地址和 SSH 地址。其实,当你看到 git@github.com:user/repo.git 这种地址时,它本质上就是一个 SSH 路径

  • Git 是货物:它只负责打包你的代码变更(Commit)。
  • SSH 是隧道:它负责把这些货物安全地送到远程仓库(Push)。

Git 其实很懒,它根本不负责身份校验。它只是把地址里的 github.com 丢给系统里的 SSH 客户端,说:“喂,帮我连上这个地方,我要发货。”

这时候,SSH 客户端就会去翻你的 config 文件,找对应的钥匙,建立隧道。隧道一旦修通,Git 就在里面欢快地传输数据。

5. 总结:一个完整的链路

当你完成一次成功的提交并推送时,底层逻辑是这样的:

  1. Git Commit:在本地把修改存进仓库。
  2. Git Push:识别到地址是 SSH 格式,呼叫 SSH 客户端。
  3. SSH 寻址:查看 ~/.ssh/config,确定去哪个 IP、用哪个端口、带哪把钥匙。
  4. 身份验证:通过 ssh-agent 提供的私钥与 GitHub 上的公钥完成“暗号对接”。
  5. 数据传输:身份确认,隧道开启,Git 开始搬运代码。

一句话总结:

ssh-keygen 造了钥匙,GitHub 拿了模具,ssh-add 把它挂在腰间,config 给了你一张地图,而 Git 则是那个顺着地图走、拿着钥匙开门送货的快递员。

莱特光电:拟通过控股子公司开展石英纤维电子布新业务

2025年12月23日 20:54
36氪获悉,莱特光电公告,公司拟通过控股子公司莱特夸石开展新业务,聚焦高端电子材料领域,主要从事石英纤维电子布(简称“Q布”)的研发、生产与销售。截至目前,莱特夸石已完成核心团队组建,处于业务规划与产能建设阶段。Q布作为第三代高端低介电电子布,其介电性能、耐热性等核心指标优于传统玻璃纤维布,为新一代信息技术产业发展提供重要支撑,行业具备良好的发展前景。

航宇微:股东拟合计减持公司不超2.02%股份

2025年12月23日 20:35
36氪获悉,航宇微公告,持股6.63%的公司名誉董事长、首席科学家颜军计划以集中竞价或大宗交易等方式,减持公司股份不超过1154万股(占公司总股本比例为1.66%)。持股1.43%的公司股东吴郁琪计划以集中竞价或大宗交易等方式,减持公司股份不超过249.5万股(占公司总股本比例为0.36%)。

别再只会调 API 了!LangChain.js 才是前端 AI 工程化的真正起点

2025年12月23日 20:07

2025 年,如果你还在代码里写:
const res = await fetch('https://api.openai.com/v1/chat/completions')

那你大概率,正在亲手制造一个未来必然崩塌的 AI 屎山工程

随着 DeepSeek 等国产大模型的爆发,前端开发者的主战场已经彻底转移:

❌ 不再是:我会不会调用模型 API
✅ 而是:我能不能编排一个稳定、可扩展、可演进的 AI 工作流

LangChain.js,正是这场迁移里的标准答案


一、LangChain 到底是什么?为什么它成了“AI 工程事实标准”?

在写任何代码之前,我们必须搞清楚一件事:

LangChain ≠ AI 模型
LangChain = AI 应用的“工程骨架”

一个前端能秒懂的比喻

  • 🧠 大模型(LLM) :一个聪明但失忆、不会用工具的大脑

  • 🦴 LangChain:让这个大脑

    • 能记事
    • 会用工具
    • 能拆任务
    • 会按流程干活

它解决的不是“模型聪不聪明”,而是工程层面的三大致命痛点

❌ 痛点 1:逻辑碎片化

复杂业务 ≠ 一次 Prompt
LangChain 的 Chain,让多步推理变成流水线。

❌ 痛点 2:数据孤岛

模型不知道你的:

  • PDF
  • 私有文档
  • 数据库

LangChain 的 RAG(检索增强生成) ,让模型“读你自己的数据”。

❌ 痛点 3:模型锁死

今天 OpenAI
明天 DeepSeek
后天再换别的?

LangChain 用 统一接口,让你实现真正的 模型自由

一句话总结
👉 LangChain 之于 AI,就像 React 之于浏览器Express 之于 Node.js


二、LangChain 的“四大支柱”,你代码里其实已经用到了

你写的实战代码,其实已经踩在 LangChain 的核心设计上,只是你可能没意识到。

🧱 1. Model I/O(模型输入输出)

  • PromptTemplate
  • ChatModel
  • OutputParser

👉 负责 “怎么喂模型、怎么拿结果”


🧱 2. Retrieval(检索 / RAG)

  • 文档切片
  • 向量化
  • 相似度搜索

👉 负责 “让模型说你自己的话”


🧱 3. Chains(执行链)

LangChain 的灵魂

把 Prompt / Model / Parser
像管道一样串起来。


🧱 4. Agents(智能体)

不给流程
只给工具

让 AI 自己决定:

  • 搜索?
  • 算数?
  • 写代码?

👉 这是 AI 从“脚本”走向“自治” 的起点。


三、Prompt 工程化:像写 React 组件一样写 Prompt

你代码里最亮眼的一点,是这一段 👇

import { PromptTemplate } from '@langchain/core/prompts';

const prompt = PromptTemplate.fromTemplate(`
你是一个 {role}
请用不超过 {limit} 字回答以下问题:
{question}
`);

💡 为什么这是“工程级 Prompt”?

因为它解决了 提示词三大老问题

✅ 1. 解耦

Prompt 不再是字符串拼接
而是 配置 + 参数

✅ 2. 可维护

  • 可版本化
  • 可复用
  • 可测试

✅ 3. 防手滑

占位符校验,避免:

“怎么模型突然胡说八道了?”

类比一句话
👉 PromptTemplate 就是 AI 世界里的 Props


四、DeepSeek 丝滑接入:这才是适配器模式的正确用法

import { ChatDeepSeek } from '@langchain/deepseek';

const model = new ChatDeepSeek({
  model: 'deepseek-reasoner',
  temperature: 0.7,
});

为什么 LangChain 对前端特别友好?

因为各家模型虽然长得像 OpenAI,但细节全是坑:

  • BaseURL 不同
  • Auth 不同
  • Token 计算不同

LangChain 的 Provider 层,把这些脏活全吃掉了

你只需要:

关心业务,不关心模型厂商


五、真正的灵魂:.pipe() 与 LCEL 声明式编排

const chain = prompt.pipe(model);

这行代码,是整篇文章的技术核心

什么是 LCEL(LangChain Expression Language)?

它借鉴的是 Unix 管道思想

cat file | grep keyword | sort

在 LangChain 中:

Prompt → Model → Output

对比一下就懂了

❌ 命令式(越写越乱):

const text = await prompt.format(...)
const res = await model.invoke(text)

✅ 声明式(可组合、可扩展):

const chain = prompt.pipe(model)
await chain.invoke(...)

LCEL 的真正价值在于

  • 天然支持 Streaming
  • 天然支持并发
  • 天然可观测

六、进阶实战:多链协作,才是 AI 工程的常态

真实场景几乎从来不是“一问一答”。

你这个例子,非常典型,也非常高级 👇

const fullChain = RunnableSequence.from([
  (input) => explainChain.invoke({ topic: input.topic }).then(r => r.text),
  (explanation) =>
    summaryChain.invoke({ explanation }).then(r =>
      `【详情】${explanation}\n【总结】${r.text}`
    )
]);

这段代码本质上在干什么?

👉 AI 流水线加工

  • 原子性:每个 Chain 都能单测
  • 组合性:像搭积木一样拼
  • 闭环性:前端只管一个输入

这,才配叫 AI 工程化


七、前端 AI 实战避坑指南

⚠️ 1. 永远别把 API Key 写在浏览器

LangChain = Node / Serverless 专属

⚠️ 2. Temperature 不是随便填的

  • 0.0:代码 / 数学
  • 0.7:博客 / 对话
  • 1.0+:创作 / 发散

⚠️ 3. 一定要用 Streaming

chain.stream()

体验差距 = ChatGPT vs 普通输入框


八、结语:你不是不会 AI,你只是缺一套“工程语法”

很多人觉得 AI 开发 = 调包
其实真正的分水岭在于:

你有没有一套可组合、可演进的 AI 结构

LangChain 的意义,不是让模型更聪明
而是让 人类工程师重新掌控复杂度


🎯 最后一句话送你

Prompt → Model → Chain

这是前端迈入 AI 工程时代的第一性原理。

React/Vue 代理配置全攻略:Vite 与 Webpack 实战指南

作者 前端无涯
2025年12月23日 18:03

在前端开发中,跨域问题是我们绕不开的坎。浏览器的同源策略限制了不同域名、端口或协议之间的资源请求,而开发环境中前端项目(通常是localhost:3000/localhost:5173)与后端接口(如http://api.example.com)往往不在同一域下,直接请求会触发跨域错误。

为了解决开发环境的跨域问题,主流的前端构建工具(Vite、Webpack)都提供了 ** 代理(Proxy)** 功能。代理的核心原理是:以构建工具启动的开发服务器作为中间层,前端请求先发送到开发服务器,再由开发服务器转发到后端接口服务器(服务器之间的请求不受同源策略限制),最后将后端响应返回给前端,从而绕过浏览器的跨域限制。

本文将详细讲解 React 和 Vue 两大主流框架,分别基于Vite(新一代前端构建工具)和Webpack(传统主流构建工具)的代理配置方案,涵盖基础配置、进阶场景和常见问题排查。

一、Vue 框架的代理配置

Vue 项目的构建工具主要分为两种:Vite(Vue 3 推荐)和Vue CLI(基于 Webpack,Vue 2/3 都支持),两者的代理配置方式略有不同。

1.1 Vue + Vite 代理配置

Vite 的代理配置在项目根目录的vite.config.js(或vite.config.ts)文件中,通过server.proxy选项配置。

基础配置示例

假设前端项目运行在http://localhost:5173,后端接口地址为http://localhost:8080/api,我们希望将前端以/api开头的请求代理到后端接口。

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  // 开发服务器配置
  server: {
    port: 5173, // 前端项目端口(默认5173,可自定义)
    open: true, // 启动时自动打开浏览器
    // 代理配置
    proxy: {
      // 匹配以/api开头的请求
      '/api': {
        target: 'http://localhost:8080', // 后端接口的基础地址
        changeOrigin: true, // 开启跨域模拟(关键:让后端认为请求来自target域名)
        rewrite: (path) => path.replace(/^/api/, '') // 路径重写(可选:如果后端接口没有/api前缀,需要去掉)
      }
    }
  }
})

配置项说明

  • target:后端接口的服务器地址(必填)。
  • changeOrigin:是否开启跨域模拟,设置为true时,开发服务器会在转发请求时修改Host请求头为target的域名,避免后端因域名校验拒绝请求(建议始终开启)。
  • rewrite:路径重写函数,用于修改转发到后端的请求路径。例如前端请求/api/user,经过重写后会转发到http://localhost:8080/user(如果后端接口本身带有/api前缀,则不需要此配置)。

测试效果

前端发送请求:

// 原本需要直接请求http://localhost:8080/api/user(跨域)
// 现在直接请求/api/user,会被代理转发
fetch('/api/user')
  .then(res => res.json())
  .then(data => console.log(data))

1.2 Vue + Vue CLI(Webpack)代理配置

Vue CLI 基于 Webpack,其代理配置在项目根目录的vue.config.js文件中,通过devServer.proxy选项配置(底层依赖webpack-dev-server的 proxy 功能)。

基础配置示例

需求与上述一致:将/api开头的请求代理到http://localhost:8080

// vue.config.js
module.exports = {
  devServer: {
    port: 8081, // 前端项目端口
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        pathRewrite: { '^/api': '' } // 路径重写(与Vite的rewrite作用一致,语法不同)
      }
    }
  }
}

注意:Vue CLI 的路径重写使用pathRewrite对象,而 Vite 使用rewrite函数,语法略有差异,但功能一致。

二、React 框架的代理配置

React 项目的构建工具同样分为Vite(新一代)和Create React App(基于 Webpack,简称 CRA,React 官方脚手架)。

2.1 React + Vite 代理配置

React + Vite 的代理配置与 Vue + Vite 完全一致,因为 Vite 的代理功能是框架无关的。

基础配置示例

// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/, '')
      }
    }
  }
})

2.2 React + Create React App(Webpack)代理配置

CRA 隐藏了 Webpack 的核心配置,因此其代理配置分为两种方式:简单配置(package.json)进阶配置(setupProxy.js)

方式 1:简单配置(package.json)

适用于单一路径的代理场景,直接在package.json中添加proxy字段。

{
  "name": "react-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  // 简单代理配置:将所有请求代理到http://localhost:8080
  "proxy": "http://localhost:8080"
}

注意:这种方式的局限性很大:

  • 只能配置一个代理目标,无法针对不同路径配置不同代理。
  • 不支持路径重写、HTTPS 等进阶配置。

方式 2:进阶配置(setupProxy.js)

适用于多路径代理、路径重写等复杂场景,需要创建src/setupProxy.js文件,并安装http-proxy-middleware依赖(CRA 已内置该依赖,若未安装可手动执行npm install http-proxy-middleware --save-dev)。

基础配置示例(匹配 /api 路径)
// src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware')

module.exports = function(app) {
  app.use(
    // 匹配以/api开头的请求
    '/api',
    createProxyMiddleware({
      target: 'http://localhost:8080',
      changeOrigin: true,
      pathRewrite: { '^/api': '' }
    })
  )
}

多代理规则示例

如果需要将/api代理到http://localhost:8080,将/admin代理到http://localhost:9090,可以配置多个代理规则:

// src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware')

module.exports = function(app) {
  // 代理/api到8080端口
  app.use(
    '/api',
    createProxyMiddleware({
      target: 'http://localhost:8080',
      changeOrigin: true,
      pathRewrite: { '^/api': '' }
    })
  )
  // 代理/admin到9090端口
  app.use(
    '/admin',
    createProxyMiddleware({
      target: 'http://localhost:9090',
      changeOrigin: true,
      pathRewrite: { '^/admin': '' }
    })
  )
}

注意:修改setupProxy.js后,需要重启 CRA 的开发服务器才能生效。

三、代理的进阶配置场景

除了基础的代理配置,我们还会遇到一些复杂场景,比如代理 HTTPS 接口、携带 Cookie、匹配正则路径等。

3.1 代理 HTTPS 接口

如果后端接口是 HTTPS 协议(如https://api.example.com),需要添加secure: false配置,忽略 SSL 证书验证(开发环境下常用,生产环境不建议)。

Vite 配置示例

proxy: {
  '/api': {
    target: 'https://api.example.com',
    changeOrigin: true,
    secure: false, // 忽略HTTPS证书验证
    rewrite: (path) => path.replace(/^/api/, '')
  }
}

Webpack(Vue CLI/CRA)配置示例

// Vue CLI:vue.config.js
proxy: {
  '/api': {
    target: 'https://api.example.com',
    changeOrigin: true,
    secure: false,
    pathRewrite: { '^/api': '' }
  }
}

// CRA:setupProxy.js
createProxyMiddleware({
  target: 'https://api.example.com',
  changeOrigin: true,
  secure: false,
  pathRewrite: { '^/api': '' }
})

3.2 跨域携带 Cookie

如果需要在跨域请求中携带 Cookie(如用户登录状态),需要同时配置前端请求的withCredentials和代理的cookieDomainRewrite

前端请求配置

// fetch请求
fetch('/api/user', {
  credentials: 'include' // 携带Cookie
})

// axios请求
axios.get('/api/user', {
  withCredentials: true // 携带Cookie
})

代理配置

// Vite
proxy: {
  '/api': {
    target: 'http://localhost:8080',
    changeOrigin: true,
    rewrite: (path) => path.replace(/^/api/, ''),
    cookieDomainRewrite: 'localhost' // 将后端返回的Cookie域名重写为前端域名
  }
}

// Webpack
createProxyMiddleware({
  target: 'http://localhost:8080',
  changeOrigin: true,
  pathRewrite: { '^/api': '' },
  cookieDomainRewrite: 'localhost'
})

3.3 正则匹配路径

如果需要匹配更复杂的路径(如以/api/v1/api/v2开头的请求),可以使用正则表达式作为代理的匹配规则。

Vite 配置示例

proxy: {
  // 匹配以/api/v开头的请求
  '^/api/v\d+': {
    target: 'http://localhost:8080',
    changeOrigin: true,
    rewrite: (path) => path.replace(/^/api/, '')
  }
}

Webpack(CRA)配置示例

app.use(
  // 正则匹配/api/v1或/api/v2
  /^/api/v\d+/,
  createProxyMiddleware({
    target: 'http://localhost:8080',
    changeOrigin: true,
    pathRewrite: { '^/api': '' }
  })
)

四、代理配置常见问题排查

配置代理后,如果请求仍然失败,可从以下几个方面排查:

4.1 代理规则未匹配

  • 检查前端请求的路径是否与代理的匹配规则一致(如前端请求/api/user,代理规则是/api,则匹配;若请求/user,则不匹配)。
  • 正则匹配时,注意正则表达式的语法是否正确(如转义字符、量词等)。

4.2 路径重写错误

  • 如果后端接口没有/api前缀,但代理配置了rewrite: (path) => path.replace(/^/api/, ''),则前端请求/api/user会被转发到/user;若后端接口有/api前缀,去掉重写配置即可。
  • 检查路径重写的语法(Vite 用rewrite函数,Webpack 用pathRewrite对象)。

4.3 changeOrigin 未开启

  • 若后端接口有域名校验(如只允许特定域名访问),未开启changeOrigin: true会导致后端拒绝请求,此时需要开启该配置。

4.4 后端接口地址错误

  • 检查target配置的后端地址是否正确(包括域名、端口、协议),可直接在浏览器中访问后端接口地址,确认接口是否正常。

4.5 开发服务器未重启

  • 修改 Vite/Vue CLI 的配置文件后,开发服务器会自动重启;但修改 CRA 的setupProxy.js后,需要手动重启开发服务器才能生效。

五、总结

开发环境的代理配置是解决跨域问题的最优方案,不同构建工具的配置方式虽有差异,但核心原理一致。

  • Vite:配置简洁,框架无关,通过server.proxy实现,支持函数式路径重写和正则匹配。
  • Webpack:Vue CLI 通过devServer.proxy配置,CRA 则分为简单的package.json配置和进阶的setupProxy.js配置,底层依赖http-proxy-middleware

在实际开发中,我们可以根据项目的框架(React/Vue)和构建工具(Vite/Webpack)选择对应的配置方式,并根据业务需求添加路径重写、HTTPS 支持、Cookie 携带等进阶配置。同时,遇到问题时可按照 “规则匹配→路径重写→跨域配置→接口地址” 的顺序排查,快速定位问题。

最后需要注意:代理配置仅适用于开发环境,生产环境的跨域问题需要通过后端配置 CORS(跨域资源共享)或 Nginx 反向代理来解决。

回顾计算属性的缓存与监听的触发返回结果

2025年12月23日 17:13

前言

网上的给出的区别有很多,今天只是简单来回归下其中的一些流程和区别的关键点。

以下面的代码为例进行思考:

<template>
  <div class="calc">
    <input v-model.number="n1" type="number" placeholder="数字1">
      <input v-model.number="n2" type="number" placeholder="数字2">
        <p>和:{{ sum }} 平均值:{{ avg }}</p>
        <input v-model="name" placeholder="用户名">
          <p :style="{color: tipColor}">{{ tip }}</p>
        </div>
</template>

<script>
  export default {
    data() {
      return { n1: 0, n2: 0, name: '', tip: '', tipColor: '#333' }
    },
    computed: {
      sum() { return this.n1 + this.n2 },
      avg() { return this.sum / 2 }
    },
    watch: {
      name(v) {
        if (!v.trim()) { this.tip = '不能为空'; this.tipColor = 'red' }
        else if (v.length < 3) { this.tip = '不少于3位'; this.tipColor = 'orange' }
        else { this.tip = '可用'; this.tipColor = 'green' }
      }
    }
  }
</script>

<style scoped>
  .calc { padding: 20px; border: 1px solid #eee; width: 300px; margin: 20px auto; }
  input { display: block; width: 100%; margin: 8px 0; padding: 6px; border: 1px solid #ccc; border-radius: 4px; }
</style>

关于 watch 和 computed 的使用我们很多,这里我们不一一介绍,但是请记住:监听是不需要 return 的,计算属性是百分百必须要有的。

1、监听和计算属性的区别

最关键的区别:

1、监听是没有缓存的,计算属性是有缓存的

2、监听是不需要有返回值的,但是机损属性是必须要有返回值的。(极限情况下不return基本没意义)

其他的区别:

对比维度 计算属性(computed) 监听器(watch)
核心用途 基于已有数据推导 / 计算新数据(数据转换 / 组合) 监听已有数据的变化,执行异步操作或复杂逻辑(无新数据产出,侧重 “副作用”)
依赖关系 只能依赖 Vue 实例中的响应式数据(data/props/ 其他 computed),自动感知依赖变化 可监听单个响应式数据、对象属性、数组,甚至通过 deep: true监听对象深层变化,支持手动指定监听目标
使用场景 1. 简单数据拼接(如全名:firstName + lastName)2. 数据格式化(如时间戳转日期字符串)3. 依赖多数据的计算(如总价:price * count)4. 需缓存的重复计算场景 1. 异步操作(如监听输入框变化,延迟请求接口获取联想数据)2. 复杂逻辑处理(如监听用户状态变化,同步更新权限菜单)3. 监听对象深层变化(如监听表单对象,统一处理提交前校验)4. 数据变化后的联动操作(非数据推导类)
是否支持异步 不支持异步操作:若在 computed中使用异步(如定时器、接口请求),无法正确返回推导结果,会得到 undefined 支持异步操作:这是 watch的核心优势之一,可在监听函数中执行任意异步逻辑

2: 监听和计算属性的基本触发流程:

核心逻辑:set → Dep → Watcher → watch/computed 联动流程

无论是 watch 还是 computed,底层联动流程的核心一致,仅在 Watcher 执行逻辑上有差异,完整流程如下:

第一步:初始化阶段 —— 依赖收集(get 拦截器 + Dep + Watcher 绑定)

  1. computed ****的依赖收集
  1. 组件初始化时,会为每个计算属性创建一个「计算属性 Watcher」; 1. 执行计算属性的 get 方法,访问依赖的响应式数据(如 this.num1); 1. 触发该数据的 get 拦截器,get 拦截器会将当前「计算属性 Watcher」添加到该数据的 Dep 依赖列表中; 1. 所有依赖数据都完成 Watcher 绑定,最终缓存计算属性的初始结果。
  1. watch ****的依赖收集
    1. 组件初始化时,会为每个 watch 监听目标创建一个「普通 Watcher」;
    2. 主动读取一次监听目标数据(如 this.username),触发该数据的 get 拦截器;
    3. get 拦截器将当前「普通 Watcher」添加到该数据的 Dep 依赖列表中;
    4. 若开启 deep: true,会递归遍历对象 / 数组的内部属性,完成深层依赖收集。

第二步:更新阶段 ——set 拦截器触发 Watcher 执行

当修改响应式数据时(如 this.num1 = 10),触发底层联动:

  1. 数据被修改,触发该数据的 set 拦截器;
  2. set 拦截器调用对应 Dep 的 notify() 方法(派发更新通知);
  3. Dep 遍历自身的依赖列表,通知所有绑定的 Watcher「数据已更新」;
  4. 不同类型的 Watcher 接收通知后,执行差异化操作(这是 watch 和 computed 表现不同的核心原因):
    • 普通 Watcher (对应 watch :收到通知后,立即执行 watch 的回调函数,传入 newVal 和 oldVal,执行异步 / 复杂逻辑;
    • 计算属性 Watcher (对应 computed :收到通知后,仅将自身标记为「脏状态」(缓存失效) ,不立即执行计算逻辑,等待下次访问计算属性时,才重新执行 get 方法计算新结果并更新缓存。

举个例子:

  watch: {
    num(newVal, oldVal) {
      console.log(`【watch】:num从${oldVal}变为${newVal},我立即执行回调`);
    }
  },

当 set 触发 watcher 后,watcher 就会立即触发:

num(newVal, oldVal) {
      console.log(`【watch】:num从${oldVal}变为${newVal},我立即执行回调`);
    }

什么叫 收到通知后, 仅将自身标记为「脏状态」(缓存失效) ,不立即执行计算逻辑,等待下次访问计算属性时,才重新执行 get 方法计算新结果并更新缓存。

举个例子:

<template>
  <div>
    <!-- 这里就是「访问计算属性」:模板渲染时会读取 numDouble 的值 -->
    <p v-if="num < 2">两倍数:{{ numDouble }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return { num: 1 };
  },
  computed: {
    numDouble() {
      console.log("计算属性执行计算");
      return this.num * 2;
    },
  },
  mounted() {
    setTimeout(() => {
    this.num = 5; // 2秒后,v-if不成立
  }, 2000);
  setTimeout(() => {
    this.num = 1; // 4秒后,v-if再次成立
  }, 4000);
  },
};
</script>

// 控制台只会打印2次“计算属性执行

为什么只打印 2 次:

原因就是我们的计算属性在 2s 后没有执行

1、 当初始化时,页面中 v-if 条件是符合的,会执行一次 get 计算得到返回值

2、当经过两秒后、 v-if 不符合条件,这个时候表明numDouble 是脏数据,会对其进行标记( v-if 不符合条件,所以无法对numDouble 进行访问, 这里也就是我们说的缓存,缓存计算属性的结果值,当脏状态取消时才会进行新的计算 )

3、当 经过 4 秒后条件再次被满足时,才会有新的计算。

3: 计算属性为什么不能异步

举个例子,我们使用延时进行模仿:

<template>
  <div>
    <!-- 这里就是「访问计算属性」:模板渲染时会读取 numDouble 的值 -->
    <p v-if="num < 2">两倍数:{{ numDouble }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return { num: 1 };
  },
  computed: {
    numDouble() {
      setTimeout(() => {
        return this.num * 2;
      }, 1000);
    },
  },
  mounted() {
    console.log(this.numDouble);
  },
};
</script>

打印结果如下:

为什么会打印 underfined 呢,这里有两个原因?

原因 1:JavaScript 中,任何函数如果没有显式写 return 语句,或 return 后没有跟随具体值,都会默认返回 undefined,这是计算属性返回 undefined 的基础原因。

举个例子:

function test() {
        setTimeout(() => {
          return 11;
        }, 1000);
      }
      setTimeout(() => {
        console.log(test());
      }, 2000);

这样写是属于语法的错误。

原因 2:setTimeout 的异步特性(关键)

即使你在 setTimeout 回调中写了 return this.num * 2,这个返回值也毫无意义,因为 setTimeout异步宏任务

  1. 当 Vue 访问 this.numDouble 时,会立即执行计算属性的函数体;
  2. 函数体执行到 setTimeout 时,只会「注册一个异步任务」,然后直接跳过 setTimeout 继续执行;
  3. 此时计算属性函数体已经执行完毕(没有显式 return),默认返回 undefined,并被 console.log 打印;
  4. 1 秒后,setTimeout 的回调函数才会执行,此时回调中的 return this.num * 2 只是回调函数自身的返回值,无法传递给计算属性,也无法改变之前已经返回的 undefined

简单说:异步回调的返回值,无法成为计算属性的返回值,计算属性会在异步任务注册后,直接默认返回 undefined

也可以分两步走

一、先明确:计算属性的函数体,只在 “被访问” 时同步执行一次(除非满足重新计算条件)

当 mounted 中访问 this.numDouble,或者模板渲染访问 numDouble 时,Vue 会同步、完整地执行一遍 ****numDouble ****函数体的代码,但这个执行过程和 setTimeout 内部的回调是完全分离的:

第一步:计算属性函数体「同步执行」(瞬间完成,不等待异步)

我们把 numDouble 的执行过程拆解成 “逐行执行”,你就能看清流程:

numDouble() {
  // 第1步:执行 setTimeout 这行代码
  // 作用:向浏览器“注册一个1秒后执行的异步任务”,仅此而已
  // 注意:这行代码执行时,不会等待1秒,也不会执行回调函数内部的代码
  setTimeout(() => {
    // 这是回调函数,此时完全没有执行!
    console.log("回调函数开始执行");
    return this.num * 2;
  }, 1000);

  // 第2步:计算属性函数体执行到末尾
  // 没有显式 return,默认返回 undefined
  // 此时,numDouble 已经完成了“返回值”的传递,整个函数体执行结束
}

简单来说就是numDouble 函数体的执行,只做了一件事 ——“安排了一个 1 秒后的任务”,然后就直接返回了 undefined,它不会停下来等 1 秒后回调执行完再返回值。

第二步:1 秒后,异步回调才执行,但为时已晚

  1. 计算属性已经在第一步就返回了 undefined,这个返回值已经被 console.log 打印,也被 Vue 缓存起来了;
  2. 1 秒后,浏览器才会执行 setTimeout 的回调函数,此时回调里的 return this.num * 2 只是 “回调函数自己的返回值”—— 这个值没有任何接收者,既不能传给 numDouble ,也不能改变之前已经返回的 undefined
  3. 更关键的是:回调执行时,numDouble 函数体早就执行完毕了,两者是完全独立的执行流程,回调的返回值无法 “回溯” 给已经执行完的计算属性。

简单来讲就是:

计算属性是要内置返回一个结果的,如果加入异步就会因为执行顺讯返回一个undefined,监听是在事件触发后对写入的回调函数的调用。

《闭包、RAG与AI面试官:一个前端程序员的奇幻LangChain之旅》

作者 Yira
2025年12月23日 17:08

《闭包、RAG与AI面试官:一个前端程序员的奇幻LangChain之旅》

在某个风和日丽的下午,小李——一位自诩“能用一行代码解决的问题绝不用两行”的前端工程师——正坐在工位上,盯着屏幕上一行报错发呆。他不是在调试React组件,也不是在修复CSS样式错乱,而是在尝试用AI模型回答一个看似简单却深不见底的问题:“什么是闭包?

但别误会,这不是一篇枯燥的技术教程。这是一场融合了LangChain魔法、DeepSeek大模型、提示词工程、工作流编排以及一点点程序员幽默感的奇妙冒险。我们将从一行import开始,穿越提示词模板、链式调用、序列化流程,最终抵达AI驱动的智能应用新大陆。准备好了吗?系好安全带,我们的LangChain飞船即将升空!


第一幕:环境变量——AI世界的“启动密钥”

一切的起点,都始于那句神秘的咒语:

import 'dotenv/config';

对,就是它!这行代码仿佛打开了通往异世界的传送门,把藏在 .env 文件里的 DEEPSEEK_API_KEY 唤醒。没有它,AI就像没插电的机器人——再聪明也动不了。小李深知:在AI时代,API密钥就是你的魔法杖

小知识:.env 文件通常包含敏感信息(如密钥、数据库密码),绝不能提交到Git!记得把它加入 .gitignore

接着,他召唤出了今天的主角之一:

import { ChatDeepSeek } from '@langchain/deepseek';

ChatDeepSeek 是 LangChain 为 DeepSeek 大模型提供的官方适配器。LangChain 的伟大之处在于——它用“适配器模式”帮我们屏蔽了不同大模型(LLM)之间的差异。换句话说,你不用再为每个模型重写一套调用逻辑。今天用 DeepSeek,明天换通义千问?只要接口一致,几乎无缝切换。

这省下的时间,足够你多喝三杯咖啡,或者多摸五分钟鱼——而后者,往往是程序员真正的刚需。


第二幕:提示词——给AI戴上“人设面具”

小李知道,直接问AI“什么是闭包?”太粗暴了。AI可能会给你一段教科书式的定义,也可能突然开始讲哲学,甚至反问你:“你确定要听真话吗?”

于是,他祭出了 PromptTemplate ——LangChain 中的提示词模板神器。

const prompt = PromptTemplate.fromTemplate(`
    你是一个{role},
    请用不超过{limit}字回答以下问题:
    {question}
`);

看,这不只是提问,这是在给AI分配角色!你可以让它扮演“毒舌面试官”、“耐心导师”,甚至是“脱口秀演员”。在这个例子中,小李设定了:

  • role: '前端面试官'
  • limit: '50'
  • question: '什么是闭包'

然后格式化:

const promptStr = await prompt.format({
    role:'前端面试官',
    limit:'50',
    question:'什么是闭包'
});

结果?AI秒回一句精准又犀利的回答:

“闭包是函数记住并访问其词法作用域的能力,即使在函数外部执行。”

50字,不多不少,面试官看了直呼内行

提示词工程(Prompt Engineering)的本质,就是“如何优雅地指挥AI”。你不是在写代码,你是在写剧本。而好的提示词,就是让AI入戏的导演手记。


第三幕:单步调用 vs 链式思维——从“问答机”到“思考者”

但小李不满足于一次性的问答。他想让AI先详细解释概念,再提炼要点,最后生成学习建议。这怎么办?总不能手动复制粘贴三次吧?

这时候,LangChain 的 Chain(链) 概念闪亮登场。

简单链:Pipe 一下,世界清净了

最基础的链,用 .pipe() 连接提示词和模型:

const chain = prompt.pipe(model);
const response = await chain.invoke({ topic: '什么是闭包' });

这里,prompt 是输入模板,model 是语言模型,pipe 把它们串成流水线。输入 {topic},输出 AI 回答。简洁、优雅、可复用

这就像你点奶茶:选口味(prompt)→ 加糖(参数)→ 出杯(invoke)。全程自动化,无需人工干预。

复杂链:RunnableSequence——AI的“多工序车间”

但真实业务往往更复杂。比如小李的需求:

  1. 先让AI详细解释“闭包”;
  2. 再让另一个提示词把解释总结成三个要点;
  3. 最后拼接成完整报告。

LangChain 提供了 RunnableSequence 来实现这种多步骤流程:

const fullChain = RunnableSequence.from([
    (input) => explainChain.invoke({topic: input.topic}).then(res => res.text),
    (explanation) => summaryChain.invoke({explain: explanation}).then(res => `知识点总结: ...`)
]);

注意:每一步都是异步的,且前一步的输出自动成为后一步的输入。整个过程像一条装配线,原料进去,成品出来

运行结果令人惊喜:

  • 第一阶段输出300字内的专业解释;
  • 第二阶段自动生成如“1. 函数携带作用域 2. 延长变量生命周期 3. 可能导致内存泄漏”这样的干货总结;
  • 最终返回结构化文本,可直接用于教学或笔记。

这哪是AI?这分明是你的私人学习助理+内容编辑+知识萃取师


第四幕:RAG?一句话说清!——测试AI的“基本功”

当然,小李也没忘了测试AI的基础能力。他直接问:

const res = await model.invoke('用一句话解释什么是RAG?');
console.log(res.content);

AI答:“RAG(检索增强生成)是一种结合外部知识库与大语言模型的技术,通过检索相关信息来增强生成内容的准确性。”

一句话,准确、简洁、无废话。温度(temperature)设为0,确保AI不“放飞自我”。

温度是什么?简单说,温度越高,AI越“有创意”(也越可能胡说八道);温度越低,越“严谨”(但也可能死板)。面试场景?当然选0.7左右,既专业又不死板。写诗?那就拉到1.2,让它尽情发挥!


第五幕:为什么LangChain是AI应用的“乐高积木”?

看到这里,你可能想问:为什么不用原生API直接调用?

答案是:复杂业务需要工程化

想象一下,你要做一个“智能面试系统”:

  • 用户输入问题;
  • AI先判断问题领域(前端/后端/算法);
  • 然后切换不同角色回答;
  • 再根据用户水平调整解释深度;
  • 最后生成学习路径+相关练习题。

如果全用手写Promise链,代码会变成意大利面条,维护成本爆炸。而LangChain的 Runnable、Chain、Agent、Memory 等抽象,让你像搭乐高一样组合功能模块。

更重要的是——每一步都可测试、可替换、可复用。今天用DeepSeek,明天换Claude?只要接口一致,几乎不用改代码!

LangChain 的哲学是:不要重复造轮子,而是把轮子组装成车


第六幕:闭包的隐喻——程序员与AI的共生关系

回到最初的问题:“什么是闭包?”

对小李来说,闭包不仅是JavaScript的一个特性,更是一种隐喻:函数携带着它诞生时的环境,在陌生的地方依然能访问“家”中的变量

而LangChain,就像是那个“环境”——它包裹着大模型的能力,让我们在复杂的AI应用世界中,依然能保持代码的清晰与可控。

我们写的每一行提示词,都是在为AI“注入上下文”;每一个Chain,都是在构建“认知流水线”。我们不是在取代AI,而是在与AI协同进化


终章:从玩具到生产——LangChain的实战价值

别以为这只是玩具代码。在真实项目中,LangChain 已被广泛应用于:

  • 智能客服(自动问答+意图识别)
  • 文档摘要(上传PDF → 自动生成摘要)
  • 代码助手(自然语言生成SQL/React组件)
  • 教育工具(自动生成习题+解析)

而这一切的核心,正是我们今天演示的:提示词 + 模型 + 链式编排

所以,下次当你被问到“什么是闭包”时,不妨笑着回答:

“闭包,就是一个函数带着它的‘童年回忆’闯荡江湖。而我,用LangChain给AI装上了‘回忆提取器’,让它不仅能答题,还能当老师、做总结、甚至讲段子。”

技术很酷,但更酷的是——我们正在用代码,重新定义人与智能的关系


彩蛋:避坑指南 & 最佳实践

  1. 不要在一个文件里重复导入
    你提供的代码片段中有多个 import 'dotenv/config' 和重复的模型初始化。实际项目中应拆分为模块,避免冲突。

  2. 合理使用 .gitignore
    确保 .env 不被提交!否则你的API密钥将公之于众。

  3. 温度控制很重要

    • temperature: 0 → 确定性输出(适合事实问答)
    • temperature: 0.7 → 平衡创意与准确(适合创作、解释)
    • temperature: 1.0+ → 放飞自我(适合诗歌、故事)
  4. Chain 可嵌套
    你可以把一个Chain作为另一个Chain的节点,构建树状工作流。

  5. 监控与日志
    在生产环境中,记得记录每一步的输入输出,便于调试和优化提示词。


Happy Coding,愿你的AI永远不胡说,你的闭包永不泄漏,你的Chain永远畅通无阻!

百度一站式全业务智能结算中台

作者 百度Geek说
2025年12月23日 16:41

导读

本文深入介绍了百度一站式全业务智能结算中台,其作为公司财务体系核心,支撑多业务线精准分润与资金流转。中台采用通用化、标准化设计,支持广告、补贴、订单等多种结算模式,实现周结与月结灵活管理。通过业务流程标准化、分润模型通用化及账单测算自动化,大幅提升结算效率与准确性,确保数据合规与业务稳健发展。未来,中台将推进全业务线结算立项线上化、数据智能分析,进一步提升数据分析智能化水平,为公司业务发展提供坚实保障。

01 概述

结算中台作为公司财务体系的核心组成部分,承担着多业务线分润计算、结算及资金流转的关键职能。采用通用化、标准化的设计理念,结算中台能够高效支撑公司内数十个业务线的分润需求,确保广告收入、订单收入、内容分发的精准结算,为公司的财务健康与业务稳健发展提供坚实保障。结算中台建设的核心目标是: 构建高效、标准化、智能化的结算中台体系,支撑多业务线分润计算与资金流转,确保结算数据准确性、高时效披露及业务快速迭代能力,同时降低运维复杂度,推动全业务线结算线上化管理。

结算中台已对接了百家号业务、搜索业务、智能体业务、小说等多个业务线的结算需求, 支持广告分润、补贴分润、订单分润三种结算模式。不同业务线根据各自的业务场景使用不同的结算模式,确保每个业务的收益分配准确无误。结算中台功能分层如图:

图片

02 基本功能

1. 结算模式

结算中台支持三种结算模式,以适应不同业务场景的结算需求:

  • 订单结算:基于直接订单数据,按照订单实际金额与分成策略进行分润计算。

  • 补贴结算:针对特定业务或用户群体,提供额外的收益补贴,以增强业务的市场竞争力。

  • 广告结算:根据分发内容的广告变现与渠道分成比例,精确计算媒体与内容的实际收益。

2. 结算能力

结算中台支持周结与月结两种结算能力:

  • 周结:适用于需要快速资金回笼的业务场景,比如短剧快速回款以后能够再次用于投流, 确保资金流转的高效性。

  • 月结:作为默认的结算周期,便于公司进行统一的财务管理与账务处理。

3. 账单测算自动化

结算中台支持重点业务账单自动测算,通过预设的分润模型,自动计算每个渠道、每位作者的应得收益,生成测算报告。这一自动化过程显著提升工作效率,减少人为错误,确保结算数据的绝对准确。

03 需求分析

在推进公司结算业务时,我们致力于实现统一化、标准化,规范业务流程,并确保数据合规化治理,我们面临着诸多问题与挑战,具体表现如下:

1. 流程与规范缺失

  • 结算流程管理混乱:存在结算需求未备案即已上线的情况,或者备案内容与实际实现不一致,甚至缺乏备案流程。

  • 日志规范陈旧:广告分润场景中,内容日志打点冗余,同时缺少扩展性,导致对新的业务场景无法很好兼容。

2. 烟囱式开发成本高

  • 标准化与统一化需求迫切:之前,各个结算业务维护各自的结算系统,涉及不同的技术栈和结算模型,线下、线上结算方式并存,导致人工处理环节多,易出错,case多,管理难度大。为提高效率,需实现结算业务的标准化与统一化,并拓展支持多种业务结算模式。

  • 分润模型通用化设计:多数业务结算方式相同,同时账单计算逻辑也相似或者相同,没有必要每个业务设计一套逻辑,需要做通用化设计。

3. 业务迭代中的新诉求

  • 测算系统需求凸显:在业务快速迭代的过程中,许多业务希望尽快看到结算效果,以推进项目落地。因此,构建高效的测算系统成为迫切需求,以加速业务迭代和决策过程。

  • 提升作者体验:为提升作者等合作伙伴的满意度和忠诚度,结算数据需实现高时效披露,确保他们能及时、准确地获取收益信息。结算账单数据的产出依赖百余条数据源,要保证数据在每天12点前产出,困难重重

  • 数据校验与监控机制:结算数据的准确性和质量直接关系到公司的财务健康和业务发展。因此,需建立完善的数据校验和监控机制,确保结算数据的准确无误和高质量。

04 技术实现

根据结算中台建设的核心目标,结合业务痛点,在结算系统建设中,基于通用化、标准化的理念,从以下五个方面来搭建统一的、规范化的结算中台。

  • 业务流程标准化:建设一套标准来定义三类结算模式下每个数据处理环节的实现方式,包括业务处理流程、数据处理过程。

  • 分润模型通用化:实现不同的账单计算算法,支持各个业务的各类作者收入分配诉求,并且实现参数配置线上化。

  • 技术架构统一:统一整个结算业务的技术栈、部署环境、功能入口和数据出口。

  • 建设账单测算能力:模拟线上结算流程的账单测算能力,支持业务快速验证分润模型参数调整带来的作者收入影响效果。

  • 建设质量保证体系:建设全流程预警机制,通过日志质检、自动对账、数据异常检测来保障账单产出数据时效性、准确性。

1. 业务流程标准化

不同业务场景,采用了通用化、标准化的设计来满足业务的特异性需求,下面是三大结算模式业务流程简图:

图片

在广告模式、补贴模式、订单模式结算流程设计中, 从日志打点、线上化、计算逻辑等方向考虑了通用化、标准化设计, 具体如下:

(1) 日志打点统一化

统一日志标准, 针对业务日志规范陈旧问题,要求所有接入的业务方严格按照统一格式打点日志,删除冗余字段, 确保数据的规范性与一致性,同时保证设计能够覆盖所有业务场景,为后续处理奠定坚实基础。

针对某些业务定制化的需求, 在广告模式、补贴模式、订单模式三种结算方式中,在设计日志打点规范时, 会预留一些扩展字段, 使用时以 JSON 形式表示, 不使用时打默认值。

(2) 账单计算线上化

在补贴结算模式中,之前不同业务都有各自的账单格式设计,同时存在离线人工计算账单的非规范化场景,账单无法统一在线计算、存储、监管。新的结算中台的补贴结算模式,将所有离线结算模式,使用统一的账单格式,全部实现线上化结算,实现了业务结算流程规范化。

(3) 账单计算逻辑优化

比如在广告模式中,百家号业务的公域视频、图文、动态场景中,由于收入口径调整,迭代效率要求,不再需要进行广告拼接,所以专门对账单计算流程做了优化调整。不仅满足业务诉求,同时做了通用化设计考虑,保证后续其他业务也可以使用这套流程的同时, 也能兼容旧的业务流程。

广告模式结算流程优化前:

图片

广告模式结算流程优化后:

图片

2. 分润模型通用化

不同业务场景,不同结算对象,有不同的结算诉求,不仅要满足业务形态多样化要求,还要具有灵活性。因此抽取业务共性做通用性设计,同时通过可插拔式设计灵活满足个性化需求。

图片

(1) 基于流量变化模型

以合作站点的优质用户投流方为代表的用户,他们在为百度提供海量数据获得收益的同时,也有自己的诉求,那就是自己内容的收益不能受到其他用户内容的影响。自己优质内容不能被其他用户冲淡,当然自己的低质内容也不会去拉低别人的收益水平。

对于此部分用户我们提供“基于流量变现的分润”策略,简单来说就是,某一篇内容的收益仅仅由它自己内容页面挂载的广告消费折算而来,这样就保证了优质用户投流方收益的相对独立,也促使优质用户产出更加多的内容。

(2) 基于内容分发模型

  • 部分作者只关注收益回报: 对百家号的某些作者来说,他们的目的很单纯,他们只关注产出的内容是否获得具有竞争力的收益回报,至于收益怎么来他们并不关心。

  • “基于流量变现”策略不妥此时,我们再使用“基于流量变现”的策略的话,就有些不妥,举个极端里的例子,有一个作者比较倒霉,每次分发都没有广告的渲染,那他是不是颗粒无收?这对作者是很不友好的。

  • “基于内容分发的分润”模型: 基于收益平衡性考虑,我们推出了更加适合百家号用户的“基于内容分发的分润”模型。在这种模型下,只要内容有分发,就一定有收益,而不管本身是否有广告消费。

  • 策略平衡考虑: 当然,为了防止海量产出低质内容来刷取利润,在分润模型上,我们同时将内容质量分和运营因子作为分润计算的权重,也就是说作者最终的收益由内容的质量和内容的分发量共同决定,以达到通过调整分润来指导内容产出的目的。

(3) 基于作者标签模型

为了实现对百家号头部优质作者进行激励,促进内容生态良性发展, 会对不同的作者进行打标, 并且使用不同的分润模型, 比如对公域的百家号作者进行打标, 优质作者, 通过动态单价及内容质量权重策略来给到他们更加的分成, 其他的普通作者, 通过内容分发模型来分润。这样不仅保证了优质作者取得高收益,也保证了其他作者也有一定的收益

另外,出于对预算的精确控制,发挥每一笔预算的钱效,优质的作者会占用较大的预算资金池,而普通作者使用占用较少的预算资金池。同时也会对每类资金池进行上下限控制,保证预算不会花超。

(4) 基于运营场景模型

为了实现对百家号作者的精细化运营,比如对一些参与各类短期活动的作者给予一定的阶段性的奖励,可以通过补贴模型来实现。在一些运营活动中,需要控制部分作者的分成上限,分润模型会进行多轮分成计算,如果作者的收益未触顶并且资金池还有余额的情况下,会对余额进行二次分配,给作者再分配一些收益。此类模型主要是为了实现灵活多变的作者分润策略。

3. 技术架构统一

根据业务流程标准化、分润模型通用化的设计原则,建设统一的结算中台。以下是结算中台统一结算业务前后的对比:

图片

图片

4. 建设账单测算能力

为各个需要测算能力的业务,设计了一套通用的测算流程,如下图:

图片

针对每个测算业务,设计了独立的测算参数管理后台,用于管理业务相关的分润模型参数,如下图:

图片

测算流程设计

(1) 功能简述: 每个测算业务, 产品需要登录模型参数管理后台,此后台支持对分润模型参数进行创建、查看、编辑、测算、复制、上线、删除,以及查看测算结果等操作, 出于业务流程合规化的要求, 每次模型参数上线前, 需要对变更的参数完成线上备案流程才可以上线,实现分润流程合规线上化。

(2) 流程简述

  • 流程简述概览: 每次测算时, 产品需要先创建一个版本的账单模型测算参数,并发起参数测算,参数状态变成待测算 。

  • 离线任务与收益计算: 此后,离线任务会轮询所有的待测算参数,提交Spark任务,调用账单计算模型来计算作者收益,最后生成TDA报告。

  • 查看与评估测算报告: 产品在管理平台看到任务状态变成测算完成时, 可以点击 TDA 链接来查看测算报告, 评估是否符合预期。

  • 根据预期结果的操作:如果不符合预期,可以编辑参数再次发起测算;如果符合预期,则可以发起备案流程,流程走完后可以提交上线。

(3) 收益明显: 通过账单测算建设, 不仅解决结算需求未备案即已上线或者备案内容与实际实现不一致,甚至缺乏备案流程的业务痛点问题,  而且把业务线下账单计算的流程做到了线上, 做到留痕可追踪。同时也满足了业务高效迭代的诉求, 一次账单测算耗时从半天下降到分钟级, 大大降低了账单测算的人力成本与时间成本。

5. 建设质量保障体系

为了保证业务质量,从以下几方面来建设:

(1) 建设数据预警机制:为保证作者账单数据及时披露, 分润业务涉及的 百余条数据源都签订了 SLA, 每份数据都关联到具体的接口人, 通过如流机器人来监控每个环节的数据到位时间, 并及时发出报警信息, 并推送给具体的接口负责人。对于产出延迟频次高的数据流,会定期拉相关负责人相关复盘,不断优化数据产出时效,保证账单数据在每天如期产出

(2) 数据异常检测机制:对账单数据进行异常波动性检测, 确保数据准确性 ,及时发现并处理潜在异常问题

(3) 自动对账机制:每天自动进行上下游系统间账单明细核对,保证出账数据流转的准确无误。

(4) 日志质检机制:每日例行对日志进行全面质检分析, 及时发现日志打点日志。

05 中台收益

结算中台作为公司财务体系的核心,承担多业务线分润计算与资金流转重任。

(1) 通过通用化、标准化设计,高效支撑数十个业务线的精准结算,确保广告、订单、内容分发的业务结算稳定、健康。近一年,结算业务零事故、零损失。

(2) 中台支持多种结算模式与灵活周期管理,实现账单测算自动化,账单测算时间从天级降到小时级。提升效率并减少错误,提升业务需求迭代效率。

(3) 通过业务流程标准化、分润模型通用化、账单测算能力建设及质量保证体系,解决了结算业务规范缺失、业务形态多样等问题。累计解决历史结算case数十个,涉及结算金额达千万级。

未来,结算中台将推进全业务线结算立项线上化、周结与测算能力落地、项目全生命周期管理,并依托大模型能力实现数据智能分析,进一步提升数据分析智能化水平,为公司业务稳健发展提供坚实保障。

06 未来规划

1、推进全业务线结算实现立项线上化;

2、推进周结 、测算能力在各业务线上落地;

3、推进项目全生命周期管理,实现项目从上线到下线整体生命周期变化线上化存档,可随时回顾复盘。

4、数据智能分析,依托公司大模型能力,实现通过多轮对话问答来进行数据分析,针对业务问题进行答疑解惑,提升数据分析的智能化水平。

JavaScript中的迭代器和生成器

2025年12月23日 16:35

先讲迭代器(Iterator):就是“能一个个往外拿东西的容器”

你可以把迭代器想象成自动售货机:

  • 你先有一堆商品(比如数组 [1,2,3]),把它们放进售货机里,这个售货机就是迭代器;
  • 你每次按“出货”按钮(调用 next() 方法),它就给你出一个商品,出完一个就少一个;
  • 等商品出完了,再按按钮就会返回 { value: undefined, done: true }(提示“没货了”);
  • 而且它只能单向前进,出了2就回不到1了,不能倒着拿。

JavaScript 迭代器的简单例子

// 1. 先准备一个数组(一堆商品)
const nums = [1, 2, 3];

// 2. 把数组变成迭代器(装进售货机)
const it = nums[Symbol.iterator]();

// 3. 按按钮拿东西(调用 next())
console.log(it.next()); // 输出 { value: 1, done: false }(拿第一个)
console.log(it.next()); // 输出 { value: 2, done: false }(拿第二个)
console.log(it.next()); // 输出 { value: 3, done: false }(拿第三个)
console.log(it.next()); // 输出 { value: undefined, done: true }(没货了)

迭代器的核心特点

  1. 必须有一个 next() 方法,调用后返回固定格式:{ value: 具体值, done: 是否迭代完 }
  2. 惰性获取:只有调用 next() 才会返回下一个值,不会一次性把所有值加载到内存;
  3. 用完就没:只能往前,不能回头,也不能重复用。

手动实现一个简单迭代器(理解原理)

就像自己造一个简易售货机:

// 手动实现迭代器:模拟售货机逻辑
function createIterator(arr) {
  let index = 0; // 记录当前拿到第几个
  return {
    // 核心的 next() 方法
    next: function() {
      // 如果没拿完,返回当前值 + 未完成;否则返回 undefined + 已完成
      if (index < arr.length) {
        return { value: arr[index++], done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
}

// 使用这个自定义迭代器
const myIt = createIterator([10, 20, 30]);
console.log(myIt.next()); // { value: 10, done: false }
console.log(myIt.next()); // { value: 20, done: false }
console.log(myIt.next()); // { value: 30, done: false }
console.log(myIt.next()); // { value: undefined, done: true }

再讲生成器:“自动造东西的迭代器”

生成器是迭代器的升级版,你可以把它想象成现做现卖的小吃摊:

  • 迭代器是“先把所有东西准备好再往外拿”(比如先把1、2、3都放进售货机);
  • 生成器是“你要一个,我才做一个”(比如你要第一个包子,我才包第一个,要第二个才包第二个);
  • 它不用提前把所有数据存在内存里,而是“按需生成”,特别省内存。

JavaScript 生成器的核心:function* + yield

function* 是生成器函数的标识(注意多了个 *),yield 就是“暂停并返回”的意思——比如你去小吃摊买包子,老板包一个给你,然后暂停,等你要下一个再继续包。

生成器的完整例子(最常用)

// 定义生成器函数(注意 function* 和 yield)
function* makeBuns() {
  console.log("开始包第一个包子");
  yield 1; // 包好第一个,返回,暂停
  console.log("开始包第二个包子");
  yield 2; // 继续,包第二个,返回,暂停
  console.log("开始包第三个包子");
  yield 3; // 继续,包第三个,返回,暂停
}

// 调用函数,得到生成器(此时函数不会执行,只是创建生成器)
const bunGen = makeBuns();

// 第一次拿:执行到第一个yield,返回 { value: 1, done: false }
console.log(bunGen.next()); // 输出:开始包第一个包子 → { value: 1, done: false }
// 第二次拿:从暂停的地方继续,执行到第二个yield
console.log(bunGen.next()); // 输出:开始包第二个包子 → { value: 2, done: false }
// 第三次拿:继续,执行到第三个yield
console.log(bunGen.next()); // 输出:开始包第三个包子 → { value: 3, done: false }
// 第四次拿:没包子了,返回 done: true
console.log(bunGen.next()); // 输出:{ value: undefined, done: true }

生成器的实用场景:按需生成大量数据

比如要生成 100 万个数字,用数组会占满内存,但生成器只在需要时计算:

// 生成器:按需生成数字,不占内存
function* generateBigNumbers(max) {
  let num = 1;
  while (num <= max) {
    yield num++; // 要一个,生成一个
  }
}

// 生成 100 万个数字的生成器(此时还没生成任何数字)
const bigNumGen = generateBigNumbers(1000000);

// 只拿前3个,后面的不会生成
console.log(bigNumGen.next().value); // 1
console.log(bigNumGen.next().value); // 2
console.log(bigNumGen.next().value); // 3

迭代器 vs 生成器 一句话区分(JS 版本)

  • 迭代器:已有数据的“搬运工”(把现成的东西一个个拿出来,比如数组迭代器);
  • 生成器:按需生成数据的“生产者”(没有现成数据,要的时候才造,用 function* + yield);
  • 生成器本质上是一种“自动实现了迭代器接口”的迭代器,写起来比手动迭代器简单 10 倍。

总结

  1. 迭代器:像自动售货机,提前装好物,按一次出一个,出完为止,核心是“遍历已有数据”;
  2. 生成器:像现做现卖的小吃摊,不用提前备货,要一个做一个,核心是“按需生成数据”,更省内存;
  3. JS 里迭代器靠 next() 方法和 { value, done } 格式,生成器靠 function* + yield,两者都是“惰性计算”,适合处理大数据。

2.2 Node的模块实现

作者 晚星star
2025年12月23日 16:31

好,我们深入第二章的第二小节:2.2 Node的模块实现

这一小节是第二章的核心部分,朴灵作者详细剖析了Node.js如何从源码层面实现CommonJS的require机制。整个过程分为三个关键步骤:优先从缓存加载路径分析和文件定位模块编译。理解这里,你就知道为什么require这么高效、为什么有缓存、为什么循环依赖不会死锁等“黑魔法”。

下面是这一小节的子结构(原书划分):

  • 2.2.1 优先从缓存加载
  • 2.2.2 路径分析和文件定位
  • 2.2.3 模块编译

为了让你更直观,我附上了一些来自读者笔记和网上分享的原书页面截图、模块加载流程图、包裹函数示意图等(这些是常见可视化辅助,很多开发者读书时都会截图或画图)。

详细讲解

2.2.1 优先从缓存加载

这是require最聪明的设计:模块只执行一次,后续直接返回缓存的exports对象。

  • 原理:Node用Module._cache(一个对象,以模块绝对路径为key)缓存已加载模块。
  • 第一次require('foo'):加载、执行、缓存module.exports。
  • 第二次require('foo'):直接从缓存取,返回同一个对象引用。

示例代码(书中有类似演示):

// a.js
console.log('a.js 执行了');
exports.done = false;

// b.js
var a = require('./a');
console.log('b.js 中,a.done =', a.done);
a.done = true;
console.log('b.js 中,修改后 a.done =', a.done);

// main.js
var a = require('./a');
var b = require('./b');
console.log('main.js 中,a.done =', a.done);

输出:

a.js 执行了  // 只打印一次!
b.js 中,a.done = false
b.js 中,修改后 a.done = true
main.js 中,a.done = true

关键点:

  • 模块代码只执行一次,避免副作用重复。
  • 导出的是对象引用,修改会共享(这是循环依赖能工作的基础:先导出空对象{})。

2.2.2 路径分析和文件定位

require的参数(模块标识符)可能是核心模块、相对路径、绝对路径或包名。Node按优先级分析:

  1. 核心模块(如'fs'、'http'):直接从内置加载,最快。
  2. 文件模块
    • 绝对路径:直接定位。
    • 相对路径(./ 或 ../):从调用者目录解析。
    • 无扩展名:依次尝试 .js → .json → .node(C++扩展)。
  3. 包模块(如require('express')):
    • 在当前目录的node_modules找。
    • 找不到?向上级目录逐级查找,直到根目录。
    • 找到目录后,读package.json的"main"字段(默认index.js)。

这个过程在源码的Module._resolveFilename函数实现。书里强调:这是NPM能工作的基础!

2.2.3 模块编译

找到文件后,Node不直接执行,而是包裹成一个函数运行,创造独立作用域。

  • 对于.js文件:读取内容,前面加(function (exports, require, module, __filename, __dirname) {,后面加});,然后用vm.runInThisContext执行。
  • 这五个参数就是模块的“私有空间”:
    • exports:导出对象(module.exports的引用)。
    • require:当前模块的require函数。
    • module:模块对象本身。
    • __filename / __dirname:当前文件路径和目录。

示例(书中原码简化版): 你的math.js内容是:

exports.add = (a, b) => a + b;

实际执行的是:

(function (exports, require, module, __filename, __dirname) {
  exports.add = (a, b) => a + b;
  // ... 你的代码
});
  • .json文件:JSON.parse后赋值给module.exports。
  • .node文件:用dlopen加载C++扩展。

编译后,module.exports缓存起来,返回给调用者。

图片懒加载

作者 Crystal328
2025年12月23日 16:28

图片懒加载是一种优化网页性能的技术,主要作用是延迟加载视口外的图片,直到用户滚动到它们附近时才加载

image.png 以下是常见的几种实现方式

1. 传统滚动事件监听方式

  • 监听到scroll事件后,调用目标元素的getBoundingClientRect()方法来实现。
function lazyLoad() {
    const images = document.querySelectorAll('img[data-src]');

    images.forEach(img => {
        if (img.getBoundingClientRect().top < window.innerHeight + 100) {
            img.src = img.dataset.src;
        }
    });
}

window.addEventListener('scroll', lazyLoad);
window.addEventListener('resize', lazyLoad);
lazyLoad(); // 初始加载

缺点:性能较差,需要手动处理节流

2、使用Intersection Observer API(现代推荐方式)

通过IntersectionObserverAPI 来实现,他可以自动"观察"元素是否可见。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做"交叉口观测器"。

const images = document.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries) => {
    // entries 是一个数组,包含了我们所需要监听的所有图片对象的信息
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target; 
      img.src = img.dataset.src;
      observer.unobserve(img); // 停止监听
    }
  });
});
// 添加需要监听的图片
images.forEach(img => observer.observe(img));

优点:性能好,不阻塞主线程,代码简洁

IntersectionObserver是浏览器原生提供的构造函数,可以自动‘观察’元素是否可见。

//观察器实例
const io = new IntersectionObserver(callback,options)

参数

  • callback: 是可见性变化时的回调函数。callback一般会触发两次
    • 一次是目标元素刚刚进入视口(开始可见)
    • 另一次是完全离开视口(开始不可见)
  • option: 是配置对象(该参数可选) 优点:性能好,不阻塞主线程,代码简洁

返回值:一个观察期实例

观察器实例方法

  • observe方法可以指定观察哪个DOM节点,就需要多次调用observe方法
  • unobserve方法用于停止对某个元素的观察
  • disconnect方法用于关闭观察器
//开始观察box元素和container
io.server(document.getElementById('box'))
io.server(document.getElementById('container'))

//停止观察box元素
io.unobserve(document.getElementById('box'))

//关闭观察器
io.disconnect();

3. 使用loading="lazy"属性(HTML原生支持)

<img src="image.jpg" loading="lazy" alt="...">

优点:最简单的方式,现代浏览器都支持

缺点:兼容性问题(IE不支持),控制粒度较粗

4、第三方库实现

常用库:

  • lazysizes
  • lozad.js
  • vanilla-lazyload

以下是插件vanilla-lazyload的具体使用

进入 https://www.npmjs.com/ 官网,然后在搜索框中输入 vanilla-lazyload 回车 ,即可找到该插件

5. 框架内置懒加载

  • React: 使用 react-lazyload 等库
  • Vue: 有 vue-lazyload 插件
  • Angular: 有内置的懒加载支持

实现要点

  1. 使用data-src自定义属性代替src属性存储真实图片URL
  2. 设置占位图(可以是小尺寸预览图或纯色背景)
  3. 考虑添加加载动画或过渡效果
  4. 对于重要图片(如首屏)不要使用懒加载

选择哪种方式取决于项目需求、目标浏览器支持和开发环境。现代项目推荐优先使用Intersection Observer或原生loading属性。

我用 NestJS + Vue3 + Prisma + PostgreSQL 打造了一个企业级 sass 多租户平台

2025年12月23日 16:23

🎯 我用 NestJS + Vue3 + Prisma + PostgreSQL 打造了一个企业级 sass 多租户平台

一个生产级的全栈管理系统开源项目,从零到一的实战分享

前言

这是一个基于 NestJS + Vue3 + Prisma + PostgreSQL ** 构建的全栈管理系统,不是简单的 CRUD,而是一个生产级别**的解决方案。它包含了多租户架构、RBAC权限管理、请求加密、完善的日志监控等企业级特性。


🔗 项目链接

🌟 GitHub 开源地址github.com/linlingqin7…
🎮 在线体验地址www.linlingqin.top/
📖 项目文档地址项目文档

体验账号:demo / demo123 (租户 000000)
完整权限账号:admin / admin123 (租户 000000)


📱 先看效果

登录页面

login.png

支持账号密码登录、验证码验证、记住密码

首页仪表板

dashboard.png

系统概览、快捷入口、数据统计、图表展示

用户管理

user.png

用户列表、角色分配、部门选择、状态管理

角色管理

role.png

角色权限配置、菜单权限树、数据权限范围

  • ✨ 现代化的界面设计,支持深色模式
  • 📱 响应式布局,完美适配各种屏幕
  • 🎨 丰富的组件和交互效果

� 完整功能截图

菜单管理

menu.png

菜单树形结构、路由配置、图标选择、权限标识

部门管理

dept.png

组织架构树、部门人员、数据权限

岗位管理

post.png

岗位配置、岗位排序、状态管理

字典管理

dict.png

数据字典维护、字典项配置

参数配置

paramSet.png

系统参数、动态配置、参数分类

通知公告

notice.png

公告发布、通知管理、类型分类

租户管理

tenant.png

多租户列表、套餐配置、租户状态

定时任务

job.png

Cron 任务配置、执行日志、任务管理

系统监控

monitor.png

服务器状态、CPU/内存使用率、磁盘信息

缓存监控

monitorCache.png

Redis 缓存信息、命令统计、键值管理

在线用户

online.png

实时在线用户、会话管理、强制下线

操作日志

operlog.png

操作记录、请求参数、响应结果、异常捕获

登录日志

loginLog.png

登录历史、IP 地址、浏览器信息、登录状态

缓存列表

cacheList.png

缓存键管理、过期时间、缓存清理

主题配置

theme.png

多主题切换、深色模式、主题色配置、布局模式

✨ 核心特性

🏢 多租户 SaaS 架构

这是本项目的一大亮点。实现了完整的租户数据隔离

// 所有数据库查询自动按租户过滤
const users = await prisma.sysUser.findMany({
  where: { name: 'John' }
  // tenantId 会自动注入,无需手动添加
});

// 跳过租户过滤(特殊场景)
@IgnoreTenant()
async getAllTenants() {
  return await this.prisma.tenant.findMany();
}

技术实现

  • 基于 Prisma Extension 实现透明的租户过滤
  • 通过 TenantGuard 从请求头自动识别租户
  • 超级管理员(租户 000000)可跨租户管理

适用场景

  • SaaS 平台
  • 企业内部多部门系统
  • 白标产品

🔐 RBAC 权限管理

不只是简单的角色权限,而是多层级、细粒度的权限控制:

@Controller('users')
export class UserController {
  // 需要特定权限才能访问
  @RequirePermission('system:user:add')
  @Post()
  create(@Body() dto: CreateUserDto) {
    return this.userService.create(dto);
  }
  
  // 需要特定角色
  @RequireRole('admin')
  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.userService.remove(id);
  }
}

权限层级

  1. 菜单级别:控制菜单显示/隐藏
  2. 按钮级别:控制页面内按钮权限
  3. 数据级别:全部/本部门/仅本人等数据范围

前端权限控制

<template>
  <!-- 按钮级权限 -->
  <n-button v-if="hasPermission('system:user:add')">
    添加用户
  </n-button>
</template>

🔒 请求加密机制

敏感数据传输采用 AES + RSA 混合加密

加密流程

  1. 前端生成随机 AES 密钥
  2. 用 AES-CBC 加密请求数据
  3. 用服务端 RSA 公钥加密 AES 密钥
  4. 发送 {encryptedKey, encryptedData} + header x-encrypted: true
// 后端自动解密
@Post('login')
async login(@Body() dto: LoginDto) {
  // dto 已自动解密,直接使用
  return this.authService.login(dto);
}

// 跳过解密(非敏感接口)
@SkipDecrypt()
@Get('captcha')
async getCaptcha() {
  return this.captchaService.generate();
}

优势

  • 保护密码、Token 等敏感信息
  • 防止中间人攻击
  • 对业务代码透明,由拦截器统一处理

📊 完善的日志监控

基于 Pino 实现的高性能结构化日志:

// 自动记录操作日志
@Operlog({
  businessType: BusinessTypeEnum.UPDATE,
  title: '修改用户'
})
@Put(':id')
async update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
  return this.userService.update(id, dto);
}

监控能力

  • 结构化日志:自动记录 requestId、tenantId、username
  • 敏感字段脱敏:password、token 等自动隐藏
  • 操作审计:记录谁在什么时间做了什么
  • 登录日志:登录历史、IP、浏览器等信息
  • 健康检查:K8s liveness/readiness 探针
  • Prometheus 指标:暴露 /api/metrics 端点

日志输出示例

{
  "level": "info",
  "time": "2025-12-22T10:30:00.000Z",
  "requestId": "a1b2c3d4",
  "tenantId": "000001",
  "username": "admin",
  "method": "POST",
  "url": "/api/system/user",
  "statusCode": 200,
  "duration": 45
}

🎭 演示账户系统

专为产品演示设计的只读账户机制:

// Demo 账户拦截器
@UseInterceptors(DemoInterceptor)
@Controller('users')
export class UserController {
  @Post()  // Demo 账户会被自动拦截
  create(@Body() dto: CreateUserDto) {
    return this.userService.create(dto);
  }
  
  @Get()  // 查询操作不受影响
  findAll() {
    return this.userService.findAll();
  }
}

特性

  • 52 个只读权限,可查看所有模块
  • 自动拦截所有写操作(POST/PUT/DELETE/PATCH)
  • 返回友好的提示信息
  • 适合演示站点、产品 Demo

🌐 国际化支持

前后端完整的 i18n 方案:

// 后端
throw new ApiException(ErrorEnum.USER_NOT_FOUND);
// 自动返回对应语言的错误信息

// 前端
const { t } = useI18n();
console.log(t('system.user.name')); // 根据语言返回对应文本

支持中文、英文,可轻松扩展其他语言。


🛠️ 技术栈详解

后端技术栈

技术 版本 核心应用场景 技术亮点
NestJS 10.x 企业级框架,构建可扩展的服务端应用 • 依赖注入
• 模块化设计
• 装饰器语法
• 内置 TypeScript
Prisma 5.x 类型安全的数据库 ORM • 自动生成类型
• 数据库迁移
• 强大的查询构建器
• 多数据库支持
PostgreSQL 14+ 主数据库,存储核心业务数据 • ACID 事务
• JSON 支持
• 丰富的数据类型
• 强大的查询优化
Redis 7+ 缓存、Session、分布式锁 • 高性能缓存
• 数据过期策略
• 发布订阅
• 分布式锁
JWT - 无状态身份认证 • Token 认证
• Refresh Token
• 跨域认证
• 移动端友好
Passport - 认证中间件 • 策略模式
• 多种认证方式
• 易于扩展
Pino - 高性能结构化日志 • JSON 日志
• 低开销
• 日志轮转
• 多传输通道
Swagger - API 文档自动生成 • 交互式文档
• 自动类型推导
• 在线测试
Terminus - 健康检查与监控 • K8s 探针
• 数据库检查
• 内存监控
• 自定义检查
class-validator - 数据验证 • 装饰器验证
• 自定义规则
• 嵌套验证
class-transformer - 数据转换 • DTO 转换
• 类型映射
• 数据脱敏
@nestjs/schedule - 定时任务调度 • Cron 表达式
• 间隔任务
• 超时控制
nestjs-cls - 请求上下文管理 • 请求追踪
• 用户上下文
• 租户上下文

前端技术栈

技术 版本 核心应用场景 技术亮点
Vue 3 3.5+ 渐进式前端框架 • Composition API
• 响应式系统
• 虚拟 DOM
• 组件化开发
Vite 7+ 新一代构建工具 • 极速冷启动
• HMR 热更新
• 按需编译
• Rollup 打包
Naive UI 最新 企业级组件库 • Vue 3 组合式 API
• TypeScript 支持
• 主题定制
• 200+ 组件
UnoCSS 最新 即时原子化 CSS 引擎 • 零运行时
• 高性能
• 预设系统
• 按需生成
Pinia 最新 下一代状态管理 • 轻量级
• TypeScript 支持
• 模块化
• DevTools 支持
Vue Router 4+ 官方路由管理 • 动态路由
• 路由守卫
• 懒加载
• 嵌套路由
Axios 最新 HTTP 请求库 • Promise API
• 拦截器
• 请求取消
• 自动转换
TypeScript 5.x 类型安全的 JavaScript • 静态类型检查
• IDE 智能提示
• 重构支持
• 接口定义
VueUse 最新 Vue 组合式函数集合 • 常用 Hooks
• 响应式工具
• 浏览器 API 封装
Elegant Router 最新 基于文件的路由系统 • 自动生成路由
• 约定式路由
• 类型安全
ECharts 5+ 数据可视化图表 • 丰富的图表类型
• 响应式设计
• 主题定制
CryptoJS - 加密算法库 • AES 加密
• RSA 加密
• MD5/SHA 哈希

🏗️ 系统架构详解

📐 整体架构图

                        ┌─────────────────────────────────┐
                        │      用户/客户端层              │
                        │  ┌──────────┐  ┌─────────────┐ │
                        │  │  浏览器   │  │  移动端App  │ │
                        │  └──────────┘  └─────────────┘ │
                        └──────────────┬──────────────────┘
                                       │ HTTPS
                        ┌──────────────▼──────────────────┐
                        │      CDN / Nginx 网关            │
                        │  • 静态资源缓存                  │
                        │  • 反向代理                      │
                        │  • 负载均衡                      │
                        │  • SSL 证书                      │
                        └──────────────┬──────────────────┘
                                       │
                ┌──────────────────────┴──────────────────────┐
                │                                              │
┌───────────────▼────────────────┐       ┌───────────────────▼──────────┐
│        前端应用 (Vue3)          │       │     后端应用 (NestJS)         │
│                                 │       │                               │
│  ┌──────────────────────────┐  │       │  ┌────────────────────────┐  │
│  │     UI 层 (Naive UI)     │  │       │  │   控制器层 (Controllers)│  │
│  │  • 组件库                │  │       │  │   • 路由定义           │  │
│  │  • 主题定制              │  │       │  │   • 请求验证           │  │
│  │  • 响应式布局            │  │       │  │   • 参数转换           │  │
│  └──────────────────────────┘  │       │  └────────────────────────┘  │
│                                 │       │              │                │
│  ┌──────────────────────────┐  │       │  ┌───────────▼─────────────┐ │
│  │   状态层 (Pinia Store)   │  │       │  │   守卫层 (Guards)       │ │
│  │  • 全局状态              │  │       │  │   1. TenantGuard       │ │
│  │  • 用户信息              │  │       │  │   2. AuthGuard         │ │
│  │  • 权限数据              │  │       │  │   3. RolesGuard        │ │
│  └──────────────────────────┘  │       │  │   4. PermissionGuard   │ │
│                                 │       │  └───────────┬─────────────┘ │
│  ┌──────────────────────────┐  │       │              │                │
│  │   路由层 (Vue Router)    │  │       │  ┌───────────▼─────────────┐ │
│  │  • 动态路由              │  │       │  │  拦截器层 (Interceptors)│ │
│  │  • 路由守卫              │  │       │  │  1. DecryptInterceptor │ │
│  │  • 懒加载                │  │       │  │  2. DemoInterceptor    │ │
│  └──────────────────────────┘  │       │  │  3. TransformInter...  │ │
│                                 │       │  │  4. LoggingInterceptor │ │
│  ┌──────────────────────────┐  │       │  └───────────┬─────────────┘ │
│  │   请求层 (Axios)         │  │       │              │                │
│  │  • 请求拦截              │◄─┼───────┼──────────────┤                │
│  │  • 响应拦截              │  │       │  ┌───────────▼─────────────┐ │
│  │  • 错误处理              │  │       │  │   业务层 (Services)     │ │
│  │  • 请求加密              │  │       │  │  • 系统管理 Service     │ │
│  └──────────────────────────┘  │       │  │  • 权限管理 Service     │ │
│                                 │       │  │  • 监控管理 Service     │ │
└─────────────────────────────────┘       │  │  • 租户管理 Service     │ │
                                          │  └───────────┬─────────────┘ │
                                          │              │                │
                                          │  ┌───────────▼─────────────┐ │
                                          │  │   数据访问层 (Prisma)   │ │
                                          │  │  • Schema 定义          │ │
                                          │  │  • 类型生成             │ │
                                          │  │  • 查询构建器           │ │
                                          │  │  • 租户扩展             │ │
                                          │  └───────────┬─────────────┘ │
                                          └──────────────┼────────────────┘
                                                         │
                        ┌────────────────────────────────┼────────────────┐
                        │                                │                │
            ┌───────────▼──────────┐         ┌──────────▼──────────┐     │
            │  PostgreSQL 数据库    │         │   Redis 缓存        │     │
            │  • 主数据存储         │         │   • 会话存储        │     │
            │  • 事务支持           │         │   • 数据缓存        │     │
            │  • 索引优化           │         │   • 分布式锁        │     │
            └───────────────────────┘         └─────────────────────┘     │
                                                                           │
            ┌──────────────────────────────────────────────────────────┐  │
            │                   外部服务集成                            │  │
            │  ┌─────────────┐  ┌──────────────┐  ┌────────────────┐  │  │
            │  │  OSS 对象存储│  │  邮件服务    │  │  短信服务      │  │  │
            │  │  • 阿里云    │  │  • SMTP      │  │  • 阿里云      │  │  │
            │  │  • 七牛云    │  │  • SendGrid  │  │  • 腾讯云      │  │  │
            │  │  • MinIO     │  └──────────────┘  └────────────────┘  │  │
            │  └─────────────┘                                          │  │
            │  ┌──────────────────────────────────────────────────────┐│  │
            │  │           监控与日志                                  ││  │
            │  │  • Prometheus (指标采集)                              ││  │
            │  │  • Grafana (可视化)                                   ││  │
            │  │  • Pino Logger (日志)                                 ││  │
            │  │  • Terminus (健康检查)                                ││  │
            │  └──────────────────────────────────────────────────────┘│  │
            └──────────────────────────────────────────────────────────┘  │
                                                                           │
                        ┌──────────────────────────────────────────────────┘
                        │        部署环境 (可选)
                        │  ┌──────────────────────────────┐
                        │  │      Docker 容器化            │
                        │  │  • 应用容器                   │
                        │  │  • 数据库容器                 │
                        │  │  • Redis 容器                 │
                        │  └──────────────────────────────┘
                        │  ┌──────────────────────────────┐
                        │  │   Kubernetes 编排             │
                        │  │  • Pod 管理                   │
                        │  │  • Service 暴露               │
                        │  │  • Ingress 路由               │
                        │  └──────────────────────────────┘
                        └───────────────────────────────────

🔄 完整请求处理流程

┌─────────────┐
│  1. 客户端   │  发起 HTTP 请求
└──────┬──────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  2. 前端请求拦截器                       │
│  • 添加 Authorization Token              │
│  • 添加租户 ID (x-tenant-id)            │
│  • 敏感数据 AES+RSA 加密                 │
│  • 添加请求 ID (x-request-id)           │
└──────┬──────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  3. Nginx 网关                           │
│  • SSL 终止                              │
│  • 静态资源服务                          │
│  • 反向代理到后端                        │
│  • 请求日志记录                          │
└──────┬──────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  4. NestJS 中间件层                      │
│  • CORS 处理                             │
│  • Body 解析                             │
│  • Helmet 安全头                         │
│  • Request ID 生成                       │
└──────┬──────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  5. 守卫层 (Guards) - 按顺序执行        │
│  ┌───────────────────────────────────┐  │
│  │ 5.1 TenantGuard                   │  │
│  │  • 提取请求头中的租户 ID           │  │
│  │  • 验证租户是否存在且有效          │  │
│  │  • 设置租户上下文 (CLS)            │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │ 5.2 JwtAuthGuard                  │  │
│  │  • 验证 JWT Token 有效性           │  │
│  │  • 解析用户信息                    │  │
│  │  • 设置用户上下文                  │  │
│  │  • 检查 Token 是否过期             │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │ 5.3 RolesGuard (可选)             │  │
│  │  • 检查用户角色                    │  │
│  │  • 验证是否满足角色要求            │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │ 5.4 PermissionGuard               │  │
│  │  • 检查用户权限                    │  │
│  │  • 验证是否有接口访问权限          │  │
│  │  • 支持权限组合 (AND/OR)           │  │
│  └───────────────────────────────────┘  │
└──────┬──────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  6. 拦截器层 (Interceptors) - 前置      │
│  ┌───────────────────────────────────┐  │
│  │ 6.1 DecryptInterceptor            │  │
│  │  • 检测加密请求头                  │  │
│  │  • RSA 解密 AES 密钥               │  │
│  │  • AES 解密请求体                  │  │
│  │  • 替换原始请求数据                │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │ 6.2 DemoInterceptor               │  │
│  │  • 检测是否为演示账户              │  │
│  │  • 拦截写操作 (POST/PUT/DELETE)    │  │
│  │  • 返回友好提示信息                │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │ 6.3 LoggingInterceptor (开始)     │  │
│  │  • 记录请求开始时间                │  │
│  │  • 记录请求基本信息                │  │
│  └───────────────────────────────────┘  │
└──────┬──────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  7. Pipe 管道验证                        │
│  • ValidationPipe (DTO 验证)            │
│  • ParseIntPipe (参数转换)              │
│  • 自定义验证管道                        │
└──────┬──────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  8. Controller 控制器                    │
│  • 接收请求参数                          │
│  • 调用 Service 方法                     │
│  • 应用装饰器 (@Operlog 等)             │
└──────┬──────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  9. Service 业务层                       │
│  • 业务逻辑处理                          │
│  • 数据验证                              │
│  • 调用 Prisma 查询                      │
│  • 缓存操作 (Redis)                      │
└──────┬──────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  10. Prisma ORM 层                       │
│  ┌───────────────────────────────────┐  │
│  │ 10.1 Tenant Extension             │  │
│  │  • 自动注入租户过滤条件            │  │
│  │  • 所有查询自动添加 tenantId       │  │
│  │  • 支持 @IgnoreTenant 跳过         │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │ 10.2 查询执行                     │  │
│  │  • 参数化查询 (防 SQL 注入)        │  │
│  │  • 事务支持                        │  │
│  │  • 查询优化                        │  │
│  └───────────────────────────────────┘  │
└──────┬──────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  11. 数据库层                            │
│  • PostgreSQL 查询执行                   │
│  • 索引查找                              │
│  • 事务提交/回滚                         │
└──────┬──────────────────────────────────┘
       │
       ▼ (返回数据)
┌─────────────────────────────────────────┐
│  12. 拦截器层 (Interceptors) - 后置      │
│  ┌───────────────────────────────────┐  │
│  │ 12.1 TransformInterceptor         │  │
│  │  • 统一响应格式                    │  │
│  │  • {code, msg, data, timestamp}    │  │
│  │  • 脱敏处理                        │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │ 12.2 LoggingInterceptor (结束)    │  │
│  │  • 计算请求耗时                    │  │
│  │  • 记录响应状态码                  │  │
│  │  • 记录到操作日志表                │  │
│  │  • 输出结构化日志                  │  │
│  └───────────────────────────────────┘  │
└──────┬──────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  13. 异常过滤器 (Exception Filters)      │
│  • 捕获异常                              │
│  • 格式化错误响应                        │
│  • 记录错误日志                          │
│  • 返回友好错误信息                      │
└──────┬──────────────────────────────────┘
       │
       ▼
┌─────────────────────────────────────────┐
│  14. 响应返回客户端                      │
│  • HTTP Response                         │
│  • 状态码 + 响应体                       │
└─────────────────────────────────────────┘

🏢 多租户架构图

┌──────────────────────────────────────────────────────────┐
                     租户 A 用户                           
                   (tenantId: 000001)                      
└────────────────────┬─────────────────────────────────────┘
                     
┌──────────────────────────────────────────────────────────┐
                     租户 B 用户                           
                   (tenantId: 000002)                      
└────────────────────┬─────────────────────────────────────┘
                     
                     
┌──────────────────────────────────────────────────────────┐
              NestJS 应用 - TenantGuard                    
  ┌────────────────────────────────────────────────────┐  
    1. 提取请求头中的租户 ID (x-tenant-id)              
    2. 验证租户是否存在且状态为启用                     
    3. 将租户 ID 存入请求上下文 (CLS)                   
    4. 后续所有操作自动使用该租户上下文                 
  └────────────────────────────────────────────────────┘  
└────────────────────┬─────────────────────────────────────┘
                     
                     
┌──────────────────────────────────────────────────────────┐
           Prisma Client - Tenant Extension                
  ┌────────────────────────────────────────────────────┐  
    所有数据库查询自动注入租户过滤:                     
                                                        
    原始查询:                                           
      prisma.sysUser.findMany({ where: { ... } })      
                                                        
    自动转换为:                                         
      prisma.sysUser.findMany({                        
        where: {                                       
          tenantId: '000001',  // 自动注入             
          ...                                          
        }                                              
      })                                               
                                                        
    支持的操作:                                         
     findMany / findUnique / findFirst                
     create / createMany                              
     update / updateMany                              
     delete / deleteMany                              
     count / aggregate                                
  └────────────────────────────────────────────────────┘  
└────────────────────┬─────────────────────────────────────┘
                     
                     
┌──────────────────────────────────────────────────────────┐
                PostgreSQL 数据库                          
                                                           
  ┌──────────────────────┐      ┌──────────────────────┐ 
      租户 A 数据                  租户 B 数据         
    ┌────────────────┐          ┌────────────────┐   
     tenantId: 0001            tenantId: 0002    
     user_id: 1                user_id: 100      
     name: 张三                name: 李四        
    └────────────────┘          └────────────────┘   
    ┌────────────────┐          ┌────────────────┐   
     tenantId: 0001            tenantId: 0002    
     user_id: 2                user_id: 101      
     name: 王五                name: 赵六        
    └────────────────┘          └────────────────┘   
  └──────────────────────┘      └──────────────────────┘ 
                                                           
   数据完全隔离,互不干扰                                 
   共享数据库,降低成本                                   
   tenantId 字段建立索引,查询高效                        
└───────────────────────────────────────────────────────────┘

特殊场景: 超级管理员查询
┌──────────────────────────────────────────────────────────┐
  @IgnoreTenant()  // 跳过租户过滤                         
  async getAllTenants() {                                  
    return await this.prisma.tenant.findMany();           
  }                                                        
  // 可以查询所有租户的数据                                
└──────────────────────────────────────────────────────────┘

🔐 权限控制架构图

┌────────────────────────────────────────────────────────────┐
│                      用户登录成功                           │
└────────────────────┬───────────────────────────────────────┘
                     │
                     ▼
┌────────────────────────────────────────────────────────────┐
│           JWT Token 生成 (包含用户基本信息)                 │
│  {                                                          │
│    userId: 1,                                               │
│    username: 'admin',                                       │
│    tenantId: '000000',                                      │
│    roles: ['admin'],                                        │
│    exp: 1640000000  // 过期时间                             │
│  }                                                          │
└────────────────────┬───────────────────────────────────────┘
                     │
                     ▼
┌────────────────────────────────────────────────────────────┐
│                  前端存储 Token                             │
│  • localStorage.setItem('token', token)                     │
│  • 每次请求自动携带: Authorization: Bearer ${token}        │
└────────────────────┬───────────────────────────────────────┘
                     │
                     ▼
┌────────────────────────────────────────────────────────────┐
│            后端接收请求 - 权限检查流程                      │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ Step 1: JwtAuthGuard                                 │  │
│  │  ├─ 验证 Token 签名                                  │  │
│  │  ├─ 检查 Token 是否过期                              │  │
│  │  ├─ 解析用户信息                                     │  │
│  │  └─ 从 Redis 加载完整用户权限                        │  │
│  │                                                      │  │
│  │     ┌──────────────────────────────────┐            │  │
│  │     │ Redis 缓存的用户权限数据         │            │  │
│  │     │ user:permissions:1               │            │  │
│  │     │ {                                │            │  │
│  │     │   roles: ['admin', 'user'],     │            │  │
│  │     │   permissions: [                │            │  │
│  │     │     'system:user:add',          │            │  │
│  │     │     'system:user:edit',         │            │  │
│  │     │     'system:user:delete',       │            │  │
│  │     │     'system:user:query',        │            │  │
│  │     │     'system:role:*',  // 通配符 │            │  │
│  │     │     ...                         │            │  │
│  │     │   ],                            │            │  │
│  │     │   menuIds: [1,2,3,4,5,...],    │            │  │
│  │     │   dataScope: 'DEPT_AND_CHILD'  │            │  │
│  │     │ }                               │            │  │
│  │     └──────────────────────────────────┘            │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ Step 2: RolesGuard (可选)                            │  │
│  │  @RequireRole('admin')                               │  │
│  │  ├─ 检查用户是否拥有指定角色                         │  │
│  │  ├─ 支持多角色: @RequireRole('admin', 'manager')    │  │
│  │  └─ 支持角色组合: AND / OR                           │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ Step 3: PermissionGuard                              │  │
│  │  @RequirePermission('system:user:edit')              │  │
│  │  ├─ 提取接口所需权限                                 │  │
│  │  ├─ 检查用户权限列表                                 │  │
│  │  ├─ 支持通配符匹配 (system:user:*)                   │  │
│  │  └─ 支持权限组合: AND / OR                           │  │
│  └──────────────────────────────────────────────────────┘  │
└────────────────────┬───────────────────────────────────────┘
                     │
         ┌───────────┴───────────┐
         │ 权限验证通过           │ 权限验证失败
         ▼                       ▼
┌──────────────────┐    ┌──────────────────────┐
│  执行业务逻辑     │    │  返回 403 Forbidden   │
│  • Controller    │    │  { code: 403,         │
│  • Service       │    │    msg: '无权限访问' }│
│  • Prisma        │    └──────────────────────┘
└──────────────────┘

数据权限控制 (Data Scope):
┌────────────────────────────────────────────────────────────┐
│  在 Service 层根据用户的 dataScope 过滤数据:                │
│                                                             │
│  async findUsers(query, user) {                             │
│    const where = { ...query };                              │
│                                                             │
│    if (user.dataScope === 'ALL') {                          │
│      // 查询所有数据 (超级管理员)                           │
│    } else if (user.dataScope === 'DEPT_AND_CHILD') {       │
│      // 查询本部门及子部门数据                              │where.deptId = { in: user.deptIds };                   │
│    } else if (user.dataScope === 'DEPT') {                  │
│      // 只查询本部门数据                                    │where.deptId = user.deptId;                            │
│    } else if (user.dataScope === 'SELF') {                  │
│      // 只查询自己的数据                                    │where.userId = user.userId;                            │
│    }                                                         │
│                                                             │
│    return await this.prisma.user.findMany({ where });       │
│  }                                                           │
└────────────────────────────────────────────────────────────┘

🔒 请求加密架构图

┌────────────────────────────────────────────────────────────┐
│                    前端 - 加密流程                          │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ 1. 准备敏感数据                                      │  │
│  │    const data = {                                    │  │
│  │      username: 'admin',                              │  │
│  │      password: 'admin123'  // 敏感信息               │  │
│  │    };                                                │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ 2. 生成随机 AES 密钥 (每次请求都不同)                │  │
│  │    const aesKey = CryptoJS.lib.WordArray.random(16); │  │
│  │    // 例: "a1b2c3d4e5f6g7h8"                         │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ 3.AES 密钥加密数据                                │  │
│  │    const encryptedData = CryptoJS.AES.encrypt(       │  │
│  │      JSON.stringify(data),                           │  │
│  │      aesKey,                                         │  │
│  │      { mode: CryptoJS.mode.CBC, iv: randomIV }       │  │
│  │    );                                                │  │
│  │    // 结果: "U2FsdGVkX1+..."  (Base64)               │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ 4. 用服务端 RSA 公钥加密 AES 密钥                     │  │
│  │    const encryptedKey = RSA.encrypt(                 │  │
│  │      aesKey.toString(),                              │  │
│  │      serverPublicKey  // 服务端提供的公钥            │  │
│  │    );                                                │  │
│  │    // RSA 2048 加密                                  │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ 5. 发送加密请求                                      │  │
│  │    POST /api/login                                   │  │
│  │    Headers:                                          │  │
│  │      x-encrypted: true  // 标识加密请求              │  │
│  │    Body:                                             │  │
│  │      {                                               │  │
│  │        encryptedKey: "MIIBIj...",  // RSA 加密的密钥 │  │
│  │        encryptedData: "U2FsdG..."  // AES 加密的数据 │  │
│  │      }                                               │  │
│  └──────────────────────────────────────────────────────┘  │
└────────────────────┬───────────────────────────────────────┘
                     │
                     │ HTTPS 加密传输
                     │
                     ▼
┌────────────────────────────────────────────────────────────┐
│                    后端 - 解密流程                          │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ 1. DecryptInterceptor 拦截请求                       │  │
│  │    if (request.headers['x-encrypted'] === 'true') {  │  │
│  │      // 执行解密流程                                 │  │
│  │    }                                                 │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ 2. 用服务端 RSA 私钥解密 AES 密钥                     │  │
│  │    const aesKey = RSA.decrypt(                       │  │
│  │      encryptedKey,                                   │  │
│  │      serverPrivateKey  // 服务端的私钥               │  │
│  │    );                                                │  │
│  │    // 得到: "a1b2c3d4e5f6g7h8"                       │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ 3.AES 密钥解密数据                                │  │
│  │    const decryptedData = AES.decrypt(                │  │
│  │      encryptedData,                                  │  │
│  │      aesKey                                          │  │
│  │    );                                                │  │
│  │    // 得到原始数据                                   │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ 4. 解析 JSON 并替换 request.body                      │  │
│  │    request.body = JSON.parse(decryptedData);         │  │
│  │    // {                                              │  │
│  │    //   username: 'admin',                           │  │
│  │    //   password: 'admin123'                         │  │
│  │    // }                                              │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ 5. 后续流程使用解密后的数据                          │  │
│  │    ControllerService 直接使用 request.body       │  │
│  │    完全透明,无需关心加解密逻辑                       │  │
│  └──────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────┘

安全优势:
┌────────────────────────────────────────────────────────────┐
│  ✓ 双重加密: AES (对称) + RSA (非对称)                      │
│  ✓ 每次请求的 AES 密钥都不同,无法重放攻击                   │
│  ✓ RSA 私钥只存在服务端,前端无法解密                        │
│  ✓ 即使 HTTPS 被破解,数据仍然加密                           │
│  ✓ 密码等敏感信息永不明文传输                               │
└────────────────────────────────────────────────────────────┘

🎯 核心功能模块详解

1️⃣ 系统管理模块

👤 用户管理

完整的用户生命周期管理,支持企业级用户体系:

  • 用户 CRUD:新增、编辑、删除、批量操作
  • 角色分配:支持一个用户多个角色
  • 部门归属:关联组织架构,实现数据权限隔离
  • 密码管理:密码重置、密码强度验证、定期修改提醒
  • 状态管理:启用/禁用、锁定/解锁
  • 导入导出:批量导入用户、导出 Excel
  • 用户画像:登录统计、操作记录、权限视图
🎭 角色管理

基于 RBAC 的灵活权限控制:

  • 权限分配:菜单权限树选择,支持半选状态
  • 数据权限:全部数据/本部门/本部门及子部门/仅本人
  • 角色继承:支持角色间的权限继承关系
  • 动态权限:权限变更实时生效,无需重新登录
  • 权限预览:可视化展示角色拥有的所有权限
📋 菜单管理

动态菜单配置,支持无限层级:

  • 树形结构:可视化的菜单树编辑
  • 菜单类型:目录、菜单、按钮三种类型
  • 图标选择:内置图标库,支持自定义
  • 路由配置:前端路由路径、组件路径
  • 权限标识:按钮级权限控制标识
  • 显示控制:菜单显示/隐藏、缓存控制
  • 外链菜单:支持外部链接菜单
🏢 部门管理

企业组织架构管理:

  • 树形结构:无限层级的部门树
  • 部门负责人:设置部门负责人
  • 排序控制:自定义部门显示顺序
  • 数据权限:基于部门的数据隔离
  • 人员统计:部门人员数量统计
💼 岗位管理

岗位体系管理:

  • 岗位定义:岗位名称、编码、排序
  • 状态管理:启用/停用岗位
  • 人员关联:查看岗位下的所有人员
📖 字典管理

系统数据字典统一管理:

  • 字典类型:定义字典分类(如:用户状态、性别等)
  • 字典数据:维护具体的字典项
  • 缓存支持:字典数据自动缓存,提升性能
  • 前端使用:前端统一调用字典接口
⚙️ 参数配置

系统参数动态配置:

  • 配置项管理:新增、编辑、删除配置项
  • 配置分类:系统配置、业务配置等
  • 缓存刷新:配置变更自动刷新缓存
  • 配置校验:支持配置值格式验证
📢 通知公告

系统公告发布与管理:

  • 公告发布:富文本编辑器,支持图文混排
  • 公告类型:通知、公告、系统消息
  • 发布控制:立即发布、定时发布
  • 阅读状态:已读/未读状态跟踪

2️⃣ 系统监控模块

👥 在线用户

实时监控在线用户状态:

  • 在线列表:显示当前所有在线用户
  • 用户信息:用户名、IP 地址、登录时间、浏览器
  • 强制下线:管理员可强制用户下线
  • 会话管理:查看用户会话信息
  • 实时统计:在线用户数量统计
📝 操作日志

完整的操作审计系统:

  • 自动记录:通过装饰器自动记录操作
  • 详细信息:操作人、操作时间、操作类型、请求参数、响应结果
  • 异常捕获:自动记录异常操作和错误堆栈
  • 条件查询:按用户、时间、模块、操作类型查询
  • 数据导出:导出日志用于审计
🔐 登录日志

登录安全审计:

  • 登录记录:成功/失败的登录记录
  • 安全信息:IP 地址、地理位置、浏览器、操作系统
  • 异常检测:异常登录行为提醒
  • 统计分析:登录时段分析、地域分析
⏰ 定时任务

灵活的任务调度系统:

  • Cron 表达式:支持标准 Cron 表达式
  • 任务管理:启动、停止、立即执行
  • 执行日志:任务执行历史、成功/失败记录
  • 并发控制:任务并发执行控制
  • 超时设置:任务执行超时时间设置
  • 错误重试:失败自动重试机制
🖥️ 系统监控

服务器运行状态监控:

  • CPU 监控:CPU 使用率、核心数
  • 内存监控:内存使用情况、JVM 信息
  • 磁盘监控:磁盘使用率、读写速度
  • 网络监控:网络流量统计
  • 进程信息:进程 PID、运行时长
💚 健康检查

K8s 友好的健康检查:

  • Liveness 探针:应用存活检查
  • Readiness 探针:应用就绪检查
  • 数据库检查:PostgreSQL 连接状态
  • Redis 检查:Redis 连接状态
  • 磁盘检查:磁盘空间检查
  • 内存检查:内存使用检查
  • Prometheus 指标:暴露 /api/metrics 端点
📁 文件上传

多存储支持的文件管理:

  • 本地存储:存储到服务器本地
  • 阿里云 OSS:存储到阿里云对象存储
  • 七牛云:存储到七牛云
  • MinIO:私有化对象存储
  • 文件预览:图片、PDF 等在线预览
  • 缩略图:自动生成图片缩略图

3️⃣ 多租户管理模块

🏘️ 租户管理

完整的 SaaS 租户管理:

  • 租户 CRUD:新增、编辑、删除租户
  • 租户信息:租户名称、联系人、到期时间
  • 套餐绑定:为租户分配功能套餐
  • 状态管理:启用、停用、过期控制
  • 数据隔离:自动的租户数据隔离
  • 容量限制:用户数、存储空间限制
📦 租户套餐

灵活的套餐体系:

  • 套餐定义:基础版、标准版、企业版
  • 功能权限:菜单权限按套餐分配
  • 资源限制:用户数、存储空间限制
  • 套餐升级:支持套餐在线升级
🔒 数据隔离

安全的多租户数据隔离:

  • 自动过滤:数据库查询自动按租户过滤
  • 跨租户查询:超级管理员可查询所有租户
  • 租户切换:支持切换查看不同租户数据
  • 数据迁移:租户数据导入导出

4️⃣ 演示账户系统

专为产品演示设计的安全机制:

  • 只读权限:52 个查询权限,可查看所有模块
  • 写操作拦截:自动拦截所有 POST/PUT/DELETE/PATCH 请求
  • 友好提示:操作被拦截时给出友好提示
  • 灵活配置:基于 RBAC 可随时调整权限范围
  • 演示重置:定时重置演示数据(可选)

🚀 快速开始

环境要求

  • Node.js >= 20.19.0
  • PostgreSQL >= 14
  • Redis >= 7
  • pnpm >= 8.0

安装步骤

1. 克隆项目

git clone https://github.com/your-repo/nest-admin-soybean.git
cd nest-admin-soybean

2. 后端配置

cd server
pnpm install

# 生成 RSA 密钥对(用于加密)
pnpm generate:keys

# 配置数据库连接
# 编辑 src/config/index.ts 中的数据库配置

# 初始化数据库
pnpm prisma:seed

3. 前端配置

cd admin-naive-ui
pnpm install

# 配置后端接口地址
# 编辑 .env.development 文件

4. 启动项目

# 启动后端 (8080端口)
cd server
pnpm start:dev

# 启动前端 (9527端口)
cd admin-naive-ui
pnpm dev

5. 访问系统

默认账号

  • 超级管理员:admin / admin123 (租户 000000)
  • 演示账户:demo / demo123 (租户 000000)

💪 技术亮点详解

1. 多租户实现原理

核心思路:通过 Prisma Extension 在 ORM 层面实现透明的租户过滤。

// tenant.extension.ts
export function tenantExtension(tenantId: string) {
  return Prisma.defineExtension({
    query: {
      // 对所有模型的查询自动添加租户过滤
      $allModels: {
        async findMany({ args, query }) {
          args.where = { ...args.where, tenantId };
          return query(args);
        },
        // ... findUnique, create, update, delete 同理
      }
    }
  });
}

// prisma.service.ts
get client() {
  const tenantId = TenantContext.getTenantId();
  if (!tenantId) return this._client;
  return this._client.$extends(tenantExtension(tenantId));
}

优势

  • 业务代码无需关心租户逻辑
  • 避免忘记添加租户条件导致的数据泄露
  • 统一管理,易于维护

2. 请求加密的实现

前端加密

// encryption.ts
export function encryptRequest(data: any) {
  // 1. 生成随机 AES 密钥
  const aesKey = CryptoJS.lib.WordArray.random(16);
  
  // 2. AES 加密数据
  const encryptedData = CryptoJS.AES.encrypt(
    JSON.stringify(data), 
    aesKey,
    { mode: CryptoJS.mode.CBC }
  ).toString();
  
  // 3. RSA 加密 AES 密钥
  const encryptedKey = rsaEncrypt(aesKey.toString(), serverPublicKey);
  
  return { encryptedKey, encryptedData };
}

后端解密

// decrypt.interceptor.ts
@Injectable()
export class DecryptInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    const request = context.switchToHttp().getRequest();
    
    if (request.headers['x-encrypted'] === 'true') {
      const { encryptedKey, encryptedData } = request.body;
      
      // 1. RSA 解密 AES 密钥
      const aesKey = rsaDecrypt(encryptedKey, privateKey);
      
      // 2. AES 解密数据
      const decryptedData = aesDecrypt(encryptedData, aesKey);
      
      // 3. 替换 body
      request.body = JSON.parse(decryptedData);
    }
    
    return next.handle();
  }
}

3. 权限系统的设计

采用装饰器 + 守卫的组合模式:

// permission.guard.ts
@Injectable()
export class PermissionGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const requiredPermission = this.reflector.get(
      'permission',
      context.getHandler()
    );
    
    if (!requiredPermission) return true;
    
    const user = context.switchToHttp().getRequest().user;
    return user.permissions.includes(requiredPermission);
  }
}

// 使用
@RequirePermission('system:user:edit')
@Put(':id')
updateUser(@Param('id') id: string, @Body() dto: UpdateUserDto) {
  return this.userService.update(id, dto);
}

权限数据结构

// 权限标识:模块:功能:操作
'system:user:add'      // 添加用户
'system:user:edit'     // 编辑用户
'system:user:delete'   // 删除用户
'system:user:query'    // 查询用户

4. 日志系统的优化

自动日志收集

// logging.interceptor.ts
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  constructor(private logger: Logger) {}
  
  intercept(context: ExecutionContext, next: CallHandler) {
    const request = context.switchToHttp().getRequest();
    const startTime = Date.now();
    
    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - startTime;
        this.logger.info({
          requestId: request.id,
          tenantId: request.tenantId,
          username: request.user?.username,
          method: request.method,
          url: request.url,
          statusCode: context.switchToHttp().getResponse().statusCode,
          duration,
        });
      })
    );
  }
}

敏感字段脱敏

// 自动隐藏敏感字段
const sensitiveFields = ['password', 'token', 'secret'];
this.logger.info(redactSensitive(data, sensitiveFields));

🎨 前端特色

1. 文件路由系统

使用 @elegant-router/vue 实现基于文件的路由:

src/views/
├── system/
│   ├── user/
│   │   └── index.vue     → /system/user
│   ├── role/
│   │   └── index.vue     → /system/role
│   └── menu/
│       └── index.vue     → /system/menu

自动生成路由

pnpm gen-route

2. 原子化 CSS

使用 UnoCSS,支持 Tailwind 风格:

<template>
  <div class="flex items-center justify-between p-4 bg-white dark:bg-dark">
    <span class="text-lg font-bold">用户管理</span>
    <n-button type="primary">添加用户</n-button>
  </div>
</template>

3. 状态管理

Pinia setup 语法:

// useAuthStore.ts
export const useAuthStore = defineStore('auth', () => {
  const token = ref(getToken());
  const userInfo = ref<UserInfo | null>(null);
  
  async function login(credentials: LoginDto) {
    const { token: newToken, user } = await api.login(credentials);
    token.value = newToken;
    userInfo.value = user;
    setToken(newToken);
  }
  
  return { token, userInfo, login };
});

4. 请求封装

统一的 Axios 封装,支持自动加密:

// api.ts
export function fetchUserList(params: UserQueryDto) {
  return request<PageResult<User>>({
    url: '/system/user',
    method: 'GET',
    params
  });
}

export function createUser(data: CreateUserDto) {
  return request({
    url: '/system/user',
    method: 'POST',
    data,
    encrypt: true  // 自动加密
  });
}

🔮 未来规划

  • 微服务架构:拆分为独立的微服务
  • 消息队列:集成 RabbitMQ/Kafka
  • 分布式追踪:接入 OpenTelemetry
  • GraphQL API:提供 GraphQL 接口
  • 移动端:开发配套的移动应用
  • 低代码平台:可视化表单设计器
  • 更多数据库:支持 MySQL、MongoDB
  • 云原生:K8s Helm Chart

如果觉得不错,欢迎 Star ⭐️:github.com/linlingqin7…

#NestJS #Vue3 #后台管理系统 #全栈开发 #开源项目

Three.js 入门指南:揭开 3D 网页的魔法面纱

作者 zYear
2025年12月23日 16:16

系列介绍

在 webgl 没出现之前,如果我们要在页面展示 3D 动画的效果,我们需要借助一些浏览器的插件进行实现。因此在 2011 年, webgl 诞生了,这套应用与 3D 应用的 API 能够让开发者使用 JS 语言与 GPU 进行通信,从而实现各式各样的 3D 效果。而本系列将借由 Three.js 对 webgl 进行介绍说明。

Three.js

Three.js 是一个基于 webgl 开发的开源库,它就像 echarts 建立于 canvas 上,通过 Three.js 我们可以不用直接接触最底层的内容,包括: 着色器、缓冲区、矩阵变换的内容,从而大大减小入门的难度。但本系列在通过介绍 Three.js 的过程中,也会插入一些 webgl 的基础知识,从而帮助你更好的理解。

本文的参考资料

Three.js

Three.js中文网 Three.js – JavaScript 3D Library www.webgl3d.cn/

webgl

WebGL 理论基础 WebGL:web 中的 2D 的 3D 图形 - Web API | MDN Three.js中文网

Three.js 的三大基础概念

Three.js 的运行机制

在 Three.js 中,要实现渲染一个3D的物体,首先我们需要一个 虚拟的场景,然后通过将我们需要展示3D物体添加到场景中,于是便有了一个“虚拟的世界”,而想要看这个虚拟的世界,便需要通过 摄像机 去拍摄,最后通过 渲染器 解析当前摄像机看到的内容,从而展示给用户。而这也使得摄像头被移动之后,仍需要重新进行渲染才可以展示出内容。

大白话

你可以理解为你在给一辆跑车拍一张海报,此时首先需要 一辆车(物体)一个展台(场景),然后我们拿着摄像机,选好我们的站位,瞄准我们需要拍摄的车,此时 摄像机显示屏(摄像机) 就展示我们能看到的东西。最后我们需要通 按下快门键(渲染器)**,才能看到我们的海报。

场景

场景 Scene

场景的创建在 Three.js 中也十分的简单,我们可以通过 Three.Scene() 进行创建,并且将他赋值给一个变量,于是我们便可以通过 add 方法将物体放置到场景中。

// 你可以通过 Scene 方法创建一个场景
const Scene = new THREE.Scene()

物体

物体,是我们在开发3D场景中极为重要的内容,毕竟场景终究只是一个空壳,具体的样子和内容还需要通过物体来展示,因此物体和场景可以放在一起进行说明。而要创建一个物体,我们首先需要对一个物体进行 外形材质 的描述。

webgl 渲染管线

由于 Three.js 是基于 WebGL 构建的高级 3D 库,理解 WebGL 的渲染管线有助于我们更深入地掌握 Three.js 中物体的渲染机制。因此,在介绍 Three.js 中的物体、几何体和材质之前,我们先简要了解 WebGL 的渲染管线。

顶点着色器阶段

顶点着色器是渲染管线的第一个阶段,他会通过 GPU 进行计算,将各个顶点转换为坐标值(裁剪空间坐标值),并且处理法线、颜色和纹理坐标等顶点数据。最终绘制顶点到相应的坐标

图元装配器

在 webgl 中,图元装配器 可以通过你输入的类型(三角面,线段,连续路径,...) 。然后将所有的顶点坐标转换为不同类型的组,从而使得后面光栅化能够对其进行材质处理(光栅化与上色)。注意: 在 webgl 中,任何面都是由三角面组合而成的

光栅化阶段

这一阶段你可以理解为绘制,也就是通过像素将内容绘制出来。通过对前面已经组成好的图形进行基础(不带颜色的)像素填充,并且每一个图形中都包含它对应的颜色、纹理等信息。

片元着色器阶段

片元着色器阶段是可以理解为上色阶段,它会处理每个图形中的信息,对其进行上色等操作。 ![[Pasted image 20251218101056.png]]

几何形状 Geometry

在 Three.js 中,你可以将几何形状理解为Three.js 对于顶点着色器的顶点位置的重新封装,我们只需要通过对应几何形状的 API 直接生成相应的顶点数据对象,然后交由图元装配器进行装配。我们在视图中看到模型的形状就是其决定的。

// 创建一个正方形
const geometry = new THREE.BoxGeometry(100, 100, 100)
材质 Material

Material 的作用是对于一个物体的外观的描述,例如: 表面的材质,颜色,透明度等,它决定了我们在视图中看到的模型的样式。其中,在 Three.js 中的面是分为正反两面的,当材质为单面时,我们只能从正面看到该物体。

const material = new THREE.side: THREE.DoubleSide,({
color: 0x00ffff,
side: THREE.DoubleSide, // 决定你的材质的面是单面还是双面
})
模型(物体) Objecet

在 Three.js 中,Mesh(网格)、Line(线)和 Points(点云)是三种常见的可渲染对象。它们都由 Geometry(几何体) 定义形状,由 Material(材质) 定义外观。只有将这些对象添加到场景(Scene)中,并通过渲染器(Renderer)进行绘制,我们才能在屏幕上看到由 Geometry 和 Material 共同定义的视觉结果。

// 创建一个网格模型对象
const mesh = new THREE.Mesh(geometry, material)
// 将网格模型对象添加到三维场景中
scene.add(mesh)

摄像机

在 Three.js 中摄像机可以理解为相机的显示器,它的类型,位置,透视将决定了我们能看到的内容,并且我们也可以随时通过移动摄像机来更换我们想看到的场景内容。

PerspectiveCamera(透视摄像机)

这是摄像机中的一种类型,通过对这个摄像机来了解一些摄像机的基本概念。

  • fov: 摄像机竖直方向的角度
  • aspect: 摄像机的长宽比(画幅)
  • near: 摄像机的近截面(你的摄像机最近的界限)
  • far: 摄像机的远截面(你的摄像机最远的界限)
// 创建透视相机
const width = 800
const height = 500
const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000)

// 摄像机的起始位置
camera.position.set(200, 200, 200)
// 摄像机对准的位置
camera.lookAt(0, 0, 0)

渲染

渲染就像我们按下摄像机的快门。当我们对当前的场景和摄像机放到同一个渲染器中时,我们便可以通过渲染器将当前摄像机看到的内容进行渲染,渲染过后渲染器会生成一个canvas,然后放置到页面中。然后我们便可以在页面中看到我们的模型。

// 创建渲染器
const render = new THREE.WebGLRenderer()
render.setSize(width, height)
// 渲染出内容
render.render(scene, camera)
// 输出内容
document.body.appendChild(render.domElement)

案例

接下来,让我们通过一个基础的立方体来回顾我们前面的知识

// 创建三维场景
  const scene = new THREE.Scene()

  // 创建一个正方形
  const geometry = new THREE.BoxGeometry(100, 100, 100)
  // 创建一个基础材质
  const material = new THREE.MeshBasicMaterial({
color: 0x00ffff,
side: THREE.DoubleSide, // 决定你的材质是单面还是双面
  })

  // 创建一个坐标轴,便于观察模型位置
  const axios = new THREE.AxesHelper(300)
  scene.add(axios)

  // 创建一个网格模型对象
  const mesh = new THREE.Mesh(geometry, material)
  // 将网格模型对象添加到三维场景中
  scene.add(mesh)

  // 创建透视相机
  const width = 800
  const height = 500
  const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000)

  // 摄像机的起始位置
  camera.position.set(200, 200, 200)
  // 摄像机对准的位置
  camera.lookAt(0, 0, 0)

  // 创建渲染器
  const render = new THREE.WebGLRenderer()
  render.setSize(width, height)
  // 渲染出内容
  render.render(scene, camera)
  // 输出内容
  document.body.appendChild(render.domElement)     
下章预告

---《Three.js 入门指南:让网页“立体”起来的秘密》

Ant Design 6.0 尝鲜:上手现代化组件开发|得物技术

作者 得物技术
2025年12月23日 14:12

一、引言

组件体验的革新

在前端开发领域,Ant Design 一直是企业级 React 应用的首选 UI 库之一。随着 Ant Design 6.0 的发布,我们又见证了一次聚焦于组件功能与用户体验的革新。本次更新不仅引入了多个全新组件,更对现有核心组件进行了功能性增强,使开发者能够以更少的代码实现更丰富的交互效果。

二、Masonry 瀑布流组件:智能动态布局

传统网格布局在处理高度不一的元素时常出现大量空白区域,Masonry(瀑布流)布局则完美解决了这一问题。Ant Design 6.0 全新推出的 Masonry 组件让实现这种流行布局变得异常简单。

基础实现与响应式配置

import { useState, useEffect, useRef } from "react";
import { Masonry, Card, Image, Spin } from "antd";
/**
 * Masonry瀑布流页面
 */
export default () => {
  const [isLoading, setIsLoading] = useState(false);
  const containerRef = useRef<HTMLDivElement>(null);
  const isLoadingRef = useRef(false);
  const imageList = [
    "https://images.xxx.com/photo-xxx-4b4e3d86bf0f",
    ...
    "https://images.xxx.com/photo-xxx-98f7befd1a60",
  ];
  const titles = [
    "山间日出",
    ...
    "自然风光",
  ];
  const descriptions = [
    "清晨的第一缕阳光",
    ...
    "色彩鲜艳的料理",
  ];
  const heights = [240260280300320350380400];
  // Mock数据生成函数
  const generateMockData = (startIndex: number, count: number) => {
    return Array.from({ length: count }, (_, index) => ({
      id: startIndex + index + 1,
      src: imageList[Math.floor(Math.random() * imageList.length)],
      title: titles[(startIndex + index) % titles.length],
      description: descriptions[(startIndex + index) % descriptions.length],
      height: heights[Math.floor(Math.random() * heights.length)],
    }));
  };
  // 初始数据:20条
  const [photoItems, setPhotoItems] = useState(() => generateMockData(0, 20));
  // 滚动监听
  useEffect(() => {
    isLoadingRef.current = isLoading;
  }, [isLoading]);
  useEffect(() => {
    const loadMoreData = async () => {
      if (isLoadingRef.current) return;
      isLoadingRef.current = true;
      setIsLoading(true);
      // 模拟API请求延迟
      await new Promise((resolve) => setTimeout(resolve, 500));
      setPhotoItems((prev) => {
        const newItems = generateMockData(prev.length, 10);
        return [...prev, ...newItems];
      });
      isLoadingRef.current = false;
      setIsLoading(false);
    };
    const checkScroll = () => {
      if (isLoadingRef.current) return;
      const container = containerRef.current;
      if (!container) return;
      const scrollTop = container.scrollTop;
      const scrollHeight = container.scrollHeight;
      const clientHeight = container.clientHeight;
      // 当滚动到距离底部100px时触发加载
      if (scrollTop + clientHeight >= scrollHeight - 100) {
        loadMoreData();
      }
    };
    const handleWindowScroll = () => {
      if (isLoadingRef.current) return;
      const windowHeight = window.innerHeight;
      const documentHeight = document.documentElement.scrollHeight;
      const scrollTop =
        window.pageYOffset || document.documentElement.scrollTop;
      // 当滚动到距离底部100px时触发加载
      if (scrollTop + windowHeight >= documentHeight - 100) {
        loadMoreData();
      }
    };
    const container = containerRef.current;
    // 初始检查一次,以防内容不足一屏
    setTimeout(() => {
      checkScroll();
      handleWindowScroll();
    }, 100);
    // 监听容器滚动
    if (container) {
      container.addEventListener("scroll", checkScroll);
    }
    // 同时监听 window 滚动(作为备选)
    window.addEventListener("scroll", handleWindowScroll);
    return () => {
      if (container) {
        container.removeEventListener("scroll", checkScroll);
      }
      window.removeEventListener("scroll", handleWindowScroll);
    };
  }, []);
  return (
    <div ref={containerRef} className="w-full h-[100vh] overflow-auto p-[24px]">
      <Masonry
        // 响应式列数配置
        columns={{ xs: 2, sm: 3, md: 4, lg: 5 }}
        // 列间距与行间距
        gutter={16}
        items={photoItems as any}
        itemRender={(item: any) => (
          <Card
            key={item.id}
            hoverable
            cover={
              <div style={{ height: item.height, overflow: "hidden" }}>
                <Image
                  src={item.src}
                  alt={item.title}
                  height={item.height}
                  width="100%"
                  style={{
                    width: "100%",
                    height: "100%",
                    objectFit: "cover",
                  }}
                  preview={{
                    visible: false,
                  }}
                />
              </div>
            }
            styles={{
              body: {
                padding: "12px",
              },
            }}
          >
            <Card.Meta title={item.title} description={item.description} />
            <div
              className="mt-[8px] text-[12px] text-[#999]"
            >
              图片 #{item.id}
            </div>
          </Card>
        )}
      />
      {isLoading && (
        <div
          className="flex items-center justify-center p-[20px] text-[#999]"
        >
          <Spin style={{ marginRight: "8px" }} />
          <span>加载中...</span>
        </div>
      )}
    </div>
  );
};

布局效果说明

Masonry 组件会根据设定的列数自动将元素排列到高度最小的列中。与传统的网格布局相比,这种布局方式能有效减少内容下方的空白区域,特别适合展示高度不一的内容块。

对于图片展示类应用,这种布局能让用户的视线自然流动,提高浏览的沉浸感和内容发现率。

三、Tooltip 平滑移动:优雅的交互引导

在 Ant Design 6.0 中,Tooltip 组件引入了独特的平滑过渡效果,通过 ConfigProvider 全局配置的 tooltip.unique 配置项,当用户在多个带有提示的元素间移动时,提示框会以流畅的动画跟随,而不是突然消失和重现。

实现平滑跟随效果

import { Tooltip, Button, Card, ConfigProvider } from "antd";
import {
  UserOutlined,
  SettingOutlined,
  BellOutlined,
  MailOutlined,
  AppstoreOutlined,
} from "@ant-design/icons";
import { TooltipPlacement } from "antd/es/tooltip";
/**
 * Tooltip 示例
 */
export default () => {
  const buttonItems = [
    {
      icon: <UserOutlined />,
      text: "个人中心",
      tip: "查看和管理您的个人资料",
      placement: "top",
    },
    {
      icon: <SettingOutlined />,
      text: "系统设置",
      tip: "调整应用程序参数和偏好",
      placement: "top",
    },
    {
      icon: <BellOutlined />,
      text: "消息通知",
      tip: "查看未读提醒和系统消息",
      placement: "top",
    },
    {
      icon: <MailOutlined />,
      text: "邮箱",
      tip: "收发邮件和管理联系人",
      placement: "bottom",
    },
    {
      icon: <AppstoreOutlined />,
      text: "应用中心",
      tip: "探索和安装更多应用",
      placement: "bottom",
    },
  ];
  return (
    <div className="w-full h-[100vh] overflow-auto p-[24px] space-y-5">
      <ConfigProvider
        tooltip={{
          unique: true,
        }}
      >
        <Card title="平滑过渡导航工具栏" bordered={false}>
          <div className="flex justify-center gap-6 py-10 px-5 bg-gradient-to-br from-[#f5f7fa] to-[#c3cfe2] rounded-3xl">
            {buttonItems.map((item, index) => (
              <Tooltip
                placement={item.placement as TooltipPlacement}
                key={index}
                title={
                  <div>
                    <div className="font-bold mb-1">{item.text}</div>
                    <div className="text-xs text-[#fff]/60">{item.tip}</div>
                  </div>
                }
                color="#1677ff"
              >
                <Button
                  type="primary"
                  shape="circle"
                  icon={item.icon}
                  size="large"
                  className="w-[60px] h-[60px] text-2xl shadow-md transition-all duration-300 ease-in-out"
                />
              </Tooltip>
            ))}
          </div>
          <div className="mt-5 p-4 bg-green-50 border border-green-300 rounded-md">
            <div className="flex items-center">
              <div className="w-3 h-3 rounded-full bg-green-500 mr-2"></div>
              <span>
                提示:尝试将鼠标在不同图标间快速移动,观察 Tooltip
                的平滑过渡效果
              </span>
            </div>
          </div>
        </Card>
      </ConfigProvider>
      <Card title="非平滑过渡导航工具栏" bordered={false}>
        <div className="flex justify-center gap-6 py-10 px-5 bg-gradient-to-br from-[#f5f7fa] to-[#c3cfe2] rounded-3xl">
          {buttonItems.map((item, index) => (
            <Tooltip
              key={index}
              placement={item.placement as TooltipPlacement}
              title={
                <div>
                  <div className="font-bold mb-1">{item.text}</div>
                  <div className="text-xs text-[#fff]/60">{item.tip}</div>
                </div>
              }
              color="#1677ff"
            >
              <Button
                type="primary"
                shape="circle"
                icon={item.icon}
                size="large"
                className="w-[60px] h-[60px] text-2xl shadow-md transition-all duration-300 ease-in-out"
              />
            </Tooltip>
          ))}
        </div>
        <div className="mt-5 p-4 bg-green-50 border border-green-300 rounded-md">
          <div className="flex items-center">
            <div className="w-3 h-3 rounded-full bg-green-500 mr-2"></div>
            <span>
              提示:尝试将鼠标在不同图标间快速移动,观察 Tooltip 的非平滑过渡效果
            </span>
          </div>
        </div>
      </Card>
    </div>
  );
};

2.gif

交互效果说明

当 tooltip.unique 设置为 true 时,用户在不同元素间移动鼠标时,Tooltip 会呈现以下行为:

  1. 平滑位置过渡:Tooltip 不会立即消失,而是平滑移动到新目标位置

  2. 内容无缝切换:提示内容在新位置淡入,旧内容淡出

  3. 视觉连续性:保持同一时刻只有一个 Tooltip 显示,避免界面混乱

这种设计特别适合工具栏、导航菜单等元素密集的区域,能有效降低用户的认知负荷,提供更加流畅的交互体验。

四、InputNumber 拨轮模式:直观的数字输入

数字输入框是表单中的常见组件,但传统的上下箭头控件在小屏幕或触摸设备上操作不便。Ant Design 6.0 的 InputNumber 组件新增了 mode="spinner" 属性,提供了更直观的“加减按钮”界面。

拨轮模式实现

import { InputNumber, Card, Row, Col, Typography, Space } from "antd";
import {
  ShoppingCartOutlined,
  DollarOutlined,
  GiftOutlined,
} from "@ant-design/icons";
const { Title, Text } = Typography;
/**
 * InputNumber 示例
 */
export default () => {
  return (
    <div className="w-full h-[100vh] overflow-auto p-[24px] space-y-5">
      <Card title="商品订购面板" bordered={false}>
        <Row gutter={[2424]}>
          <Col span={8}>
            <div className="text-center">
              <div className="w-[80px] h-[80px] mx-auto mb-4 rounded-3xl bg-[#f0f5ff] flex items-center justify-center text-[32px] text-[#1677ff]">
                <ShoppingCartOutlined />
              </div>
              <Title level={5} className="mb-3">
                购买数量(非数字拨轮)
              </Title>
              <InputNumber
                min={1}
                max={50}
                defaultValue={1}
                size="large"
                className="w-[250px]!"
                addonBefore="数量"
              />
              <div className="mt-2 text-xs text-gray-600">限购50件</div>
            </div>
          </Col>
          <Col span={8}>
            <div className="text-center">
              <div className="w-[80px] h-[80px] mx-auto mb-4 rounded-3xl bg-[#fff7e6] flex items-center justify-center text-[32px] text-[#fa8c16]">
                <DollarOutlined />
              </div>
              <Title level={5} className="mb-3">
                折扣力度(数字拨轮)
              </Title>
              <InputNumber
                min={0}
                max={100}
                defaultValue={10}
                mode="spinner"
                size="large"
                formatter={(value) => `${value ?? 0}%`}
                parser={(value) =>
                  Number.parseFloat(value?.replace("%", "") ?? "0") as any
                }
                className="w-[250px]!"
                addonBefore="折扣"
              />
              <div className="mt-2 text-xs text-gray-600">0-100%范围</div>
            </div>
          </Col>
          <Col span={8}>
            <div className="text-center">
              <div className="w-[80px] h-[80px] mx-auto mb-4 rounded-3xl bg-[#f6ffed] flex items-center justify-center text-[32px] text-[#52c41a]">
                <GiftOutlined />
              </div>
              <Title level={5} className="mb-3">
                礼品数量(数字拨轮,自定义加减按钮)
              </Title>
              <Space.Compact block className="justify-center!">
                <Space.Addon>
                  <span>礼品</span>
                </Space.Addon>
                <InputNumber
                  min={0}
                  max={10}
                  defaultValue={0}
                  mode="spinner"
                  size="large"
                  className="w-[250px]!"
                  controls={{
                    upIcon: <span className="text-base">➕</span>,
                    downIcon: <span className="text-base">➖</span>,
                  }}
                />
              </Space.Compact>
              <div className="mt-2 text-xs text-gray-600">每单最多10份</div>
            </div>
          </Col>
        </Row>
        <div className="mt-8 p-4 bg-[#fff0f6] rounded-lg border border-dashed border-[#ffadd2]">
          <Text type="secondary">
            <strong>设计提示:</strong>
            拨轮模式相比传统箭头控件,提供了更大的点击区域和更明确的视觉反馈,特别适合触摸设备和需要频繁调整数值的场景。加减按钮的分离式设计也降低了误操作的可能性。
          </Text>
        </div>
      </Card>
    </div>
  );
};

3.gif

交互优势分析

拨轮模式相比传统数字输入框具有明显优势:

  1. 触摸友好:更大的按钮区域适合移动端操作

  2. 意图明确:“+”和“-”符号比小箭头更直观

  3. 快速调整:支持长按连续增减数值

  4. 视觉反馈:按钮有明确的状态变化(按下、悬停)

在电商、数据仪表盘、配置面板等需要频繁调整数值的场景中,这种设计能显著提升用户的操作效率和满意度。

五、Drawer 拖拽调整:灵活的侧边面板

抽屉组件常用于移动端导航或详情面板,但固定尺寸有时无法满足多样化的内容展示需求。Ant Design 6.0 为 Drawer 组件新增了 resizable 属性,允许用户通过拖拽边缘实时调整面板尺寸。

可调整抽屉实现

import { Drawer, Button, Card, Typography, Divider, List, Flex } from "antd";
import {
  DragOutlined,
  CalendarOutlined,
  FileTextOutlined,
  TeamOutlined,
  CommentOutlined,
  PaperClipOutlined,
} from "@ant-design/icons";
import { useState } from "react";
import { DrawerResizableConfig } from "antd/es/drawer";
const { Title, Text, Paragraph } = Typography;
/**
 * Drawer 示例
 */
export default () => {
  const [open, setOpen] = useState(false);
  const [drawerWidth, setDrawerWidth] = useState(400);
  const [resizable, setResizable] = useState<boolean | DrawerResizableConfig>(
    false,
  );
  const tasks = [
    { id: 1, title: "完成项目需求文档", time: "今天 10:00", priority: "high" },
    { id: 2, title: "团队周会", time: "今天 14:30", priority: "medium" },
    { id: 3, title: "代码审查", time: "明天 09:00", priority: "high" },
    { id: 4, title: "客户演示准备", time: "后天 15:00", priority: "medium" },
  ];
  const showDrawerWithResizable = () => {
    setOpen(true);
    setDrawerWidth(400);
    setResizable({
      onResize: (size) => {
        setDrawerWidth(size);
      },
    });
  };
  const showDrawerWithoutResizable = () => {
    setOpen(true);
    setDrawerWidth(600);
    setResizable(false);
  };
  const onClose = () => {
    setOpen(false);
  };
  return (
    <div className="w-full h-[100vh] flex items-center justify-center overflow-auto p-[24px] space-y-5">
      <Card
        title="任务管理面板"
        variant="outlined"
        className="max-w-[800px] mx-auto"
      >
        <div className="py-10 px-5 text-center">
          <div className="w-20 h-20 mx-auto mb-6 rounded-full bg-[#1677ff] flex items-center justify-center text-[36px] text-white">
            <DragOutlined />
          </div>
          <Title level={3}>可调整的任务详情面板</Title>
          <Paragraph type="secondary" className="max-w-[600px] my-4 mx-auto">
            点击下方按钮打开一个可拖拽调整宽度的抽屉面板。尝试拖动抽屉左侧边缘,根据内容需要调整面板尺寸。
          </Paragraph>
          <Flex justify="center" gap={10}>
            <Button
              type="primary"
              size="large"
              onClick={showDrawerWithResizable}
              icon={<CalendarOutlined />}
              className="mt-6"
            >
              打开任务抽屉(可拖拽宽度)
            </Button>
            <Button
              type="primary"
              size="large"
              onClick={showDrawerWithoutResizable}
              icon={<CalendarOutlined />}
              className="mt-6"
            >
              打开任务抽屉(不可拖拽宽度)
            </Button>
          </Flex>
        </div>
      </Card>
      <Drawer
        title={
          <div className="flex items-center">
            <CalendarOutlined className="mr-2 text-[#1677ff]" />
            <span>任务详情与计划</span>
            {resizable && (
              <div className="ml-3 py-0.5 px-2 bg-[#f0f5ff] rounded-[10px] text-xs text-[#1677ff]">
                可拖拽调整宽度
              </div>
            )}
          </div>
        }
        placement="right"
        onClose={onClose}
        open={open}
        size={drawerWidth}
        resizable={resizable}
        extra={
          <Button type="text" icon={<DragOutlined />}>
            {resizable ? "拖拽边缘调整" : "不可拖拽"}
          </Button>
        }
        styles={{
          body: {
            paddingTop: "12px",
          },
          header: {
            borderBottom: "1px solid #f0f0f0",
          },
        }}
      >
        <div className="mb-6">
          <div className="flex items-center mb-4">
            <FileTextOutlined className="mr-2 text-[#52c41a]" />
            <Title level={5} className="m-0">
              当前任务
            </Title>
          </div>
          <Card size="small">
            <List
              itemLayout="horizontal"
              dataSource={tasks}
              renderItem={(item) => (
                <List.Item>
                  <List.Item.Meta
                    avatar={
                      <div
                        className={`w-8 h-8 rounded-md flex items-center justify-center ${
                          item.priority === "high"
                            ? "bg-[#fff2f0] text-[#ff4d4f]"
                            : "bg-[#f6ffed] text-[#52c41a]"
                        }`}
                      >
                        {item.priority === "high" ? "急" : "常"}
                      </div>
                    }
                    title={<a>{item.title}</a>}
                    description={<Text type="secondary">{item.time}</Text>}
                  />
                </List.Item>
              )}
            />
          </Card>
        </div>
        <Divider />
        <div className="mb-6">
          <div className="flex items-center mb-4">
            <TeamOutlined className="mr-2 text-[#fa8c16]" />
            <Title level={5} className="m-0">
              团队动态
            </Title>
          </div>
          <Paragraph>
            本周团队主要聚焦于项目第三阶段的开发工作,前端组已完成了核心组件的重构,后端组正在进行性能优化。
          </Paragraph>
        </div>
        <div className="mb-6">
          <div className="flex items-center mb-4">
            <CommentOutlined className="mr-2 text-[#722ed1]" />
            <Title level={5} className="m-0">
              最新反馈
            </Title>
          </div>
          <Card size="small" type="inner">
            <Paragraph>
              "新的界面设计得到了客户的积极反馈,特别是可调整的面板设计,让不同角色的用户都能获得适合自己工作习惯的布局。"
            </Paragraph>
            <Text type="secondary">—— 产品经理,XXX</Text>
          </Card>
        </div>
        <div>
          <div className="flex items-center mb-4">
            <PaperClipOutlined className="mr-2 text-[#eb2f96]" />
            <Title level={5} className="m-0">
              使用提示
            </Title>
          </div>
          <div className="p-3 bg-[#f6ffed] rounded-md border border-[#b7eb8f]">
            <Text type="secondary">
              当前抽屉宽度: <strong>{drawerWidth}px</strong>。您可以: 1.
              拖动左侧边缘调整宽度 2. 内容区域会根据宽度自动重新布局 3.
              适合查看不同密度的信息
            </Text>
          </div>
        </div>
      </Drawer>
    </div>
  );
};

拖拽交互的价值

可调整抽屉的设计带来了明显的用户体验提升:

  1. 自适应内容:用户可以根据当前查看的内容类型调整面板尺寸

  2. 个性化布局:不同用户或场景下可以设置不同的面板大小

  3. 多任务处理:宽面板适合详情查看,窄面板适合边操作边参考

  4. 渐进式披露:可以从紧凑视图逐步展开到详细视图

六、Modal 背景模糊:朦胧美学的视觉升级

在传统 Web 应用中,模态框的遮罩层往往是简单的半透明黑色,视觉效果单调且缺乏现代感。而在 iOS 和 macOS 等系统中,毛玻璃(frosted glass)效果已成为标志性的设计语言效果样式。Ant Design 6.0 为所有弹层组件引入了原生背景模糊支持,并提供了强大的语义化样式定制能力,让开发者能轻松打造出高级感十足的视觉效果。

背景模糊与语义化样式定制

以下示例展示了如何结合 Ant Design 6.0 的背景模糊特性和 antd-style 库,实现两种不同风格的模态框:

import { useState } from "react";
import { Button, Flex, Modal, Card, Image, Typography, Space } from "antd";
import type { ModalProps } from "antd";
import { createStyles } from "antd-style";
const { Title, Text } = Typography;
// 使用 antd-style 的 createStyles 定义样式
const useStyles = createStyles(({ token }) => ({
  // 用于模态框容器的基础样式
  container: {
    borderRadius: token.borderRadiusLG * 1.5,
    overflow: "hidden",
  },
}));
// 示例用的共享内容
const sharedContent = (
  <Card size="small" bordered={false}>
    <Image
      height={300}
      src="https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp"
      alt="示例图片"
      preview={false}
      className="mx-auto!"
    />
    <Text type="secondary" style={{ display: "block", marginTop: 8 }}>
      Ant Design 6.0 默认的模糊背景与 antd-style
      定制的毛玻璃面板相结合,营造出深邃而富有层次的视觉体验。
    </Text>
  </Card>
);
export default () => {
  const [blurModalOpen, setBlurModalOpen] = useState(false);
  const [gradientModalOpen, setGradientModalOpen] = useState(false);
  const { styles: classNames } = useStyles();
  // 场景1:背景玻璃模糊效果(朦胧美学)
  const blurModalStyles: ModalProps["styles"] = {
    body: {
      padding: 24,
    },
  };
  // 场景2:渐变色背景模态框(无模糊效果)
  const gradientModalStyles: ModalProps["styles"] = {
    mask: {
      backgroundImage: `linear-gradient(
        135deg, 
        rgba(99, 102, 241, 0.8) 0%, 
        rgba(168, 85, 247, 0.6) 50%, 
        rgba(236, 72, 153, 0.8) 100%
      )`,
    },
    body: {
      padding: 24,
    },
    header: {
      background: "linear-gradient(to right, #6366f1, #a855f7)",
      color: "#fff",
      borderBottom: "none",
    },
    footer: {
      borderTop: "1px solid #e5e7eb",
      textAlign: "center",
    },
  };
  // 共享配置
  const sharedProps: ModalProps = {
    centered: true,
    classNames,
  };
  return (
    <div className="w-full h-[100vh] overflow-auto p-[24px] space-y-5">
      <Card
        title="Ant Design 6 模态框样式示例"
        bordered={false}
        extra={
          <Text type="secondary" className="text-sm">
            朦胧美学 + 渐变背景,高级感拉满!
          </Text>
        }
      >
        <Flex
          gap="middle"
          align="center"
          justify="center"
          style={{ padding: 40, minHeight: 300 }}
        >
          <Button
            type="primary"
            size="large"
            onClick={() => setBlurModalOpen(true)}
          >
            🌫️ 背景玻璃模糊效果
          </Button>
          <Button size="large" onClick={() => setGradientModalOpen(true)}>
            🎨 渐变色背景模态框
          </Button>
          {/* 模态框 1:背景玻璃模糊效果(朦胧美学) */}
          <Modal
            {...sharedProps}
            title="背景玻璃模糊效果"
            styles={blurModalStyles}
            open={blurModalOpen}
            onOk={() => setBlurModalOpen(false)}
            onCancel={() => setBlurModalOpen(false)}
            okText="太美了"
            cancelText="关闭"
            mask={{ enabled: true, blur: true }}
            width={600}
          >
            {sharedContent}
            <div
              style={{
                marginTop: 16,
                padding: 16,
                background: "rgba(255, 255, 255, 0.6)",
                borderRadius: 8,
                backdropFilter: "blur(10px)",
              }}
            >
              <Text type="secondary">
                <strong>💡 设计亮点:</strong>
                启用了 mask=&#123;&#123; blur: true &#125;&#125;,
                背景会自动应用模糊效果,营造出朦胧美学的高级质感。
              </Text>
            </div>
          </Modal>
          {/* 模态框 2:渐变色背景(无模糊效果) */}
          <Modal
            {...sharedProps}
            title="渐变色背景模态框"
            styles={gradientModalStyles}
            open={gradientModalOpen}
            onOk={() => setGradientModalOpen(false)}
            onCancel={() => setGradientModalOpen(false)}
            okText="好看"
            cancelText="关闭"
            mask={{ enabled: true, blur: false }}
            width={600}
          >
            {sharedContent}
            <div
              style={{
                marginTop: 16,
                padding: 16,
                background: "linear-gradient(135deg, #fef3c7 0%, #fce7f3 100%)",
                borderRadius: 8,
                border: "1px solid rgba(168, 85, 247, 0.2)",
              }}
            >
              <Text type="secondary">
                <strong>🎨 设计亮点:</strong>
                通过 styles.mask 设置渐变背景色,同时 styles.header
                应用了渐变头部,打造独特的视觉体验。
              </Text>
            </div>
          </Modal>
        </Flex>
        <div className="mt-6 p-5 bg-gradient-to-r from-blue-50 to-purple-50 rounded-xl border border-purple-200">
          <Title level={5} className="mb-3">
            📚 技术要点
          </Title>
          <Space direction="vertical" size="small" className="w-full">
            <Text>
              • <strong>玻璃模糊:</strong>使用 mask=&#123;&#123; blur: true &#125;&#125; 启用原生模糊效果
            </Text>
            <Text>
              • <strong>渐变背景:</strong>通过 styles.mask.backgroundImage 设置渐变色
            </Text>
            <Text>
              • <strong>语义化定制:</strong>使用 styles.header/body/footer 精准控制各部分样式
            </Text>
            <Text>
              • <strong>antd-style 集成:</strong>使用 createStyles 定义可复用的样式类名
            </Text>
          </Space>
        </div>
      </Card>
    </div>
  );
};

核心特性解析

1. 背景模糊开关

通过 mask 属性的 blur 配置项,可以一键开启/关闭背景模糊效果:

  • mask={{ enabled: true, blur: true }}:启用毛玻璃效果
  • mask={{ enabled: true, blur: false }}:使用传统半透明遮罩

2. 语义化样式定制

styles 属性允许精准控制组件各个部分的样式,无需编写复杂的 CSS 选择器:

  • styles.mask:遮罩层样式(可设置渐变背景)
  • styles.header:头部样式(可定制颜色、边框)
  • styles.body:内容区样式(可调整间距)
  • styles.footer:底部样式(可设置对齐方式)

3. antd-style 集成

结合 antd-style 可以创建主题感知的样式:

  • 访问 Design Token(如 token.borderRadiusLG)
  • 样式自动响应主题切换(亮色/暗色模式)
  • 通过 classNames 属性应用 CSS 类名

视觉效果对比

使用背景模糊和语义化样式定制后,Modal 的视觉呈现发生了显著变化:

1. 背景模糊效果: 遮罩层从单调的半透明黑色变为毛玻璃效果,背景内容呈现柔和的模糊感

2. 精准样式控制: 通过 styles.mask/header/body/footer 可以像搭积木一样组装出品牌化的对话框

3. 主题联动: 结合 antd-style 后,样式会自动响应全局主题切换,无需手动维护暗色模式样式

4. 维护性提升: 告别 .ant-modal .ant-modal-content .ant-modal-header 这样的深层选择器,样式意图清晰明确

这种设计让 Ant Design 6.0 的组件定制从"CSS 覆盖战争"升级为"API 声明式配置",显著降低了样式维护成本,同时保持了高度的灵活性。

七、Card 赛博朋克风格:霓虹科技美学的呈现

在传统的企业级应用中,卡片组件往往采用简洁素雅的设计。但对于游戏、科技、创意类产品,开发者往往需要更具视觉冲击力的效果。Ant Design 6.0 的 Card 组件配合 antd-style,可以轻松实现赛博朋克风格的霓虹发光边框、深色内阴影和动态动画效果,让你的界面充满未来感和科技感。

赛博朋克卡片实现

以下示例展示了如何使用 antd-style 的 CSS-in-JS 能力,为 Card 组件打造完整的赛博朋克视觉风格:

import { Card, Typography, Button, Space, Avatar, Row, Col } from "antd";
import { createStyles } from "antd-style";
import {
  ThunderboltOutlined,
  RocketOutlined,
  FireOutlined,
  StarOutlined,
  TrophyOutlined,
} from "@ant-design/icons";
const { Title, Text, Paragraph } = Typography;
// 使用 antd-style 创建赛博朋克风格样式
const useStyles = createStyles(({ css }) => ({
  // 赛博朋克卡片 - 紫色霓虹
  cyberpunkCard: css`
    background: rgba(1515350.9);
    border2px solid #a855f7;
    border-radius16px;
    overflow: hidden;
    position: relative;
    transition: all 0.3s ease;
    /* 发光边框效果 */
    box-shadow0 0 20px rgba(168852470.5),
      inset 0 0 20px rgba(168852470.1);
    &:hover {
      transformtranslateY(-5px);
      box-shadow0 0 30px rgba(168852470.8),
        inset 0 0 30px rgba(168852470.2);
      border-color#c084fc;
    }
    /* 顶部霓虹灯条 */
    &::before {
      content"";
      position: absolute;
      top0;
      left0;
      right0;
      height3px;
      backgroundlinear-gradient(
        90deg,
        transparent,
        #a855f7,
        #c084fc,
        #a855f7,
        transparent
      );
      animation: neonFlow 3s ease-in-out infinite;
    }
    @keyframes neonFlow {
      0%100% { opacity1; }
      50% { opacity0.5; }
    }
  `,
  // 霓虹文字
  neonText: css`
    color: #fff;
    text-shadow0 0 10px currentColor,
                 0 0 20px currentColor,
                 0 0 30px currentColor;
    font-weight: bold;
  `,
  // 霓虹按钮
  neonButton: css`
    background: transparent !important;
    border2px solid currentColor !important;
    color: inherit !important;
    text-shadow0 0 10px currentColor;
    box-shadow0 0 10px currentColor,
                inset 0 0 10px rgba(2552552550.1);
    transition: all 0.3s ease !important;
    &:hover {
      transformscale(1.05);
      box-shadow0 0 20px currentColor,
                  inset 0 0 20px rgba(2552552550.2!important;
    }
  `,
  // 数据面板
  dataPanel: css`
    background: rgba(0000.3);
    border1px solid rgba(2552552550.1);
    border-radius8px;
    padding16px;
    backdrop-filterblur(10px);
  `,
}));
export default () => {
  const { styles } = useStyles();
  return (
    <Row gutter={[24, 24]}>
      <Col span={8}>
        <Card
          className={styles.cyberpunkCard}
          hoverable
          styles={{ body: { padding24 } }}
        >
          <div style={{ position"relative", zIndex: 1 }}>
            {/* 头部 */}
            <div style={{ display"flex", alignItems: "center", marginBottom: 16 }}>
              <Avatar
                size={64}
                icon={<ThunderboltOutlined />}
                style={{
                  background: "linear-gradient(135deg, #a855f7, transparent)",
                  border: "2px solid #a855f7",
                  color: "#a855f7",
                  filter: "drop-shadow(0 0 10px #a855f7)",
                }}
              />
            </div>
            {/* 标题 */}
            <Title level={4} className={styles.neonText} style={{ color"#a855f7" }}>
              QUANTUM PROCESSOR
            </Title>
            <Text style={{ color"#888", display: "block", marginBottom: 16 }}>
              量子处理器
            </Text>
            {/* 描述 */}
            <Paragraph style={{ color"#bbb", marginBottom: 20 }}>
              第九代量子处理器,采用纳米级光刻技术,配备AI神经网络加速引擎。
            </Paragraph>
            {/* 数据面板 */}
            <div className={styles.dataPanel} style={{ marginBottom: 20 }}>
              <Space direction="vertical" style={{ width"100%" }} size={12}>
                <div style={{ display"flex", justifyContent: "space-between" }}>
                  <Text style={{ color"#888" }}>处理速度</Text>
                  <Text strong style={{ color"#a855f7" }}>5.2 PHz</Text>
                </div>
                <div style={{ display"flex", justifyContent: "space-between" }}>
                  <Text style={{ color"#888" }}>核心数量</Text>
                  <Text strong style={{ color"#a855f7" }}>128 核</Text>
                </div>
              </Space>
            </div>
            {/* 能量条 */}
            <div style={{ marginBottom: 20 }}>
              <div style={{ display"flex", justifyContent: "space-between", marginBottom: 8 }}>
                <Text style={{ color"#888", fontSize: 12 }}>POWER LEVEL</Text>
                <Text strong style={{ color"#a855f7", fontSize: 12 }}>9999</Text>
              </div>
              <div style={{
                height6,
                background: "rgba(0, 0, 0, 0.3)",
                borderRadius: 3,
                overflow: "hidden",
                border: "1px solid rgba(255, 255, 255, 0.1)",
              }}>
                <div style={{
                  height"100%",
                  width: "92%",
                  background: "linear-gradient(90deg, #a855f7, transparent)",
                  boxShadow: "0 0 10px #a855f7",
                }} />
              </div>
            </div>
            {/* 操作按钮 */}
            <Space style={{ width"100%" }}>
              <Button
                type="primary"
                className={styles.neonButton}
                style={{ color"#a855f7", flex: 1 }}
                icon={<StarOutlined />}
              >
                激活
              </Button>
              <Button
                className={styles.neonButton}
                style={{ color"#a855f7" }}
                icon={<TrophyOutlined />}
              >
                详情
              </Button>
            </Space>
          </div>
        </Card>
      </Col>
    </Row>
  );
};

6.gif

核心技术要点

1. 霓虹发光边框

通过多层 box-shadow 实现外发光和内阴影的叠加效果:

box-shadow: 
  0 0 20px rgba(168852470.5),        /* 外发光 */
  inset 0 0 20px rgba(168852470.1);  /* 内阴影 */

2. 动态霓虹灯条

使用伪元素和渐变动画创建流动的霓虹灯效果:

&::before {
  content"";
  backgroundlinear-gradient(90deg, transparent, #a855f7, transparent);
  animation: neonFlow 3s ease-in-out infinite;
}

3. 霓虹文字效果

通过 text-shadow 的多层叠加模拟霓虹灯文字:

text-shadow: 
  0 0 10px currentColor,
  0 0 20px currentColor,
  0 0 30px currentColor;

4. 毛玻璃数据面板

结合半透明背景和 backdrop-filter 实现毛玻璃效果:

background: rgba(0000.3);
backdrop-filter: blur(10px);

5. 交互动画

hover 时同步触发多个动画效果:

  • 卡片上浮:transform: translateY(-5px)
  • 发光增强:box-shadow 强度提升
  • 边框颜色变化:border-color 过渡

样式定制优势

使用 Ant Design 6.0 + antd-style 实现赛博朋克风格的优势:

1. CSS-in-JS 强大能力: 支持嵌套、伪元素、动画等高级特性,无需额外 CSS 文件

2. 类型安全: TypeScript 提供完整的类型提示,减少样式错误

3. 动态主题: 可以轻松切换不同颜色的霓虹主题(紫色、青色、粉色等)

4. 组件封装: 样式与组件逻辑共存,便于复用和维护

5. 性能优化: antd-style 自动处理样式注入和缓存,性能优秀

这种设计风格通过强烈的视觉冲击力和独特的科技感,能够有效吸引用户注意力,提升品牌记忆度,特别适合面向年轻用户群体的产品。

八、升级建议与实践策略

对于考虑升级到 Ant Design 6.0 的团队,建议采取以下策略:

1.渐进式升级路径

  1. 新项目直接使用:全新项目建议直接使用 6.0 版本,享受所有新特性

  2. 现有项目评估:评估项目依赖和定制程度,制定分阶段升级计划

  3. 组件逐步替换:可以先替换使用新功能的组件,再逐步迁移其他部分

2.兼容性注意事项

  1. 检查废弃 API:Ant Design 6.0 移除了之前版本已标记为废弃的 API

  2. 样式覆盖检查:如果项目中有深度定制样式,需要检查与新版本的兼容性

  3. 测试核心流程:升级后重点测试表单提交、数据展示等核心用户流程

九、总结

Ant Design 6.0 的组件功能更新聚焦于解决实际开发中的痛点,通过引入 Masonry 瀑布流布局、Tooltip 平滑移动、InputNumber 拨轮模式、Drawer 拖拽调整、Modal 背景模糊以及 Card 深度定制等特性,显著提升了开发效率和用户体验。

这些更新体现了现代前端设计的几个核心趋势:

1. 交互流畅性: 如 Tooltip 的平滑过渡,减少界面跳跃感

2. 设备适配性: 如 InputNumber 的触摸友好设计

3. 布局灵活性: 如 Masonry 的动态布局和 Drawer 的可调整尺寸

4. 视觉现代化: 如 Modal 的背景模糊效果,营造朦胧美学的高级质感

5. 样式可控性: 通过 classNamesstyles API 实现精准的组件定制

6. 风格多样性: 结合 antd-style 可实现从企业风到赛博朋克等多样化视觉风格

特别是与 antd-style 的深度集成,让开发者能够充分发挥 CSS-in-JS 的强大能力,从简洁的企业级设计到炫酷的赛博朋克风格,都能轻松实现。这些改进让 Ant Design 6.0 不仅保持了企业级应用的稳定性和专业性,还增加了更多现代化、人性化的交互细节和视觉创意空间,是构建下一代 Web 应用的理想选择。

往期回顾

1.Java 设计模式:原理、框架应用与实战全解析|得物技术

2.Go语言在高并发高可用系统中的实践与解决方案|得物技术 

3.从0到1搭建一个智能分析OBS埋点数据的AI Agent|得物技术

4.数据库AI方向探索-MCP原理解析&DB方向实战|得物技术

5.项目性能优化实践:深入FMP算法原理探索|得物技术

文 /三七

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

Next.js第十五章(Image)

作者 小满zs
2025年12月22日 10:17

Image组件

该组件是Next.js内置的图片组件,是基于原生img标签进行扩展,并不代表原生img标签不能使用。

  • 尺寸优化:支持使用现代化图片格式,如webpavifapng等,并自动根据设备提供正确的尺寸。
  • 视觉稳定性:防止图片加载时发生布局偏移,具体参考CLS
  • 懒加载:在图片进入视口才会加载,使用浏览器原生懒加载,并可选择添加模糊显示占位符。
  • 灵活性:可按需调整图像大小,即使是存储在远程服务器上的图像也可以调整。

图片引入

1. src本地图片引入

Next.js建议我们把图片放在根目录下的public文件夹中,然后使用/开头访问。

public.png

import Image from "next/image"
export default function Home() {
    return (
        <div>
            <h1>Home</h1>
            <Image
                src="/1.png"
                width={100}
                height={100}
                alt="1"
            />
        </div>
    )
}
2. import静态引入

使用import引入图片,是不需要填写宽度和高度,Next.js会自动确定图片的尺寸。

{
    "compilerOptions": {
        "paths": {
            "@/*": ["./src/*"],
            "@/public/*": ["./public/*"] // 新增这一行代码,配置图片路径。
        }
    }
}

使用静态import引入图片,你会发现无需填写宽度和高度,Next.js会自动确定图片的尺寸。

import Image from "next/image"
import test from '@/public/1.png'
export default function Home() {
    return (
        <div>
            <h1>Home</h1>
            <Image
                src={test}
                alt="1"
            />
        </div>
    )
}
3. 远程图片引入
import Image from "next/image"
export default async function Home() {
    const len = 20;
    return (
        <div>
            <h1>Home</h1>
            {Array.from({ length: len }).map((_, index) => (
                <Image
                    key={index}
                    src={`https://eo-img.521799.xyz/i/pc/img${index + 1}.webp`}
                    alt="1"
                    width={192}
                    height={108}
                />
            ))}
        </div>
    )
}

当我们直接使用远程图片引入的时候Next.js会报错,因为Next.js默认只允许加载本地图片,如果需要加载远程图片,需要配置next.config.js文件。

image-loader.ts:86 Uncaught Error: Invalid src prop (eo-img.521799.xyz/i/pc/img1.w…) on next/image, hostname "eo-img.521799.xyz" is not configured under images in your next.config.js

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  images: {
    remotePatterns: [
      {
        protocol: 'https', // 协议
        hostname: 'eo-img.521799.xyz', // 主机名
        pathname: '/i/pc/**', // 路径
        port: '', // 端口
      },
    ],
  },
};
4. LCP警告

如果图片是首屏或者LCP图片,需要添加loading="eager"属性,否则会触发LCP警告,因为Image组件默认是懒加载的。

  • lazy: 懒加载,默认值,在图片进入视口才会加载。
  • eager: 立即加载,在图片进入视口就会加载。

Image with src "eo-img.521799.xyz/i/pc/img1.w…" was detected as the Largest Contentful Paint (LCP). Please add the loading="eager" property if this image is above the fold. Read more: nextjs.org/docs/app/ap…

import Image from "next/image"
export default async function Home() {
    const len = 20
    return (
        <div>
            <h1>Home</h1>
            {Array.from({ length: len }).map((_, index) => (
                <Image
                    key={index}
                    src={`https://eo-img.521799.xyz/i/pc/img${index + 1}.webp`}
                    alt="1"
                    width={192}
                    height={108}
                    loading="eager" // 立即加载
                />
            ))}
        </div>
    )
}

第二种解决方案使用preload属性加载图片,表示提前预加载图片,不过Next.js还是更加推荐使用loading="eager"属性加载图片。

import Image from "next/image"
export default async function Home() {
    const len = 20
    return (
        <div>
            <h1>Home</h1>
            {Array.from({ length: len }).map((_, index) => (
                <Image
                    key={index}
                    src={`https://eo-img.521799.xyz/i/pc/img${index + 1}.webp`}
                    alt="1"
                    width={192}
                    height={108}
                    preload={index < 10} // 优先加载策略
                />
            ))}
        </div>
    )
}
5. 图片格式优化

Next.js 会通过请求Accept头自动检测浏览器支持的图像格式,以确定最佳输出格式

Accept:image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8

我们可以同时启用 AVIF 和 WebP 格式。对于支持 AVIF 的浏览器,系统将优先使用 AVIF 格式,WebP 格式作为备选方案。目前AVIF格式最优。

const nextConfig: NextConfig = {
  /* config options here */
  images: {
    formats: ['image/avif', 'image/webp'], //默认是 ['image/webp']
  },
};
6. 设备适配

如果你的老板告诉你要兼容哪些设备,你可以使用deviceSizesimageSizes属性来配置。

const nextConfig: NextConfig = {
  /* config options here */
  images: {
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], // 设备尺寸
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // 图片尺寸
  },
};

这两个属性结合会最终生成一个srcset属性,用于浏览器选择最佳图片。

srcset.png

那为什么需要两个数组去实现呢?

我们观察上图可以发现,imageSizes用于生成小图片尺寸例如(缩略图,头像等),而deviceSizes用于生成大图片尺寸例如(横幅图、背景图、全屏展示图)。

import Image from "next/image"

// 头像 - 固定 64px
export function Avatar() {
  return (
    <Image
      src="/avatar.jpg"
      width={64}
      height={64}
      alt="用户头像"
      sizes="64px"  // ← 告诉浏览器这张图只需要 64px
    />
  )
}

// 横幅图 - 响应式全宽
export function Banner() {
  return (
    <Image
      src="/banner.jpg"
      width={1920}
      height={600}
      alt="横幅"
      sizes="100vw"  // ← 占满整个视口宽度使用 deviceSizes
    />
  )
}

// 响应式内容图
export function ContentImage() {
  return (
    <Image
      src="/content.jpg"
      width={1200}
      height={800}
      alt="内容图"
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 1200px"
      // ↑ 手机上 100% 宽度平板上 50%,桌面最大 1200px
    />
  )
}

Props

以下是 Image 组件可用的属性:

必需属性
属性 类型 示例 说明
src String src="/profile.png" 图片源路径,支持本地路径或远程 URL
alt String alt="Picture of the author" 图片替代文本,用于无障碍访问和 SEO
尺寸相关
属性 类型 示例 说明
width Integer (px) width={500} 图片宽度,静态导入时可选
height Integer (px) height={500} 图片高度,静态导入时可选
fill Boolean fill={true} 填充父容器,替代 width 和 height
sizes String sizes="(max-width: 768px) 100vw" 响应式图片尺寸
优化相关
属性 类型 示例 说明
quality Integer (1-100) quality={80} 图片压缩质量,默认为 75
loader Function loader={imageLoader} 自定义图片加载器函数
unoptimized Boolean unoptimized={true} 禁用图片优化,使用原图
加载相关
属性 类型 示例 说明
loading String loading="lazy" 加载策略,"lazy" 或 "eager"
preload Boolean preload={true} 是否预加载,用于 LCP 元素
placeholder String placeholder="blur" 占位符类型,"blur" 或 "empty"
blurDataURL String blurDataURL="data:image/jpeg..." 模糊占位符的 Data URL
事件回调
属性 类型 示例 说明
onLoad Function onLoad={e => done()} 图片加载完成时的回调
onError Function onError={e => fail()} 图片加载失败时的回调
其他属性
属性 类型 示例 说明
style Object style={{objectFit: "contain"}} 内联样式对象
overrideSrc String overrideSrc="/seo.png" 覆盖 src,用于 SEO 优化
decoding String decoding="async" 解码方式,"async"/"sync"/"auto"
❌
❌