NO. 0211 — — 2025.02.11
扩展编辑器,整合搜索...问题解决清单捏:)
浏览 185 喜欢 3 评论 3
浅更新一下,最近压力比较大,几个项目同时并行,还要修好多大佬提出的 bug,有点痛并快乐的感觉吧,闲言少叙…
扩展 Toast UI Markdown 编辑器 React 组件,实现图片上传与其他功能
这篇简述一下 Toast UI 编辑器的自定义事件处理(以图片上传为例)
首先知道我们要处理什么,一个是插入图片是将原来的直接输入 base64 的方式改为上传到后端,然后要封装为通用组件在所有地方使用
图片上传
首先我们追一下源码
我们发现这里预留了一个 hook,其接收两个参数,一个是文件(二进制),还有一个回调函数
于是我们可以写一个函数来完成上传
typescript
const handleImageUpload = async (
blob: Blob,
callback: (url: string, altText: string) => void,
) => {
const formData = new FormData();
formData.append('file', blob);
const response = await fetch('/api/v1/upload', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + getToken(),
},
body: formData,
});
const res = await response.json();
if (res.code === 0) {
message.success('图片已上传并插入');
} else {
message.error('图片上传中遇到问题');
}
const imageUrl = `${window.location.href.replace(location.pathname, '')}${
res.data
}`;
callback(imageUrl, 'image');
};
封装组件
组件这里可能就是一点不太一样,因为我们需要 ref 拿到其实例上的函数 getMarkdown(),我们需要将 ref 做一下传递:
typescript
useImperativeHandle(ref, () => ({
getInstance: () => editorRef.current?.getInstance(),
getRootElement: () => editorRef.current?.getRootElement() || null,
setMarkdown: (markdown: string) => {
editorRef.current?.getInstance().setMarkdown(markdown);
},
context: editorRef.current?.context,
setState: editorRef.current?.setState,
forceUpdate: editorRef.current?.forceUpdate,
}));
我们使用 useImperativeHandle 传递之后,然后可以封装为组件
typescript
import { getToken } from '@/utils/token';
import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
import '@toast-ui/editor/dist/toastui-editor.css';
import { Editor } from '@toast-ui/react-editor';
import { Button, message } from 'antd';
import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
type EditorHandle = {
getInstance: () => Editor | null;
getRootElement: () => HTMLElement | null;
context: any;
setState: any;
forceUpdate: any;
};
const CustomEditor = forwardRef<EditorHandle, NonNullable<unknown>>(
(props, ref) => {
const editorRef = useRef<Editor>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
useImperativeHandle(ref, () => ({
getInstance: () => editorRef.current?.getInstance(),
getRootElement: () => editorRef.current?.getRootElement() || null,
setMarkdown: (markdown: string) => {
editorRef.current?.getInstance().setMarkdown(markdown);
},
context: editorRef.current?.context,
setState: editorRef.current?.setState,
forceUpdate: editorRef.current?.forceUpdate,
}));
const handleImageUpload = async (
blob: Blob,
callback: (url: string, altText: string) => void,
) => {
const formData = new FormData();
formData.append('file', blob);
const response = await fetch('/api/v1/upload', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + getToken(),
},
body: formData,
});
const res = await response.json();
if (res.code === 0) {
message.success('图片已上传并插入');
} else {
message.error('图片上传中遇到问题');
}
const imageUrl = `${window.location.href.replace(location.pathname, '')}${
res.data
}`;
callback(imageUrl, 'image');
};
function enterFullscreen(element: HTMLElement) {
if (element.requestFullscreen) {
element.requestFullscreen();
}
}
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
const toggleFullscreen = () => {
setIsFullscreen(!isFullscreen);
if (!isFullscreen) {
enterFullscreen(document.documentElement);
} else {
exitFullscreen();
}
};
return (
<div
style={
isFullscreen
? {
display: 'flex',
flexDirection: 'column',
transition: 'all 0.5s',
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'white',
zIndex: 9999,
padding: '20px',
}
: {
display: 'flex',
flexDirection: 'column',
transition: 'all 0.5s',
}
}
>
<div
style={{
marginBottom: '10px',
padding: '8px',
}}
>
<span>
想要写点什么呢 ٩(๑˃̵ᴗ˂̵๑)۶ {!isFullscreen && '| 更好的编辑体验?试试'}
</span>
<Button
type="link"
size={'small'}
icon={
isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />
}
onClick={toggleFullscreen}
>
{isFullscreen ? '退出全屏' : '全屏'}
</Button>
</div>
<Editor
initialValue=""
ref={editorRef}
initialEditType="markdown"
previewStyle="vertical"
height={isFullscreen ? '100vh' : '600px'}
useCommandShortcut={true}
hooks={{
addImageBlobHook: handleImageUpload,
}}
/>
</div>
);
},
);
export default CustomEditor;
然后我们在所需地方直接使用即可:
typescript
<CustomEditor ref={editorRef} />
用 ES 迁移到 MeiliSearch
要被es折磨疯了…对于小服务器和轻需求,它实在太重了!!!
实现迁移非常简单,首先是配置文件:
java
package com.grtsinry43.grtblog.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.meilisearch.sdk.Client;
import com.meilisearch.sdk.Config;
/**
* @author grtsinry43
* @date 2025/2/11 14:20
* @description 热爱可抵岁月漫长
*/
@Configuration
public class MeiliSearchConfig {
@Bean
public Client meiliSearchClient() {
return new Client(new Config("http://localhost:7700", "your_api_key"));
}
}
实现搜索逻辑:
java
package com.grtsinry43.grtblog.service;
import com.grtsinry43.grtblog.dto.AggregatedSearchResult;
import com.grtsinry43.grtblog.dto.AggregatedSearchResult.HighlightedArticleDocument;
import com.grtsinry43.grtblog.dto.AggregatedSearchResult.HighlightedMomentDocument;
import com.grtsinry43.grtblog.dto.AggregatedSearchResult.HighlightedPageDocument;
import com.grtsinry43.grtblog.esdao.ArticleDocument;
import com.grtsinry43.grtblog.esdao.MomentDocument;
import com.grtsinry43.grtblog.esdao.PageDocument;
import com.meilisearch.sdk.Client;
import com.meilisearch.sdk.SearchRequest;
import com.meilisearch.sdk.model.Searchable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class MeiliSearchService {
private final Client meiliSearchClient;
@Autowired
public MeiliSearchService(Client meiliSearchClient) {
this.meiliSearchClient = meiliSearchClient;
}
public AggregatedSearchResult searchAll(String keyword) {
AggregatedSearchResult result = new AggregatedSearchResult();
result.setPages(searchPages(keyword));
result.setArticles(searchArticles(keyword));
result.setMoments(searchMoments(keyword));
return result;
}
// 下面是详细搜索逻辑
}
最后配置数据同步:
java
package com.grtsinry43.grtblog.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.grtsinry43.grtblog.entity.Article;
import com.grtsinry43.grtblog.entity.Page;
import com.grtsinry43.grtblog.entity.StatusUpdate;
import com.grtsinry43.grtblog.esdao.ArticleDocument;
import com.grtsinry43.grtblog.esdao.MomentDocument;
import com.grtsinry43.grtblog.esdao.PageDocument;
import com.grtsinry43.grtblog.esrepo.ArticleRepository;
import com.grtsinry43.grtblog.esrepo.MomentRepository;
import com.grtsinry43.grtblog.esrepo.PageRepository;
import com.grtsinry43.grtblog.mapper.ArticleMapper;
import com.grtsinry43.grtblog.mapper.PageMapper;
import com.grtsinry43.grtblog.mapper.StatusUpdateMapper;
import com.meilisearch.sdk.Client;
import com.meilisearch.sdk.Index;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Service
public class MeiliDataSyncService {
private final Client meiliSearchClient;
// ...构造函数
public void syncAllRecent() {
syncArticlesToMeiliSearch(articleDocuments);
syncMomentsToMeiliSearch(statusUpdateDocuments);
syncPagesToMeiliSearch(pageDocuments);
}
public void deleteContent(Long id, String type) {
Index index = null;
if ("article".equals(type)) {
index = meiliSearchClient.index("articles");
index.deleteDocumentsByFilter("id=" + id);
} else if ("moment".equals(type)) {
index = meiliSearchClient.index("moments");
index.deleteDocumentsByFilter("id=" + id);
} else if ("page".equals(type)) {
index = meiliSearchClient.index("pages");
index.deleteDocumentsByFilter("id=" + id);
}
}
public void deleteAllContent() {
Index articleIndex = meiliSearchClient.index("articles");
articleIndex.deleteAllDocuments();
Index momentIndex = meiliSearchClient.index("moments");
momentIndex.deleteAllDocuments();
Index pageIndex = meiliSearchClient.index("pages");
pageIndex.deleteAllDocuments();
}
private void syncArticlesToMeiliSearch(List<ArticleDocument> articleDocuments) {
Index index = meiliSearchClient.index("articles");
ObjectMapper objectMapper = new ObjectMapper();
try {
String json = objectMapper.writeValueAsString(articleDocuments);
index.addDocuments(json);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
//... 另外的实现
}
实测其性能和体验有很大提高。
手记
记
2025.02.11
Loading comments...