做毕业设计、课设或者企业内部的小型知识管理模块,这几年我越来越频繁地被问到同一个技术组合:SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0。这套东西不是哪个商业框架推出来的噱头,而是 Java Web 全栈开发里最“能打”的一套组合拳。最近正好拆了一套“多维分类知识管理系统”的源码,前后端齐全还带文档,我把它从头到尾过了一遍,这里把设计思路、实现细节、部署过程、以及我实际踩过的坑都整理出来。
这套系统解决的是典型的“知识库分类管理”问题:知识条目不止挂在一个分类下,而是需要按主题、部门、标签、适用场景等多个维度同时归类;后台需要一套可维护的分类树、可检索的条目列表、可管控的用户权限。说白了,就是给一堆零散的知识文档找一个结构化的“货架”,并且这个货架能从多个角度去索引同一件“货物”。适合正在选型毕设题目的学生,也适合中小团队想在项目里快速落地一套内容管理后台的开发者。全文不吹不黑,只讲这套代码里真正值得看的东西,以及那些文档里不会写但实操一定会遇到的细节。
1. 项目拆解:多维分类知识管理系统到底在解决什么问题
1.1 从标题拆需求:“多维分类”的多,到底多在哪里
很多人在看到“多维分类”这个词的时候,第一反应就是“分类树多级呗”。其实多级分类只是最外层的那一层皮。真正把需求理解透,你会发现“多维”说的是知识条目和分类之间的关系不是一条线,而是一张网。
举个例子。一篇关于“MySQL索引优化”的文档,按主题它应该归到“数据库性能优化”这个节点下;按部门它属于“后端研发组”的产出;按知识类型它又算“技术规范”;按适用场景它还适合“线上故障复盘”时参考。如果只做一棵分类树,你无论把它放到哪个节点都是错,因为它在业务上本来就有多个归属维度。
所以这套系统的核心设计,没有把分类做成简单的树形字段,而是抽象出了“维度”这个基础概念。每个维度下面各自维护一棵分类树,知识条目通过一张关系表和多个维度下的分类节点做关联。这样一条知识可以同时挂在技术主题、所属部门、知识类型、适用场景等多个维度下,检索时任意指定一个维度组合,都能把对应条目捞出来。
这种设计在毕设答辩时特别好讲,因为它不是一个拍脑袋的表结构,而是真实业务里的通用模型。做企业内部知识管理、内容中台、文档平台,几乎都是这套思路。理解了这一点,你再看后端代码里的表和前端页面里的分类器,就都顺了。
1.2 技术栈选型的逻辑:为什么偏偏是这套组合
先说 SpringBoot2。SpringBoot3 出来之后,确实有一波同学直接跳到 3.x,但在实际项目里,SpringBoot2 依然是存量系统和企业级项目里占比最大的版本。原因很朴素:稳定、资料多、第三方生态兼容性好。尤其做毕设和课设的人,遇到问题网上随便一搜就有答案,而 SpringBoot3 因为 Jakarta 命名空间切换到 Spring Security 6 的配置变化,很多老套路已经不好使了。这套系统选 SpringBoot2,不是落后,是在“能跑、能讲、能扩展”之间取平衡。
Vue3 就不用说了,组合式 API 加<script setup>之后,写后台管理系统的体验比 Options API 时代舒服得多。而且 Element Plus、Vue Router 4、Pinia 这些配套都已经非常成熟,2026 年再开新项目,Vue3 基本是默认答案。
MyBatis-Plus 的价值在于它把最琐碎的单表 CRUD 干掉了。你不需要为每张表手写一套insert/select/update/delete的 XML,继承一个BaseMapper就全都有了。但真正用好 MyBatis-Plus 的人知道,它的重点不是“少写代码”,而是条件构造器LambdaQueryWrapper和分页插件能让复杂的筛选和分页逻辑在 Service 层就很清晰地表达出来。这对后台管理系统这种“查询条件一屏、表格一行”的场景简直量身定做。
MySQL8.0 的选择更直接。窗口函数、递归 CTE、utf8mb4字符集这些都是刚需。分类树要递归查询、数据要支持 emoji、统计要窗口函数排序,8.0 一个版本全包圆。再加上现在启动 MySQL8.0 基本就是一条 Docker 命令的事,没有不上车的理由。
这套技术栈的组合稳定性,决定了源码里能踩的坑基本都被社区踩平了,你拿来做二次开发或者学习,成本是最低的。
2. MySQL8.0 数据库设计与后端核心实现
2.1 表结构设计:分类、知识条目、多维关系,三张表把模型立住
这套系统后端数据库设计的灵魂,我觉得是“分类维度”和“关系表”的处理方式。先看图,建表 SQL 大概是这个思路:
-- 分类表:通过 dimension 字段区分不同维度 CREATE TABLE `category` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `parent_id` BIGINT NOT NULL DEFAULT 0 COMMENT '父分类ID,0表示根节点', `dimension` VARCHAR(32) NOT NULL COMMENT '维度编码:theme/dept/type/scene', `name` VARCHAR(64) NOT NULL COMMENT '分类名称', `sort_no` INT NOT NULL DEFAULT 0 COMMENT '同级排序号', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '0停用 1启用', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_dimension_parent` (`dimension`, `parent_id`) ) COMMENT='多维分类表';-- 知识条目表 CREATE TABLE `knowledge` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `title` VARCHAR(255) NOT NULL COMMENT '标题', `summary` VARCHAR(500) DEFAULT NULL COMMENT '摘要', `content` LONGTEXT COMMENT '正文内容', `author_id` BIGINT DEFAULT NULL COMMENT '发布人', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1已发布 2下架', `view_count` INT NOT NULL DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) COMMENT='知识条目表';-- 知识条目与分类节点的关联表 CREATE TABLE `knowledge_category` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `knowledge_id` BIGINT NOT NULL, `category_id` BIGINT NOT NULL, `dimension` VARCHAR(32) NOT NULL COMMENT '冗余维度,方便按维度筛选', PRIMARY KEY (`id`), UNIQUE KEY `uk_kc` (`knowledge_id`, `category_id`), KEY `idx_category` (`category_id`) ) COMMENT='知识-分类关联表';这里有几个设计的“为什么”值得细说。
为什么不给每个维度单独建一张关联表?比如建knowledge_theme、knowledge_dept?因为业务上维度的种类可能随时增加,每加一个维度就改一次表结构,代码里还要跟着加一套 Mapper,维护成本很高。用dimension字段区分,加维度只需要往category表里插记录,代码一行不用改。
为什么关联表上要冗余dimension字段?因为按维度筛选分类时,如果dimension不在关联表上,你要先把分类 ID 范围查出来再关联,SQL 多一层嵌套;冗余之后,直接WHERE dimension = 'theme' AND category_id IN (...),一条 SQL 搞定,索引也能用上。
还有个细节,布尔字段我用TINYINT而不是BOOLEAN,时间字段统一DATETIME而不是TIMESTAMP,主要是为了避开 MySQL 8.0 里TIMESTAMP的 2038 年问题和时区转换的隐含坑。字符集在建库时直接指定utf8mb4,utf8mb4_unicode_ci排序规则,不然存 emoji 会直接报错。这种地方看着小,真到线上才知道疼。
2.2 分类树查询:MySQL8.0 递归 CTE 的实战用法
分类树最怕的就是“查询一个节点要带出整棵子树”。传统做法是先在内存里把全表捞出来组装成树,再过滤,数据量一大就慢。MySQL8.0 的递归 CTE 正好解决这个问题。
-- 查询某个分类节点下的所有子节点ID WITH RECURSIVE category_tree AS ( SELECT id, parent_id, name, dimension FROM category WHERE id = #{rootId} UNION ALL SELECT c.id, c.parent_id, c.name, c.dimension FROM category c INNER JOIN category_tree t ON c.parent_id = t.id ) SELECT * FROM category_tree;这段 SQL 的执行逻辑非常直观:先查根节点,再不断用UNION ALL把“父节点在临时结果集里”的子节点捞进来,直到没有新行产生为止。项目里做“按分类筛选知识条目”时,就是先用递归 CTE 把选中分类的所有子孙 ID 查出来,再结合关联表做匹配。对比老办法里写三层嵌套循环 Java 组装树,这个方案代码量少、性能稳定,而且在 MySQL8.0 里就是标准姿势。
实际写的时候要注意递归深度,MySQL 默认cte_max_recursion_depth = 1000,对后台管理系统的分类层级来说完全够用。但如果在递归里写了死循环的条件——比如根节点的parent_id指向了自己——会把数据库线程跑满。所以插入数据时对parent_id != id的条件做校验,这条我在源码里没看到强制校验,自己是加了的,建议你也加上。
2.3 MyBatis-Plus 落地:分页插件、条件构造器与自动填充
MyBatis-Plus 在这套系统里不是摆设,最值得看的是三个能力。
第一个是分页插件。不配插件的情况下,selectPage只会把所有数据查出来在内存里做假分页,数据一多整个后端就废了。正确配置:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }加上这个配置,MyBatis-Plus 才会在走BaseMapper.selectPage时自动生成LIMIT ?和COUNT(*)的优化 SQL。分页参数建议统一放在一个PageQuery基类里,子类继承后加筛选字段,避免每个接口都重复写current和size的接收逻辑。
第二个是条件构造器。后台管理系统查询条件多,直接用LambdaQueryWrapper写非常清爽:
LambdaQueryWrapper<Knowledge> wrapper = Wrappers.lambdaQuery(); wrapper.eq(Knowledge::getStatus, status) .like(StringUtils.hasText(keyword), Knowledge::getTitle, keyword) .between(startTime != null && endTime != null, Knowledge::getCreateTime, startTime, endTime) .orderByDesc(Knowledge::getCreateTime); IPage<Knowledge> page = knowledgeMapper.selectPage(pageParam, wrapper);like第一个参数传布尔条件,前端没传keyword时整个条件自动跳过,代码比if-else拼 SQL 干净得多。
第三个是自动填充。创建时间和更新时间如果用代码层手动set,每个 Service 都要写一遍,漏一个就是 null。MyBatis-Plus 的做法是定义一个MetaObjectHandler:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }配合实体类字段上加@TableField(fill = FieldFill.INSERT)和@TableField(fill = FieldFill.INSERT_UPDATE),数据库层、应用层、甚至假如有第三套,都不会出现时间字段不一致。
2.4 Service/Controller 层的分层与统一返回体
后端分层这块,这套系统的写法是标准的 Controller-Service-Mapper 三层。不过有一个很值得强调的点:接口返回体一定要统一。我自己接手过太多前后端联调时因为返回结构不统一吵起来的项目,这套源码在统一返回体上做得挺规范:
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }Controller 层只管接收参数、调用 Service、把 Result 抛回去,业务判断全部下沉。Service 层做事务管理:比如新增知识条目时要同时把多个维度的分类关联关系写进knowledge_category,这个操作必须加@Transactional,否则写到一半报错,关联数据就错乱了。写 Controller 的人最容易犯的毛病是业务逻辑堆在 Controller 里,这套代码把这层控制得不错,拆系统的时候至少不用为了“找个方法”翻半层代码。
3. Vue3 后台管理系统前端实现:从初始化到功能落地
3.1 Vite + Vue3 初始化:2026 年开发 Vue3 项目的标准起手式
现在创建 Vue3 项目,社区的主流玩法已经非常固定:Vite 做构建工具,<script setup>写组合式逻辑,TypeScript 可选但推荐。初始化命令:
npm create vite@latest knowledge-admin -- --template vue模板选择上,如果不熟悉 TS 语法,先选vue的纯 JS 模板也能跑,但如果后面想接若依那类的模板或者让代码更稳,建议直接vue-ts。我个人的习惯是:小项目纯 JS 快速出活,中大型项目上 TS。不过这里要说一句,Vue3 生态对 TS 的支持已经非常成熟,哪怕之前没写过 TS,跟着项目写两周也就会了。
装依赖:
npm install npm install vue-router@4 pinia element-plus @element-plus/icons-vue axios sassElement Plus 是当前 Vue3 后台管理系统事实上的组件库标准。表格、树、表单、对话框这些后台高频组件它都有,主题通过 CSS 变量覆盖,改起来也不费劲。
装完跑npm run dev,Vite 的默认端口是 5173,如果你后端接口文档写的是 8080,一眼就能看出前后端是分离的。这和你以前用 JSP 把前后端揉在一起完全不同,理解“分离”是看明白这套源码的第一步。
3.2 工程目录设计:把后台管理系统的通用骨架搭出来
目录结构我盘了一下,这类 Vue3 后台管理系统基本长一个样:
src/ ├── api/ # 接口请求层 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── layout/ # 后台布局:侧边栏、顶部栏、内容区 ├── router/ # 路由配置 ├── stores/ # Pinia 状态管理 ├── styles/ # 全局样式 ├── utils/ # 工具函数 ├── views/ # 页面组件 │ ├── dashboard/ │ ├── knowledge/ │ └── category/ ├── App.vue ├── main.js这个骨架的价值在于,不管换什么业务,后台管理系统都能往这个结构里塞。views按业务模块分包,components只放跨页面复用的,api层把每个模块的请求收拢,页面组件里最好不要直接写axios调用,不然工具类一旦要统一加请求头,你就得改十几个文件。
布局这块,建议先画一个Layout.vue,用 Element Plus 的el-container拼出左边的菜单栏和右边的主内容区,再用 Vue Router 的<router-view>渲染子路由。动态菜单如果要做,核心是根据后端返回的菜单数据生成路由,但那是进阶玩法。这套系统的菜单可以先做成前端的静态路由,先把知识管理的核心功能理顺,后面再考虑权限动态菜单。
3.3 核心页面实现:分类树、级联选择、富文本编辑
知识管理页面的核心交互有三个,都是 Vue3 + Element Plus 的高频用法。
第一个是分类树。左侧用el-tree展示当前维度的分类树,数据直接从后端拿,结构是带有children的嵌套数组,配合<script setup>里ref就行。点击节点时带上node.id请求接口,右侧列表按分类筛选。如果节点后面要显示条目数量,后端可以在返回树结构时把count字段带上,前端在自定义节点模板里渲染:
<el-tree :data="treeData" node-key="id" :props="{ label: 'name', children: 'children' }" @node-click="handleNodeClick"> <template #default="{ data }"> <span class="tree-node"> <span>{{ data.name }}</span> <span class="tree-node-count">{{ data.count }}</span> </span> </template> </el-tree>第二个是级联选择。由于知识条目要挂多个维度分类,新增页面里放几个el-cascader,每个级联器对应一个维度。需要注意,el-cascader的v-model绑定的默认是“路径数组”,比如[“技术主题”, “后端”, “数据库”],传给后端时要转成最终的叶子节点 ID,否则存储会出现混乱。这个转换逻辑建议放在前端提交前统一处理:
function getLeafIds(selection) { return selection.map(path => path[path.length - 1]); }第三个是富文本编辑。知识条目正文用content字段存 LONGTEXT,前端通常接一个富文本编辑器。组件库自带的富文本都比较基础,如果要 wangEditor 或 tinymce 这类独立的,要注意上传图片的接口和回显时样式scope带来的 CSS 隔离问题。后端存储富文本时要小心 XSS 注入,至少在上传接口对 HTML 做白名单过滤,不然某篇“知识文档”里藏段脚本是很容易的事。毕设阶段可能不会被攻击盯上,但养成习惯,以后工作了不用补课。
3.4 前后端 API 对接:Axios 封装与跨域代理
Vue3 项目里和 SpringBoot 后端通信,几乎都是 Axios。直接裸用 Axios 不是不行,但后台系统接口多了之后,日志、鉴权、错误提示全都要统一,于是封装一层是标配。这套源码里的做法值得抄:
// src/utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 15000 }) service.interceptors.request.use( config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, error => Promise.reject(error) ) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') if (res.code === 401) { router.push('/login') } return Promise.reject(new Error(res.message)) } return res }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default service拦截器里做了三件事:请求带上 Token、后端返回非 200 时统一提示、401 时跳登录。页面里调用的时候只需要关心业务数据,错误处理算后端不一致的问题全部被拦截器吃掉。
跨域问题,Vite 开发环境不用后端开 CORS,直接配代理省事又安全:
// vite.config.js server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }changeOrigin: true的作用是让后端看到的请求 HOST 变成localhost:8080,避免一些对 Host 做校验的框架拦截。生产环境要解决跨域,最干净的办法是让 Nginx 把/api反向代理到后端地址,后端不用动任何配置,这点我到后面部署章节再展开。
4. 环境准备、本地部署与数据初始化
4.1 用 Docker 快速拉起 MySQL8.0 并导入数据
拿到这套源码后,第一步不是打开 IDE,而是先把数据库跑起来。MySQL8.0 用 Docker 安装是最省心的姿势,两条命令就够了:
docker pull mysql:8.0 docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ -e TZ=Asia/Shanghai \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci几个参数依次说:-d后台运行,--name容器名,-p把容器内 3306 映射到宿主机 3306,MYSQL_ROOT_PASSWORD指定 root 密码,TZ设置时区为上海,后面两个参数让 MySQL 默认使用utf8mb4字符集。
容器起来以后,等十几秒让 MySQL 完成初始化,再建库导数据:
docker exec -it mysql8 mysql -uroot -proot123 -e "CREATE DATABASE knowledge_manage DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" docker exec -i mysql8 mysql -uroot -proot123 knowledge_manage < knowledge_manage.sql注意docker exec -i表示保持标准输入开放,这样才能把我们本地的 SQL 文件内容重定向进容器。
如果你不想用 Docker,直接在 Linux 上装 MySQL8.0 也不是不行。CentOS 系的命令是yum install mysql-server;Ubuntu/Debian 系是apt install mysql-server。装完之后要改 root 密码、确认监听地址、设置字符集,普通同学折腾下来一小时就没了,这也是我推荐 Docker 的原因——省下的时间不如去看代码。
4.2 后端配置:数据源、时区、MyBatis-Plus 日志
源码里application.yml的配置我建议逐行看一遍,现在把关键项挑出来说:
spring: datasource: url: jdbc:mysql://localhost:3306/knowledge_manage?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0两个最容易踩坑的参数我单独说。
serverTimezone=Asia/Shanghai是必须的,不然 Java 驱动和 MySQL 服务器的时区不一致,存进去的时间比北京时间差 8 小时,这辈子最先感受到“时区问题”基本就在这。allowPublicKeyRetrieval=true是为了解决 MySQL8.0 用户密码认证使用caching_sha2_password插件时,客户端首次连接无法获取公钥的问题。本地直连不配置可能连不上,很多人在这卡一下午。
log-impl配置成StdOutImpl后,控制台会打印每一条执行 SQL,排错时会比较方便;上线前记得删掉这段,不然日志文件膨胀速度很快。
4.3 前端构建与 Nginx 部署:生产环境这样玩
前后端分离的项目,生产环境最稳的部署方案是:前端构建出静态文件,Nginx 托管静态文件的同时,把/api请求反向代理到 SpringBoot 端口。
先构建前端:
npm run build生成dist/目录,里面有index.html和一堆带哈希的 JS/CSS 文件。把这目录上传到服务器的/usr/share/nginx/html下面,然后 Nginx 配置加一段:
server { listen 80; server_name your-domain.com; # 前端静态页面 root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html这行尤其重要:Vue Router 的history模式刷新/knowledge这种二级页面时,Nginx 会去找这个物理路径,找不到就 404,这行配置让它回退到index.html,由前端路由接管页面。如果你没有这行,部署后点击刷新报 404 是必然的,这也是前后端分离项目部署最常见的坑。
生产环境的跨域问题,到这里就被 Nginx 反向代理消化掉了,后端不需要@CrossOrigin,前端请求也还是写相对路径/api。前后端在同一域名下,浏览器不认为这是跨域。
5. 常见问题与排查实录
5.1 若依 Vue3 + TS 项目报错:类型错乱的解法套路
很多人的 Vue3 项目是从若依这类开源脚手架改出来的,改到一半就会被 TS 类型错误折磨。看到最多的报错是类似Property 'xxx' does not exist on type 'never'或者Cannot find module '@/api/xxx'。
先说结论:出现never类型报错,通常是变量声明时没有给初始值,或者ref()没传类型参数,TypeScript 把它推断成了“永远不可能的类型”。
// 错误写法 const form = ref([]) // 正确写法 const form = ref<FormData[]>([])Cannot find module八成是tsconfig.json里的路径别名@没配,或者 IDE 没有重启。若依的 Vue3 版本默认用vite,路径别名在vite.config.ts里用resolve.alias配一遍,tsconfig.json再配一遍baseUrl和paths:
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }记住这个套路,TS 报错大多不是语法难,而是“类型没有给对、目录没有配好”。把这两个源头理顺,新手期能少掉一半头发。
5.2 Vue3 样式覆盖:Tabs 标签页的高频修改玩法和浏览器兼容
做后台管理系统,Element Plus 的 Tabs 样式几乎都要定制。很多人直接用全局 CSS 写法,结果把其他页面的样式也影响了。在 Vue3<style scoped>下修改组件库内部样式,必须用:deep()选择器:
<style scoped> :deep(.el-tabs__item) { height: 36px; line-height: 36px; font-weight: 500; } :deep(.el-tabs__item.is-active) { color: #409eff; border-bottom: 2px solid #409eff; } :deep(.el-tabs__nav-wrap::after) { height: 1px; background-color: #e4e7ed; } </style>不加:deep(),Vue3 会给模板里的元素加一个>
Flutter在OpenHarmony上实现房间列表:从环境搭建到状态管理实战
房间列表这个功能,是我在做家具购买记录 App 时第一个从"单一页面"迈向"多房间数据管理"的转折点。OpenHarmony 设备上跑 Flutter,听着像是个折腾活儿,但实际走通之后,你会发现这套组合比想象中顺手。这篇文章…
文件包含与下载读取漏洞解析:原理、审计与防御
上个礼拜帮一家做了八年私有化系统的客户做代码审计,前后台加起来不到三十个接口,我却在小半天里接连定位了三类同源问题:文件包含、文件下载和文件读取漏洞。它们长得都很像——一个从 URL 或 POST 过来的文件名参数,被直接拼进了…
nvlddmkm事件0深度解析:GPU驱动TDR超时与稳定性调优指南
1. 项目概述:为什么“nvlddmkm事件0”成了游戏玩家的深夜噩梦“nvlddmkm事件0”——这串看似随机的字符组合,最近频繁出现在Windows事件查看器里,紧随其后的往往是《赛博朋克2077》突然黑屏、《艾尔登法环》卡死在加载界面、或者《绝地求生》…
Java Set接口全解析:HashSet、TreeSet选型与去重机制详解
1. Set 接口的定位:去重这件事,Java 替你封装好了先说个我自己的经历。几年前做数据清洗,需要从几十万行日志里筛出唯一的 IP 列表。第一版代码用了 List,每次判断contains都 O(n),数据量一上来直接卡死。后来换成了Ha…
文件夹一键批量导出:Better Export PDF 如何把 100 篇笔记快速变成 100 个 PDF
文件夹一键批量导出:Better Export PDF 如何把 100 篇笔记快速变成 100 个 PDF 【免费下载链接】obsidian-better-export-pdf Obsidian PDF export enhancement plugin 项目地址: https://gitcode.com/gh_mirrors/ob/obsidian-better-export-pdf 如果你正在用…
C++开发环境Docker化:从工具链到调试的完整实践
C 和 Docker 放在一起,很多人第一反应是"C 是编译型语言,容器不是为它准备的"。我一开始也这么想,直到一次跨平台交付被环境问题折磨到怀疑人生,才认真把整套工具链搬进了 Docker。这篇文章记录我实际搭建 C 与 Docker …