feat:增加个人博客系统文档
@@ -12,6 +12,8 @@ import './style/var.css'
|
|||||||
import './style/vp-code.css'
|
import './style/vp-code.css'
|
||||||
import './style/custom-block.css'
|
import './style/custom-block.css'
|
||||||
import MyLayout from './components/MyLayout.vue';
|
import MyLayout from './components/MyLayout.vue';
|
||||||
|
import VPSwiper from '@cssnr/vitepress-swiper'
|
||||||
|
import '@cssnr/vitepress-swiper/style.css'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: DefaultTheme,
|
extends: DefaultTheme,
|
||||||
@@ -25,5 +27,6 @@ export default {
|
|||||||
app.component("StatsChart", StatsChart);
|
app.component("StatsChart", StatsChart);
|
||||||
app.component("MenuList", MenuList);
|
app.component("MenuList", MenuList);
|
||||||
app.component("ArticleMetadata", ArticleMetadata);
|
app.component("ArticleMetadata", ArticleMetadata);
|
||||||
|
app.component("VPSwiper", VPSwiper);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,16 +23,17 @@ export const routers = [
|
|||||||
{ text: 'Spring IOC简介', link: '/Web/SpringBoot/Spring-IOC' },
|
{ text: 'Spring IOC简介', link: '/Web/SpringBoot/Spring-IOC' },
|
||||||
{ text: 'Spring AOP简介', link: '/Web/SpringBoot/Spring-AOP' },
|
{ text: 'Spring AOP简介', link: '/Web/SpringBoot/Spring-AOP' },
|
||||||
{ text: 'Spring MVC简介', link: '/Web/SpringBoot/Spring-MVC' },
|
{ text: 'Spring MVC简介', link: '/Web/SpringBoot/Spring-MVC' },
|
||||||
{ text: 'SpringBoot启动流程', link: '/Web/SpringBoot/SpringBoot-Start-Process' },
|
{ text: '启动流程', link: '/Web/SpringBoot/SpringBoot-Start-Process' },
|
||||||
{ text: 'SpringBoot Starter原理', link: '/Web/SpringBoot/SpringBoot-Starter' },
|
{ text: 'Starter原理', link: '/Web/SpringBoot/SpringBoot-Starter' },
|
||||||
{ text: 'SpringBoot Bean简介', link: '/Web/SpringBoot/SpringBoot-Bean' },
|
{ text: 'Bean简介', link: '/Web/SpringBoot/SpringBoot-Bean' },
|
||||||
{ text: 'SpringBoot注解', link: '/Web/SpringBoot/SpringBoot-Annotation' },
|
{ text: '注解', link: '/Web/SpringBoot/SpringBoot-Annotation' },
|
||||||
{ text: 'SpringBoot3原生镜像', link: '/Web/SpringBoot/SpringBoot3-GraalVM' },
|
{ text: 'SpringBoot3原生镜像', link: '/Web/SpringBoot/SpringBoot3-GraalVM' },
|
||||||
{ text: 'SpringBoot技巧', link: '/Web/SpringBoot/SpringBoot-Skills' },
|
{ text: 'SpringBoot技巧', link: '/Web/SpringBoot/SpringBoot-Skills' },
|
||||||
{ text: 'SpringBoot RestClient简介', link: '/Web/SpringBoot/SpringBoot-RestClient' },
|
{ text: 'RestClient简介', link: '/Web/SpringBoot/SpringBoot-RestClient' },
|
||||||
{ text: 'SpringBoot Redis简介和使用', link: '/Web/SpringBoot/SpringBoot-Redis' },
|
{ text: 'Redis简介和使用', link: '/Web/SpringBoot/SpringBoot-Redis' },
|
||||||
{ text: 'SpringBoot WebSocket使用', link: '/Web/SpringBoot/SpringBoot-WebSocket' },
|
{ text: 'WebSocket简介和使用', link: '/Web/SpringBoot/SpringBoot-WebSocket' },
|
||||||
{ text: 'SpringBoot 线程与事件', link: '/Web/SpringBoot/SpringBoot-Thread' }
|
{ text: 'Quarzt简介和使用', link: '/Web/SpringBoot/SpringBoot-Quartz' },
|
||||||
|
{ text: '线程与事件', link: '/Web/SpringBoot/SpringBoot-Thread' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -116,7 +117,8 @@ export const routers = [
|
|||||||
{
|
{
|
||||||
text: '👤 个人项目',
|
text: '👤 个人项目',
|
||||||
items: [
|
items: [
|
||||||
{ text: '家庭管理系统', link: '/Practice/Personal/SweetHut' }
|
{ text: '家庭管理系统', link: '/Practice/Personal/SweetHut' },
|
||||||
|
{ text: '个人博客系统', link: '/Practice/Personal/Blog' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -36,6 +36,10 @@
|
|||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vp-swiper-button {
|
||||||
|
color: #bd34fe;
|
||||||
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
color: #bd34fe;
|
color: #bd34fe;
|
||||||
}
|
}
|
||||||
88
docs/Practice/Personal/Blog.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
title: 个人博客系统
|
||||||
|
date: 2026-06-08
|
||||||
|
---
|
||||||
|
|
||||||
|
# 一、项目背景
|
||||||
|
  为满足个人技术积累与内容分享需求,独立开发了一套多端博客系统,支持博客管理、发布与多端阅读。
|
||||||
|
  系统包含博客网站、博客管理后台与移动端 App,覆盖 Web 端与移动端用户场景,实现了从内容创作、存储、检索到多端展示的完整闭环。
|
||||||
|
|
||||||
|
# 二、系统架构
|
||||||
|
|
||||||
|
# 三、核心模块
|
||||||
|
## 3.1 博客网站
|
||||||
|
1. 首页博客列表展示(分页、置顶)
|
||||||
|
2. 博客分类与标签筛选
|
||||||
|
3. 博客归档(按时间维度)
|
||||||
|
4. 博客全文搜索(Elasticsearch)
|
||||||
|
5. Markdown 文章渲染与代码高亮
|
||||||
|
6. 博客阅读时长和阅读人数统计
|
||||||
|
7. 评论发布与互动(匿名)
|
||||||
|
|
||||||
|
<VPSwiper
|
||||||
|
:slides="[
|
||||||
|
'../images/blog_home.png',
|
||||||
|
'../images/blog_category.png',
|
||||||
|
'../images/blog_archive.png',
|
||||||
|
'../images/blog_search.png',
|
||||||
|
'../images/blog_content.png',
|
||||||
|
'../images/blog_commit.png'
|
||||||
|
]"
|
||||||
|
button-text="点击全屏"
|
||||||
|
:navigation="false"
|
||||||
|
effect="coverflow"
|
||||||
|
/>
|
||||||
|
|
||||||
|
## 3.2 博客管理后台
|
||||||
|
1. 博客新增、编辑、删除(权限控制)
|
||||||
|
2. Markdown 编辑器(md-editor-v3)
|
||||||
|
3. 博客图片上传(RustFS)
|
||||||
|
4. 分类 / 标签管理
|
||||||
|
5. 博客状态管理(草稿 / 发布)
|
||||||
|
6. 博客访问数据查看
|
||||||
|
7. 统计数据仪表盘(ECharts)
|
||||||
|
|
||||||
|
<VPSwiper
|
||||||
|
:slides="[
|
||||||
|
'../images/blog_login.png',
|
||||||
|
'../images/blog_stats.png',
|
||||||
|
'../images/blog_article.png',
|
||||||
|
'../images/blog_edit.png'
|
||||||
|
]"
|
||||||
|
button-text="点击全屏"
|
||||||
|
:navigation="false"
|
||||||
|
effect="coverflow"
|
||||||
|
/>
|
||||||
|
|
||||||
|
## 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。
|
||||||
@@ -7,7 +7,6 @@ date: 2026-06-04
|
|||||||
  针对家庭信息分散、难以统一管理的问题,设计并实现了一套移动端家庭管理平台,将账单、旅行、健康、纪念日等数据进行集中管理,并通过私有化部署与数据加密保障安全,实现家庭生活的数字化沉淀。
|
  针对家庭信息分散、难以统一管理的问题,设计并实现了一套移动端家庭管理平台,将账单、旅行、健康、纪念日等数据进行集中管理,并通过私有化部署与数据加密保障安全,实现家庭生活的数字化沉淀。
|
||||||
|
|
||||||
# 二、系统架构
|
# 二、系统架构
|
||||||

|
|
||||||
|
|
||||||
# 三、核心模块
|
# 三、核心模块
|
||||||
- **账单记录**
|
- **账单记录**
|
||||||
|
|||||||
BIN
docs/Practice/images/blog_archive.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
docs/Practice/images/blog_article.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
docs/Practice/images/blog_category.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
docs/Practice/images/blog_commit.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
docs/Practice/images/blog_content.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
docs/Practice/images/blog_edit.png
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
docs/Practice/images/blog_home.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
docs/Practice/images/blog_login.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/Practice/images/blog_search.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
docs/Practice/images/blog_stats.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
120
docs/Web/SpringBoot/SpringBoot-Quartz.md
Normal file
@@ -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
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-quartz</artifactId>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
  持久化:
|
||||||
|
```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);
|
||||||
|
```
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: SpringBoot WebSocket使用
|
title: SpringBoot WebSocket简介和使用
|
||||||
date: 2026-06-08
|
date: 2026-06-08
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,26 @@ npm install @vueuse/core
|
|||||||
```
|
```
|
||||||
|
|
||||||
# 二、使用
|
# 二、使用
|
||||||
## 2.1 useWebSocket
|
## 2.1 实时获取当前时间
|
||||||
|
```ts
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<p>当前时间: {{ formattedTime }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useNow, useDateFormat } from '@vueuse/core'
|
||||||
|
|
||||||
|
// 获取当前时间响应式对象
|
||||||
|
const now = useNow()
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formattedTime = useDateFormat(now, 'YYYY-MM-DD HH:mm:ss')
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2.2 useWebSocket
|
||||||
```ts
|
```ts
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
iconst { status, send } = useWebSocket('wss://example.com/ws', {
|
iconst { status, send } = useWebSocket('wss://example.com/ws', {
|
||||||
@@ -35,4 +54,105 @@ iconst { status, send } = useWebSocket('wss://example.com/ws', {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 2.3 防抖和节流
|
||||||
|
| | 防抖 Debounce | 节流 Throttle |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **执行时机** | 连续触发 **停下后** | **每隔一段时间** |
|
||||||
|
| **执行次数** | 通常 1 次(最后) | 多次,但受限制 |
|
||||||
|
| **典型场景** | 搜索联想、校验 | 滚动、拖拽 |
|
||||||
|
|
||||||
|
  防抖:
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<input @input="handleInput" placeholder="输入搜索关键字" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
|
||||||
|
const keyword = ref('')
|
||||||
|
|
||||||
|
// 输入停止 500ms 后执行搜索
|
||||||
|
const debouncedSearch = useDebounceFn((val) => {
|
||||||
|
console.log('发起搜索请求:', val)
|
||||||
|
// api.search(val)
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
const handleInput = (e) => {
|
||||||
|
keyword.value = e.target.value
|
||||||
|
debouncedSearch(keyword.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
  节流:
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div style="padding: 20px">
|
||||||
|
<p>点击次数(被节流):{{ count }}</p>
|
||||||
|
<button @click="throttledClick">
|
||||||
|
狂点我(节流 500ms)
|
||||||
|
</button>
|
||||||
|
<p style="color:#999;font-size:12px">
|
||||||
|
快速连点也不会一直 +1,每 500ms 最多加一次
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useThrottleFn } from '@vueuse/core'
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
|
||||||
|
// 节流函数:500ms 内最多执行一次
|
||||||
|
const throttledClick = useThrottleFn(() => {
|
||||||
|
count.value++
|
||||||
|
console.log('真正执行 +1')
|
||||||
|
}, 500)
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
如果是表单提交,应该使用状态锁来限制,即当提交后,将按钮置为loading状态,提交成功后再重置。
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
{{ loading ? '提交中...' : '提交' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (loading.value) return
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟接口请求
|
||||||
|
await submitForm()
|
||||||
|
ElMessage.success('提交成功')
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('提交失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = () => {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, 1500))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
:::
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"deploy": "npm run build && node deploy.js"
|
"deploy": "npm run build && node deploy.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@cssnr/vitepress-swiper": "^0.3.1",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"viewerjs": "^1.11.7",
|
"viewerjs": "^1.11.7",
|
||||||
|
|||||||
20
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@cssnr/vitepress-swiper':
|
||||||
|
specifier: ^0.3.1
|
||||||
|
version: 0.3.1(swiper@12.2.0)(vue@3.5.25)
|
||||||
echarts:
|
echarts:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
@@ -156,6 +159,12 @@ packages:
|
|||||||
'@chevrotain/utils@11.0.3':
|
'@chevrotain/utils@11.0.3':
|
||||||
resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==}
|
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':
|
'@docsearch/css@3.8.2':
|
||||||
resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==}
|
resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==}
|
||||||
|
|
||||||
@@ -1156,6 +1165,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==}
|
resolution: {integrity: sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
swiper@12.2.0:
|
||||||
|
resolution: {integrity: sha512-K8uXsBZU6ME97Ia3xbBge8IRCnR1lOmIILzvY/jGVic7dSTQ530s3uO8RvXbPUtkkXLWIwmZLRPbtDxRWVAFdg==}
|
||||||
|
engines: {node: '>= 4.7.0'}
|
||||||
|
|
||||||
tabbable@6.3.0:
|
tabbable@6.3.0:
|
||||||
resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==}
|
resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==}
|
||||||
|
|
||||||
@@ -1457,6 +1470,11 @@ snapshots:
|
|||||||
|
|
||||||
'@chevrotain/utils@11.0.3': {}
|
'@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/css@3.8.2': {}
|
||||||
|
|
||||||
'@docsearch/js@3.8.2(@algolia/client-search@5.45.0)(search-insights@2.17.3)':
|
'@docsearch/js@3.8.2(@algolia/client-search@5.45.0)(search-insights@2.17.3)':
|
||||||
@@ -2504,6 +2522,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
copy-anything: 4.0.5
|
copy-anything: 4.0.5
|
||||||
|
|
||||||
|
swiper@12.2.0: {}
|
||||||
|
|
||||||
tabbable@6.3.0: {}
|
tabbable@6.3.0: {}
|
||||||
|
|
||||||
tinyexec@1.0.2: {}
|
tinyexec@1.0.2: {}
|
||||||
|
|||||||