feat(image): 实现 Markdown 图片文件名同步
- 新增 ImageName 实体类和对应的 Mapper- 在 MarkdownFileService 中添加图片文件名同步方法 - 优化 HomePage 组件,支持实时预览 Markdown 内容 - 新增 MarkdownImageExtractor 工具类,用于提取 Markdown 中的图片文件名
This commit is contained in:
@@ -2,10 +2,12 @@ package com.test.bijihoudaun;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
|
@EnableAsync
|
||||||
public class BijiHoudaunApplication {
|
public class BijiHoudaunApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.test.bijihoudaun.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableAsync
|
||||||
|
public class AsyncConfig {
|
||||||
|
|
||||||
|
@Bean("imageNameSyncExecutor")
|
||||||
|
public Executor imageNameSyncExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(5);
|
||||||
|
executor.setMaxPoolSize(10);
|
||||||
|
executor.setQueueCapacity(100);
|
||||||
|
executor.setThreadNamePrefix("imageNameSync-");
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.test.bijihoudaun.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(name = "图片名称实体")
|
||||||
|
@TableName("image_name")
|
||||||
|
public class ImageName {
|
||||||
|
@Schema(description = "图片名称id", implementation = Long.class)
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "关联的Markdown文件ID", implementation = Long.class)
|
||||||
|
@TableField("markdown_id")
|
||||||
|
private Long markdownId;
|
||||||
|
|
||||||
|
@Schema(description = "文件名", implementation = String.class)
|
||||||
|
@TableField("file_name")
|
||||||
|
private String fileName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.test.bijihoudaun.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.test.bijihoudaun.entity.ImageName;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ImageNameMapper extends BaseMapper<ImageName> {
|
||||||
|
// 可以在这里添加自定义方法
|
||||||
|
}
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
package com.test.bijihoudaun.service.impl;
|
package com.test.bijihoudaun.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.test.bijihoudaun.entity.ImageName;
|
||||||
import com.test.bijihoudaun.entity.MarkdownFile;
|
import com.test.bijihoudaun.entity.MarkdownFile;
|
||||||
import com.test.bijihoudaun.entity.MarkdownFileVO;
|
import com.test.bijihoudaun.entity.MarkdownFileVO;
|
||||||
|
import com.test.bijihoudaun.mapper.ImageNameMapper;
|
||||||
import com.test.bijihoudaun.mapper.MarkdownFileMapper;
|
import com.test.bijihoudaun.mapper.MarkdownFileMapper;
|
||||||
import com.test.bijihoudaun.service.MarkdownFileService;
|
import com.test.bijihoudaun.service.MarkdownFileService;
|
||||||
|
import com.test.bijihoudaun.util.MarkdownImageExtractor;
|
||||||
import com.test.bijihoudaun.util.SnowflakeIdGenerator;
|
import com.test.bijihoudaun.util.SnowflakeIdGenerator;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@@ -23,20 +28,30 @@ public class MarkdownFileServiceImpl
|
|||||||
@Autowired
|
@Autowired
|
||||||
MarkdownFileMapper markdownFileMapper;
|
MarkdownFileMapper markdownFileMapper;
|
||||||
@Resource
|
@Resource
|
||||||
|
ImageNameMapper imageNameMapper;
|
||||||
|
@Resource
|
||||||
SnowflakeIdGenerator snowflakeIdGenerator;
|
SnowflakeIdGenerator snowflakeIdGenerator;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MarkdownFile updateMarkdownContent(MarkdownFile markdownFile) {
|
public MarkdownFile updateMarkdownContent(MarkdownFile markdownFile) {
|
||||||
|
long id;
|
||||||
markdownFile.setUpdatedAt(new Date());
|
markdownFile.setUpdatedAt(new Date());
|
||||||
// 如果ID为空或0,则视为新文件
|
// 如果ID为空或0,则视为新文件
|
||||||
if (markdownFile.getId() == null || markdownFile.getId() == 0L) {
|
if (markdownFile.getId() == null || markdownFile.getId() == 0L) {
|
||||||
markdownFile.setId(snowflakeIdGenerator.nextId());
|
long l = snowflakeIdGenerator.nextId();
|
||||||
|
markdownFile.setId(l);
|
||||||
markdownFile.setCreatedAt(new Date());
|
markdownFile.setCreatedAt(new Date());
|
||||||
this.save(markdownFile); // 使用MyBatis-Plus的save方法
|
this.save(markdownFile); // 使用MyBatis-Plus的save方法
|
||||||
|
id=l;
|
||||||
} else {
|
} else {
|
||||||
this.updateById(markdownFile); // 使用MyBatis-Plus的updateById方法
|
this.updateById(markdownFile); // 使用MyBatis-Plus的updateById方法
|
||||||
|
id=markdownFile.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> strings = MarkdownImageExtractor.extractImageFilenames(markdownFile.getContent());
|
||||||
|
// 异步处理图片文件名同步
|
||||||
|
syncImageNames(id, strings);
|
||||||
return markdownFile;
|
return markdownFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +77,7 @@ public class MarkdownFileServiceImpl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<MarkdownFile> test() {
|
public List<MarkdownFile> test() {
|
||||||
List<MarkdownFile> markdownFiles = markdownFileMapper.selectList(null);
|
return markdownFileMapper.selectList(null);
|
||||||
return markdownFiles;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -93,4 +107,63 @@ public class MarkdownFileServiceImpl
|
|||||||
public List<MarkdownFileVO> getRecentFiles(int limit) {
|
public List<MarkdownFileVO> getRecentFiles(int limit) {
|
||||||
return markdownFileMapper.selectRecentWithGrouping(limit);
|
return markdownFileMapper.selectRecentWithGrouping(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Async("imageNameSyncExecutor")
|
||||||
|
public void syncImageNames(Long markdownId, List<String> strings) {
|
||||||
|
// 查询数据库中已存在的文件名
|
||||||
|
List<ImageName> imageNames = imageNameMapper.selectList(new LambdaUpdateWrapper<ImageName>()
|
||||||
|
.eq(ImageName::getMarkdownId, markdownId));
|
||||||
|
|
||||||
|
// 若是数据库中的数据为null,则插入
|
||||||
|
if (CollUtil.isEmpty(imageNames)) {
|
||||||
|
if (CollUtil.isNotEmpty(strings)) {
|
||||||
|
List<ImageName> list = strings.stream().map(fileName -> {
|
||||||
|
ImageName imageName = new ImageName();
|
||||||
|
imageName.setFileName(fileName);
|
||||||
|
imageName.setMarkdownId(markdownId);
|
||||||
|
return imageName;
|
||||||
|
}).toList();
|
||||||
|
// 批量插入新的文件名
|
||||||
|
list.forEach(imageName -> imageNameMapper.insert(imageName));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 数据库中已有记录,需要对比处理
|
||||||
|
// 获取数据库中的文件名列表
|
||||||
|
List<String> dbFileNames = imageNames.stream()
|
||||||
|
.map(ImageName::getFileName)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 找出需要新增的文件名(在strings中但不在数据库中)
|
||||||
|
List<String> toInsert = strings.stream()
|
||||||
|
.filter(fileName -> !dbFileNames.contains(fileName))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 找出需要删除的记录(在数据库中但不在strings中)
|
||||||
|
List<ImageName> toDelete = imageNames.stream()
|
||||||
|
.filter(imageName -> !strings.contains(imageName.getFileName()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 插入新增的文件名
|
||||||
|
if (CollUtil.isNotEmpty(toInsert)) {
|
||||||
|
List<ImageName> insertList = toInsert.stream().map(fileName -> {
|
||||||
|
ImageName imageName = new ImageName();
|
||||||
|
imageName.setFileName(fileName);
|
||||||
|
imageName.setMarkdownId(markdownId);
|
||||||
|
return imageName;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
imageNameMapper.insert(insertList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除不再需要的记录
|
||||||
|
if (CollUtil.isNotEmpty(toDelete)) {
|
||||||
|
List<Long> deleteIds = toDelete.stream()
|
||||||
|
.map(ImageName::getId)
|
||||||
|
.toList();
|
||||||
|
imageNameMapper.deleteByIds(deleteIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.test.bijihoudaun.util;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markdown图片提取工具类
|
||||||
|
* 用于从Markdown文本中提取图片文件名
|
||||||
|
*/
|
||||||
|
public class MarkdownImageExtractor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从Markdown内容中提取图片文件名
|
||||||
|
* 支持各种URL格式:
|
||||||
|
* - 绝对URL: http://example.com/path/uuid.png
|
||||||
|
* - 相对URL: /path/uuid.png
|
||||||
|
* - 协议相对URL: //example.com/path/uuid.png
|
||||||
|
*
|
||||||
|
* @param markdownContent 包含图片的Markdown文本
|
||||||
|
* @return 图片文件名列表
|
||||||
|
*/
|
||||||
|
public static List<String> extractImageFilenames(String markdownContent) {
|
||||||
|
if (markdownContent == null || markdownContent.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用正则表达式匹配Markdown图片语法中的文件名
|
||||||
|
// 模式:  其中url以UUID格式的文件名结尾
|
||||||
|
Pattern pattern = Pattern.compile("!\\[.*?\\]\\([^)]*?([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\\.[a-zA-Z0-9]+)\\)");
|
||||||
|
Matcher matcher = pattern.matcher(markdownContent);
|
||||||
|
|
||||||
|
List<String> filenames = new ArrayList<>();
|
||||||
|
while (matcher.find()) {
|
||||||
|
filenames.add(matcher.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return filenames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查Markdown内容中是否包含指定的图片文件名
|
||||||
|
*
|
||||||
|
* @param markdownContent Markdown文本
|
||||||
|
* @param filename 要查找的文件名
|
||||||
|
* @return 是否包含该文件名
|
||||||
|
*/
|
||||||
|
public static boolean containsImage(String markdownContent, String filename) {
|
||||||
|
if (markdownContent == null || filename == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractImageFilenames(markdownContent).contains(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从Markdown内容中提取图片URL
|
||||||
|
*
|
||||||
|
* @param markdownContent 包含图片的Markdown文本
|
||||||
|
* @return 图片URL列表
|
||||||
|
*/
|
||||||
|
public static List<String> extractImageUrls(String markdownContent) {
|
||||||
|
if (markdownContent == null || markdownContent.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配Markdown图片语法中的完整URL
|
||||||
|
Pattern pattern = Pattern.compile("!\\[.*?\\]\\(([^)]+)\\)");
|
||||||
|
Matcher matcher = pattern.matcher(markdownContent);
|
||||||
|
|
||||||
|
List<String> urls = new ArrayList<>();
|
||||||
|
while (matcher.find()) {
|
||||||
|
urls.add(matcher.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.sqlite.JDBC
|
driver-class-name: org.sqlite.JDBC
|
||||||
# url: jdbc:sqlite:C:\it\houtaigunli\biji\mydatabase.db
|
url: jdbc:sqlite:C:\it\houtaigunli\biji\mydatabase.db
|
||||||
url: jdbc:sqlite:C:\KAIFA\2\mydatabase.db
|
# url: jdbc:sqlite:C:\KAIFA\2\mydatabase.db
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: none
|
ddl-auto: none
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
<div v-if="!showEditor" v-html="previewHtml" class="markdown-preview"></div>
|
<div v-if="!showEditor" :key="selectedFile.id" class="markdown-preview"></div>
|
||||||
<!-- Vditor 编辑器 -->
|
<!-- Vditor 编辑器 -->
|
||||||
<div v-show="showEditor" id="vditor" class="vditor" />
|
<div v-show="showEditor" id="vditor" class="vditor" />
|
||||||
</div>
|
</div>
|
||||||
@@ -350,7 +350,6 @@ const imageUrls = ref([]);
|
|||||||
const originalImages = ref([]);
|
const originalImages = ref([]);
|
||||||
|
|
||||||
const vditor = ref(null);
|
const vditor = ref(null);
|
||||||
const previewHtml = ref('');
|
|
||||||
const saveStatus = ref('空闲');
|
const saveStatus = ref('空闲');
|
||||||
let debounceTimer = null;
|
let debounceTimer = null;
|
||||||
|
|
||||||
@@ -560,34 +559,21 @@ const renderMenu = (item) => {
|
|||||||
// 选择文件预览
|
// 选择文件预览
|
||||||
const previewFile = async (file) => {
|
const previewFile = async (file) => {
|
||||||
if (file.id === null) {
|
if (file.id === null) {
|
||||||
editData.value=file
|
editData.value = file;
|
||||||
selectedFile.value=null
|
selectedFile.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await Preview(file.id)
|
const response = await Preview(file.id);
|
||||||
|
const content = String(response || '');
|
||||||
// 确保内容为字符串
|
|
||||||
const content = String(response.data || '');
|
|
||||||
|
|
||||||
selectedFile.value = {
|
selectedFile.value = {
|
||||||
...file,
|
...file,
|
||||||
content: content
|
content: content
|
||||||
};
|
};
|
||||||
await nextTick();
|
showEditor.value = false; // 确保进入预览模式
|
||||||
const previewElement = document.querySelector('.markdown-preview');
|
|
||||||
if (previewElement) {
|
|
||||||
Vditor.preview(previewElement, content, {
|
|
||||||
// 在这里提供一个基本的配置对象
|
|
||||||
mode: 'light', // 或者 'dark',可以根据当前主题动态设置
|
|
||||||
hljs: {
|
|
||||||
enable: true,
|
|
||||||
style: 'github'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取笔记内容失败: ' + error.message);
|
ElMessage.error('获取笔记内容失败: ' + error.message);
|
||||||
|
selectedFile.value = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -993,6 +979,23 @@ watch(activeMenu, (newVal) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch([selectedFile, showEditor], ([newFile, newShowEditor]) => {
|
||||||
|
if (newFile && !newShowEditor) {
|
||||||
|
nextTick(() => {
|
||||||
|
const previewElement = document.querySelector('.markdown-preview');
|
||||||
|
if (previewElement) {
|
||||||
|
Vditor.preview(previewElement, newFile.content, {
|
||||||
|
mode: 'light',
|
||||||
|
hljs: {
|
||||||
|
enable: true,
|
||||||
|
style: 'github'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
const handleToggleRegistration = async (value) => {
|
const handleToggleRegistration = async (value) => {
|
||||||
try {
|
try {
|
||||||
await toggleRegistration(value);
|
await toggleRegistration(value);
|
||||||
|
|||||||
BIN
mydatabase.db
BIN
mydatabase.db
Binary file not shown.
29
sql/image_name.sql
Normal file
29
sql/image_name.sql
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
Navicat Premium Dump SQL
|
||||||
|
|
||||||
|
Source Server : biji数据库
|
||||||
|
Source Server Type : SQLite
|
||||||
|
Source Server Version : 3045000 (3.45.0)
|
||||||
|
Source Schema : main
|
||||||
|
|
||||||
|
Target Server Type : SQLite
|
||||||
|
Target Server Version : 3045000 (3.45.0)
|
||||||
|
File Encoding : 65001
|
||||||
|
|
||||||
|
Date: 01/08/2025 21:41:44
|
||||||
|
*/
|
||||||
|
|
||||||
|
PRAGMA foreign_keys = false;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for image_name
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS "image_name";
|
||||||
|
CREATE TABLE "image_name" (
|
||||||
|
"id" INTEGER NOT NULL,
|
||||||
|
"markdown_id" INTEGER NOT NULL,
|
||||||
|
"file_name" text NOT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
PRAGMA foreign_keys = true;
|
||||||
Reference in New Issue
Block a user