diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index fecb03b..a9d3007 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -12,6 +12,8 @@ import './style/var.css' import './style/vp-code.css' import './style/custom-block.css' import MyLayout from './components/MyLayout.vue'; +import VPSwiper from '@cssnr/vitepress-swiper' +import '@cssnr/vitepress-swiper/style.css' export default { extends: DefaultTheme, @@ -25,5 +27,6 @@ export default { app.component("StatsChart", StatsChart); app.component("MenuList", MenuList); app.component("ArticleMetadata", ArticleMetadata); + app.component("VPSwiper", VPSwiper); }, }; diff --git a/docs/.vitepress/theme/router/index.ts b/docs/.vitepress/theme/router/index.ts index 9d0c97f..2001a6f 100644 --- a/docs/.vitepress/theme/router/index.ts +++ b/docs/.vitepress/theme/router/index.ts @@ -23,16 +23,17 @@ export const routers = [ { text: 'Spring IOC简介', link: '/Web/SpringBoot/Spring-IOC' }, { text: 'Spring AOP简介', link: '/Web/SpringBoot/Spring-AOP' }, { text: 'Spring MVC简介', link: '/Web/SpringBoot/Spring-MVC' }, - { text: 'SpringBoot启动流程', link: '/Web/SpringBoot/SpringBoot-Start-Process' }, - { text: 'SpringBoot Starter原理', link: '/Web/SpringBoot/SpringBoot-Starter' }, - { text: 'SpringBoot Bean简介', link: '/Web/SpringBoot/SpringBoot-Bean' }, - { text: 'SpringBoot注解', link: '/Web/SpringBoot/SpringBoot-Annotation' }, + { text: '启动流程', link: '/Web/SpringBoot/SpringBoot-Start-Process' }, + { text: 'Starter原理', link: '/Web/SpringBoot/SpringBoot-Starter' }, + { text: 'Bean简介', link: '/Web/SpringBoot/SpringBoot-Bean' }, + { text: '注解', link: '/Web/SpringBoot/SpringBoot-Annotation' }, { text: 'SpringBoot3原生镜像', link: '/Web/SpringBoot/SpringBoot3-GraalVM' }, { text: 'SpringBoot技巧', link: '/Web/SpringBoot/SpringBoot-Skills' }, - { text: 'SpringBoot RestClient简介', link: '/Web/SpringBoot/SpringBoot-RestClient' }, - { text: 'SpringBoot Redis简介和使用', link: '/Web/SpringBoot/SpringBoot-Redis' }, - { text: 'SpringBoot WebSocket使用', link: '/Web/SpringBoot/SpringBoot-WebSocket' }, - { text: 'SpringBoot 线程与事件', link: '/Web/SpringBoot/SpringBoot-Thread' } + { text: 'RestClient简介', link: '/Web/SpringBoot/SpringBoot-RestClient' }, + { text: 'Redis简介和使用', link: '/Web/SpringBoot/SpringBoot-Redis' }, + { text: 'WebSocket简介和使用', link: '/Web/SpringBoot/SpringBoot-WebSocket' }, + { text: 'Quarzt简介和使用', link: '/Web/SpringBoot/SpringBoot-Quartz' }, + { text: '线程与事件', link: '/Web/SpringBoot/SpringBoot-Thread' } ] }, { @@ -116,7 +117,8 @@ export const routers = [ { text: '👤 个人项目', items: [ - { text: '家庭管理系统', link: '/Practice/Personal/SweetHut' } + { text: '家庭管理系统', link: '/Practice/Personal/SweetHut' }, + { text: '个人博客系统', link: '/Practice/Personal/Blog' } ] }, ] diff --git a/docs/.vitepress/theme/style/var.css b/docs/.vitepress/theme/style/var.css index 2901665..ca54d33 100644 --- a/docs/.vitepress/theme/style/var.css +++ b/docs/.vitepress/theme/style/var.css @@ -36,6 +36,10 @@ border-top: none; } +.vp-swiper-button { + color: #bd34fe; +} + strong { - color: #bd34fe; + color: #bd34fe; } \ No newline at end of file diff --git a/docs/Practice/Personal/Blog.md b/docs/Practice/Personal/Blog.md new file mode 100644 index 0000000..09c8f15 --- /dev/null +++ b/docs/Practice/Personal/Blog.md @@ -0,0 +1,88 @@ +--- +title: 个人博客系统 +date: 2026-06-08 +--- + +# 一、项目背景 +  为满足个人技术积累与内容分享需求,独立开发了一套多端博客系统,支持博客管理、发布与多端阅读。 +  系统包含博客网站、博客管理后台与移动端 App,覆盖 Web 端与移动端用户场景,实现了从内容创作、存储、检索到多端展示的完整闭环。 + +# 二、系统架构 + +# 三、核心模块 +## 3.1 博客网站 +1. 首页博客列表展示(分页、置顶) +2. 博客分类与标签筛选 +3. 博客归档(按时间维度) +4. 博客全文搜索(Elasticsearch) +5. Markdown 文章渲染与代码高亮 +6. 博客阅读时长和阅读人数统计 +7. 评论发布与互动(匿名) + + + +## 3.2 博客管理后台 +1. 博客新增、编辑、删除(权限控制) +2. Markdown 编辑器(md-editor-v3) +3. 博客图片上传(RustFS) +4. 分类 / 标签管理 +5. 博客状态管理(草稿 / 发布) +6. 博客访问数据查看 +7. 统计数据仪表盘(ECharts) + + + +## 3.3 移动端APP +1. 博客列表与详情浏览 +2. Markdown 内容原生渲染(markdown_widget) +3. 分类、归档、搜索功能 +4. 移动端自适应布局 + +# 四、技术栈 +| 层级 | 技术 | +|---|---| +| **前端** | Vue 3、Element Plus、Vue Router、Pinia、md-editor-v3、Axios | +| **后端** | FastAPI、Pydantic、SQLAlchemy、Uvicorn | +| **移动端** | Flutter、markdown_widget、provider、dio | +| **中间件** | Elasticsearch(全文检索、分词索引) | +| **存储** | MySQL 8.0(业务数据)、RustFS(图片存储) | +| **运维** | Docker、Nginx、Fluent Bit (日志采集)、OpenObserver (日志可视化) | + +# 五、核心设计 +- 基于 FastAPI​ 构建高性能后端 API,采用分层结构设计,保证业务逻辑清晰、接口响应高效。 +- 博客内容统一采用 Markdown 格式,Web 端与移动端分别基于 md-editor-v3与 markdown_widget实现一致的内容渲染效果。 +- 使用 Elasticsearch​ 构建博客全文检索引擎,支持标题、内容、标签多维搜索,显著提升内容查找效率。 +- 图片资源统一上传至 RustFS 对象存储,通过 URL 访问,降低应用服务器存储压力,提升静态资源加载速度。 +- 管理后台与博客前端共享同一后端服务,通过接口级权限控制,实现“管理操作需鉴权、公开访问无需登录”的安全设计。 +- Web 端采用 Vue 3 + Pinia​ 实现状态管理与组件化开发;移动端基于 Flutter​ 实现跨平台一致体验。 + +# 六、难点 / 亮点 +## 6.1 博客搜索性能优化 +- **问题**:随着博客数量增长,MySQL LIKE 查询性能下降明显。 +- **方案**:引入 Elasticsearch,构建博客索引,支持中文分词与多字段检索。 + +## 6.2 图片存储与访问优化 +- **问题**:本地存储图片不利于扩展,且增加服务器带宽压力。 +- **方案**:集成 RustFS 对象存储,所有博客图片统一上传并返回访问 URL。 diff --git a/docs/Practice/Personal/SweetHut.md b/docs/Practice/Personal/SweetHut.md index 04f5813..01f7ea4 100644 --- a/docs/Practice/Personal/SweetHut.md +++ b/docs/Practice/Personal/SweetHut.md @@ -7,7 +7,6 @@ date: 2026-06-04   针对家庭信息分散、难以统一管理的问题,设计并实现了一套移动端家庭管理平台,将账单、旅行、健康、纪念日等数据进行集中管理,并通过私有化部署与数据加密保障安全,实现家庭生活的数字化沉淀。 # 二、系统架构 -![架构图](../images/family_manage_arch.png) # 三、核心模块 - **账单记录** diff --git a/docs/Practice/images/blog_archive.png b/docs/Practice/images/blog_archive.png new file mode 100644 index 0000000..0de6f3f Binary files /dev/null and b/docs/Practice/images/blog_archive.png differ diff --git a/docs/Practice/images/blog_article.png b/docs/Practice/images/blog_article.png new file mode 100644 index 0000000..9cfba5f Binary files /dev/null and b/docs/Practice/images/blog_article.png differ diff --git a/docs/Practice/images/blog_category.png b/docs/Practice/images/blog_category.png new file mode 100644 index 0000000..e546522 Binary files /dev/null and b/docs/Practice/images/blog_category.png differ diff --git a/docs/Practice/images/blog_commit.png b/docs/Practice/images/blog_commit.png new file mode 100644 index 0000000..b4db4f2 Binary files /dev/null and b/docs/Practice/images/blog_commit.png differ diff --git a/docs/Practice/images/blog_content.png b/docs/Practice/images/blog_content.png new file mode 100644 index 0000000..2d31936 Binary files /dev/null and b/docs/Practice/images/blog_content.png differ diff --git a/docs/Practice/images/blog_edit.png b/docs/Practice/images/blog_edit.png new file mode 100644 index 0000000..57bb981 Binary files /dev/null and b/docs/Practice/images/blog_edit.png differ diff --git a/docs/Practice/images/blog_home.png b/docs/Practice/images/blog_home.png new file mode 100644 index 0000000..a86a0b0 Binary files /dev/null and b/docs/Practice/images/blog_home.png differ diff --git a/docs/Practice/images/blog_login.png b/docs/Practice/images/blog_login.png new file mode 100644 index 0000000..c788428 Binary files /dev/null and b/docs/Practice/images/blog_login.png differ diff --git a/docs/Practice/images/blog_search.png b/docs/Practice/images/blog_search.png new file mode 100644 index 0000000..90dd1d7 Binary files /dev/null and b/docs/Practice/images/blog_search.png differ diff --git a/docs/Practice/images/blog_stats.png b/docs/Practice/images/blog_stats.png new file mode 100644 index 0000000..6aaa566 Binary files /dev/null and b/docs/Practice/images/blog_stats.png differ diff --git a/docs/Web/SpringBoot/SpringBoot-Quartz.md b/docs/Web/SpringBoot/SpringBoot-Quartz.md new file mode 100644 index 0000000..7ee696c --- /dev/null +++ b/docs/Web/SpringBoot/SpringBoot-Quartz.md @@ -0,0 +1,120 @@ +--- + title: SpringBoot Quarzt简介和使用 + date: 2026-06-08 +--- + +# 一、基本概念 +  Quartz 是 Java 生态最主流的企业级作业调度框架,支持简单间隔触发、Cron 表达式、持久化到数据库、集群部署。 +- Job(任务)​ — 实现 execute()方法,写你要执行的业务逻辑 +- Trigger(触发器)​ — 定义"什么时候执行",常用 SimpleTrigger(固定间隔)和 CronTrigger(Cron 表达式) +- Scheduler(调度器)​ — 把 Job + Trigger 绑定,负责启动和停止调度 + +# 二、原理 +  Job相当于员工,只知道干活;Trigger 相当于排班表,决定什么时候干活;Scheduler 相当于老板,统一管理 Job 和 Trigger。 +  Quartz内部有一个核心线程 QuartzSchedulerThread,会不停的检查哪个员工需要在啥时候干活,但并不是按照固定间隔轮询的方式,而是计算出最近一次 Trigger 的触发时间,通过 wait()精确等待,当等待结束后,调度线程会唤醒线程池中的工作线程,执行到点的 Job。如果当前没有任何 Trigger了,默认最多等待 30 秒,然后再重新扫描​。如果此时有新增/修改/删除任务了,调度线程会被 notifyAll()立即唤醒,重新计算最近的触发时间。 + +# 三、安装 +```xml + + org.springframework.boot + spring-boot-starter-quartz + +``` + +  持久化: +```yml +spring: + quartz: + job-store-type: jdbc +``` + +  启用后会在数据库自动建表。 + +# 四、使用 +## 4.1 定义Job +```java +@Component +@DisallowConcurrentExecution // 防止同一 Job 并发执行 +public class QuartzJob extends QuartzJobBean { + @Override + protected void executeInternal(JobExecutionContext context) { + // 获取任务内容 + JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); + + // TODO 根据任务内容执行任务 + } +} +``` + +## 4.2 定义Trigger +```java +@Component +public class QuartzUtils { + public static JobDetail buildJobDetail(Task task, JobKey jobKey) { + JobDataMap jobDataMap = new JobDataMap(); + jobDataMap.put("id", task.getId()); + jobDataMap.put("type", task.getType()); + jobDataMap.put("content", task.getContent()); + + return JobBuilder.newJob(QuartzJob.class) + .withIdentity(jobKey) // 使用 id 作为唯一标识 + .usingJobData(jobDataMap) + .build(); + } + + public static Trigger buildTrigger(Task task, JobKey jobKey) { + Trigger trigger; + // 判断任务类型:Cron 任务或者一次性任务 + if (task.getCronExpression() != null && !task.getCronExpression().isEmpty()) { + // 定时任务:使用 Cron 表达式 + trigger = TriggerBuilder.newTrigger() + .withIdentity(jobKey.getName() + "Trigger") + .withSchedule(CronScheduleBuilder.cronSchedule(task.getCronExpression())) + .forJob(jobKey) + .build(); + } else if (task.getExecuteTime() != null) { + // 一次性任务:使用 executeTime + trigger = TriggerBuilder.newTrigger() + .withIdentity(jobKey.getName() + "Trigger") + .startAt(task.getExecuteTime()) + .forJob(jobKey) + .build(); + } else { + // 如果没有 Cron 表达式和 executeTime,任务不能调度,抛出异常 + throw new CustomException("任务没有有效的 Cron 表达式或执行时间"); + } + + return trigger; + } +} +``` + +::: tip +这里的Task可以根据实际情况定义: +```java +@Data +public class Task { + private Long id; + private String type; + private String content; + private Date executeTime; + private String cronExpression; +} +``` +::: + +## 4.3 执行Scheduler +```java +@Resource +private Scheduler scheduler; + +// 新增/更新 +JobKey jobKey = new JobKey(task.getId().toString()); +scheduler.scheduleJob(QuartzUtils.buildJobDetail(task, jobKey), QuartzUtils.buildTrigger(task, jobKey)); + +// 更新 +scheduler.rescheduleJob(QuartzUtils.buildJobDetail(task, jobKey), QuartzUtils.buildTrigger(task, jobKey)); + +// 删除 +scheduler.deleteJob(jobKey); +``` \ No newline at end of file diff --git a/docs/Web/SpringBoot/SpringBoot-WebSocket.md b/docs/Web/SpringBoot/SpringBoot-WebSocket.md index 10499f8..ccaa0df 100644 --- a/docs/Web/SpringBoot/SpringBoot-WebSocket.md +++ b/docs/Web/SpringBoot/SpringBoot-WebSocket.md @@ -1,5 +1,5 @@ --- - title: SpringBoot WebSocket使用 + title: SpringBoot WebSocket简介和使用 date: 2026-06-08 --- diff --git a/docs/Web/Vue/VueUse.md b/docs/Web/Vue/VueUse.md index 4123bc1..e7bc679 100644 --- a/docs/Web/Vue/VueUse.md +++ b/docs/Web/Vue/VueUse.md @@ -9,7 +9,26 @@ npm install @vueuse/core ``` # 二、使用 -## 2.1 useWebSocket +## 2.1 实时获取当前时间 +```ts + + + +``` + +## 2.2 useWebSocket ```ts -``` \ No newline at end of file +``` + +## 2.3 防抖和节流 +| | 防抖 Debounce | 节流 Throttle | +| :--- | :--- | :--- | +| **执行时机** | 连续触发 **停下后** | **每隔一段时间** | +| **执行次数** | 通常 1 次(最后) | 多次,但受限制 | +| **典型场景** | 搜索联想、校验 | 滚动、拖拽 | + +  防抖: +```vue + + + +``` + +  节流: +```vue + + + +``` + +::: warning +如果是表单提交,应该使用状态锁来限制,即当提交后,将按钮置为loading状态,提交成功后再重置。 +```vue + + + +``` +::: \ No newline at end of file diff --git a/package.json b/package.json index b3b39bd..7c737f6 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "deploy": "npm run build && node deploy.js" }, "dependencies": { + "@cssnr/vitepress-swiper": "^0.3.1", "echarts": "^6.0.0", "tippy.js": "^6.3.7", "viewerjs": "^1.11.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55eb7d0..4a05286 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@cssnr/vitepress-swiper': + specifier: ^0.3.1 + version: 0.3.1(swiper@12.2.0)(vue@3.5.25) echarts: specifier: ^6.0.0 version: 6.0.0 @@ -156,6 +159,12 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@cssnr/vitepress-swiper@0.3.1': + resolution: {integrity: sha512-RwX9S8I2lwJELSqfsVBwczLPmRTahrb6Pg5wDa6xI2d7ZZ3H39/SLnHf28Qis7BOyueZgExvnkpjOw47+p00iA==} + peerDependencies: + swiper: '>=11.0.0' + vue: ^3.0.0 + '@docsearch/css@3.8.2': resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} @@ -1156,6 +1165,10 @@ packages: resolution: {integrity: sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==} engines: {node: '>=16'} + swiper@12.2.0: + resolution: {integrity: sha512-K8uXsBZU6ME97Ia3xbBge8IRCnR1lOmIILzvY/jGVic7dSTQ530s3uO8RvXbPUtkkXLWIwmZLRPbtDxRWVAFdg==} + engines: {node: '>= 4.7.0'} + tabbable@6.3.0: resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==} @@ -1457,6 +1470,11 @@ snapshots: '@chevrotain/utils@11.0.3': {} + '@cssnr/vitepress-swiper@0.3.1(swiper@12.2.0)(vue@3.5.25)': + dependencies: + swiper: 12.2.0 + vue: 3.5.25 + '@docsearch/css@3.8.2': {} '@docsearch/js@3.8.2(@algolia/client-search@5.45.0)(search-insights@2.17.3)': @@ -2504,6 +2522,8 @@ snapshots: dependencies: copy-anything: 4.0.5 + swiper@12.2.0: {} + tabbable@6.3.0: {} tinyexec@1.0.2: {}